/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.uioverrides; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.util.Log; import android.view.MotionEvent; import android.view.View; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.touch.SwipeDetector; import com.android.launcher3.util.TouchController; import com.android.quickstep.RecentsView; import com.android.quickstep.TaskView; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; /** * Touch controller for swipe interaction in Overview state */ public class OverviewSwipeController extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener { private static final String TAG = "OverviewSwipeController"; private static final float ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS = 0.1f; private static final int SINGLE_FRAME_MS = 16; // Progress after which the transition is assumed to be a success in case user does not fling private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; private final Launcher mLauncher; private final SwipeDetector mDetector; private final RecentsView mRecentsView; private final int[] mTempCords = new int[2]; private AnimatorPlaybackController mCurrentAnimation; private boolean mCurrentAnimationIsGoingUp; private boolean mNoIntercept; private boolean mSwipeDownEnabled; private float mDisplacementShift; private float mProgressMultiplier; private float mEndDisplacement; private TaskView mTaskBeingDragged; public OverviewSwipeController(Launcher launcher) { mLauncher = launcher; mRecentsView = launcher.getOverviewPanel(); mDetector = new SwipeDetector(launcher, this, SwipeDetector.VERTICAL); } private boolean canInterceptTouch() { if (mCurrentAnimation != null) { // If we are already animating from a previous state, we can intercept. return true; } if (AbstractFloatingView.getTopOpenView(mLauncher) != null) { return false; } return mLauncher.isInState(OVERVIEW); } private boolean isEventOverHotseat(MotionEvent ev) { if (mLauncher.getDeviceProfile().isVerticalBarLayout()) { return ev.getY() > mLauncher.getDragLayer().getHeight() * OVERVIEW.getVerticalProgress(mLauncher); } else { return mLauncher.getDragLayer().isEventOverHotseat(ev); } } @Override public void onAnimationCancel(Animator animation) { if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) { Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception()); mDetector.finishedScrolling(); mCurrentAnimation = null; } } @Override public boolean onControllerInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { mNoIntercept = !canInterceptTouch(); if (mNoIntercept) { return false; } // Now figure out which direction scroll events the controller will start // calling the callbacks. final int directionsToDetectScroll; boolean ignoreSlopWhenSettling = false; if (mCurrentAnimation != null) { directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; ignoreSlopWhenSettling = true; } else { mTaskBeingDragged = null; mSwipeDownEnabled = true; int currentPage = mRecentsView.getCurrentPage(); if (currentPage == 0) { // User is on home tile directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; } else { View view = mRecentsView.getChildAt(currentPage); if (mLauncher.getDragLayer().isEventOverView(view, ev) && view instanceof TaskView) { // The tile can be dragged down to open the task. mTaskBeingDragged = (TaskView) view; directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; } else if (isEventOverHotseat(ev)) { // The hotseat is being dragged directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE; mSwipeDownEnabled = false; } else { mNoIntercept = true; return false; } } } mDetector.setDetectableScrollConditions( directionsToDetectScroll, ignoreSlopWhenSettling); } if (mNoIntercept) { return false; } onControllerTouchEvent(ev); return mDetector.isDraggingOrSettling(); } @Override public boolean onControllerTouchEvent(MotionEvent ev) { return mDetector.onTouchEvent(ev); } private void reinitAnimationController(boolean goingUp) { if (!goingUp && !mSwipeDownEnabled) { goingUp = true; } if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) { // No need to init return; } if (mCurrentAnimation != null) { mCurrentAnimation.setPlayFraction(0); } mCurrentAnimationIsGoingUp = goingUp; float range = mLauncher.getAllAppsController().getShiftRange(); long maxDuration = (long) (2 * range); DragLayer dl = mLauncher.getDragLayer(); if (mTaskBeingDragged == null) { // User is either going to all apps or home mCurrentAnimation = mLauncher.getStateManager() .createAnimationToNewWorkspace(goingUp ? ALL_APPS : NORMAL, maxDuration); if (goingUp) { mEndDisplacement = -range; } else { View ws = mLauncher.getWorkspace(); mTempCords[1] = ws.getHeight() - ws.getPaddingBottom(); dl.getDescendantCoordRelativeToSelf(ws, mTempCords); float distance = mTempCords[1]; if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) { mTempCords[1] = 0; dl.getDescendantCoordRelativeToSelf(mLauncher.getHotseat(), mTempCords); distance = mTempCords[1] - distance; } else { distance = dl.getHeight() - distance; } mEndDisplacement = distance; } } else { if (goingUp) { AnimatorSet anim = new AnimatorSet(); ObjectAnimator translate = ObjectAnimator.ofFloat( mTaskBeingDragged, View.TRANSLATION_Y, -mTaskBeingDragged.getBottom()); translate.setInterpolator(LINEAR); translate.setDuration(maxDuration); anim.play(translate); ObjectAnimator alpha = ObjectAnimator.ofFloat(mTaskBeingDragged, View.ALPHA, 0); alpha.setInterpolator(DEACCEL_1_5); alpha.setDuration(maxDuration); anim.play(alpha); mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration); mEndDisplacement = -mTaskBeingDragged.getBottom(); } else { AnimatorSet anim = new AnimatorSet(); // TODO: Setup a zoom animation mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration); mTempCords[1] = mTaskBeingDragged.getHeight(); dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords); mEndDisplacement = dl.getHeight() - mTempCords[1]; } } mCurrentAnimation.getTarget().addListener(this); mCurrentAnimation.dispatchOnStart(); mProgressMultiplier = 1 / mEndDisplacement; } @Override public void onDragStart(boolean start) { if (mCurrentAnimation == null) { reinitAnimationController(mDetector.wasInitialTouchPositive()); mDisplacementShift = 0; } else { mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier; mCurrentAnimation.pause(); } } @Override public boolean onDrag(float displacement, float velocity) { float totalDisplacement = displacement + mDisplacementShift; boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0; if (isGoingUp != mCurrentAnimationIsGoingUp) { reinitAnimationController(isGoingUp); } mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier); return true; } @Override public void onDragEnd(float velocity, boolean fling) { final boolean goingToEnd; if (fling) { boolean goingUp = velocity < 0; if (!goingUp && !mSwipeDownEnabled) { goingToEnd = false; } else if (goingUp != mCurrentAnimationIsGoingUp) { // In case the fling is in opposite direction, make sure if is close enough // from the start position if (mCurrentAnimation.getProgressFraction() >= ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS) { // Not allowed goingToEnd = false; } else { reinitAnimationController(goingUp); goingToEnd = true; } } else { goingToEnd = true; } } else { goingToEnd = mCurrentAnimation.getProgressFraction() > SUCCESS_TRANSITION_PROGRESS; } float progress = mCurrentAnimation.getProgressFraction(); long animationDuration = SwipeDetector.calculateDuration( velocity, goingToEnd ? (1 - progress) : progress); float nextFrameProgress = Utilities.boundToRange( progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f); mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd)); ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f); anim.setDuration(animationDuration); anim.setInterpolator(scrollInterpolatorForVelocity(velocity)); anim.start(); } private void onCurrentAnimationEnd(boolean wasSuccess) { // TODO: Might be a good time to log something. if (mTaskBeingDragged == null) { LauncherState state = wasSuccess ? (mCurrentAnimationIsGoingUp ? ALL_APPS : NORMAL) : OVERVIEW; mLauncher.getStateManager().goToState(state, false); } else if (wasSuccess) { if (mCurrentAnimationIsGoingUp) { mRecentsView.onTaskDismissed(mTaskBeingDragged); } else { mTaskBeingDragged.launchTask(false); } } mDetector.finishedScrolling(); mTaskBeingDragged = null; mCurrentAnimation = null; } }