/* * 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.touchcontrollers; import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE; import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.os.SystemClock; import android.os.VibrationEffect; import android.view.MotionEvent; import android.view.View; import android.view.animation.Interpolator; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.touch.BaseSwipeDetector; import com.android.launcher3.touch.PagedOrientationHandler; import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.util.FlingBlockCheck; import com.android.launcher3.util.TouchController; import com.android.launcher3.views.BaseDragLayer; import com.android.quickstep.SysUINavigationMode; import com.android.quickstep.util.VibratorWrapper; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; /** * Touch controller for handling task view card swipes */ public abstract class TaskViewTouchController extends AnimatorListenerAdapter implements TouchController, SingleAxisSwipeDetector.Listener { private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f; private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300; private static final long MAX_TASK_DISMISS_ANIMATION_DURATION = 600; public static final int TASK_DISMISS_VIBRATION_PRIMITIVE = Utilities.ATLEAST_R ? VibrationEffect.Composition.PRIMITIVE_TICK : -1; public static final float TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE = 1f; public static final VibrationEffect TASK_DISMISS_VIBRATION_FALLBACK = VibratorWrapper.EFFECT_TEXTURE_TICK; protected final T mActivity; private final SingleAxisSwipeDetector mDetector; private final RecentsView mRecentsView; private final int[] mTempCords = new int[2]; private final boolean mIsRtl; private AnimatorPlaybackController mCurrentAnimation; private boolean mCurrentAnimationIsGoingUp; private boolean mAllowGoingUp; private boolean mAllowGoingDown; private boolean mNoIntercept; private float mDisplacementShift; private float mProgressMultiplier; private float mEndDisplacement; private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); private Float mOverrideVelocity = null; private TaskView mTaskBeingDragged; private boolean mIsDismissHapticRunning = false; public TaskViewTouchController(T activity) { mActivity = activity; mRecentsView = activity.getOverviewPanel(); mIsRtl = Utilities.isRtl(activity.getResources()); SingleAxisSwipeDetector.Direction dir = mRecentsView.getPagedOrientationHandler().getUpDownSwipeDirection(); mDetector = new SingleAxisSwipeDetector(activity, this, dir); } private boolean canInterceptTouch(MotionEvent ev) { if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0) { // Don't intercept swipes on the nav bar, as user might be trying to go home // during a task dismiss animation. if (mCurrentAnimation != null) { mCurrentAnimation.getAnimationPlayer().end(); } return false; } if (mCurrentAnimation != null) { mCurrentAnimation.forceFinishIfCloseToEnd(); } if (mCurrentAnimation != null) { // If we are already animating from a previous state, we can intercept. return true; } if (AbstractFloatingView.getTopOpenViewWithType(mActivity, TYPE_ACCESSIBLE) != null) { return false; } return isRecentsInteractive(); } protected abstract boolean isRecentsInteractive(); /** Is recents view showing a single task in a modal way. */ protected abstract boolean isRecentsModal(); protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { } @Override public void onAnimationCancel(Animator animation) { if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) { clearState(); } } @Override public boolean onControllerInterceptTouchEvent(MotionEvent ev) { if ((ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) && mCurrentAnimation == null) { clearState(); } if (ev.getAction() == MotionEvent.ACTION_DOWN) { mNoIntercept = !canInterceptTouch(ev); if (mNoIntercept) { return false; } // Now figure out which direction scroll events the controller will start // calling the callbacks. int directionsToDetectScroll = 0; boolean ignoreSlopWhenSettling = false; if (mCurrentAnimation != null) { directionsToDetectScroll = DIRECTION_BOTH; ignoreSlopWhenSettling = true; } else { mTaskBeingDragged = null; for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) { TaskView view = mRecentsView.getTaskViewAt(i); if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer() .isEventOverView(view, ev)) { // Disable swiping up and down if the task overlay is modal. if (isRecentsModal()) { mTaskBeingDragged = null; break; } mTaskBeingDragged = view; int upDirection = mRecentsView.getPagedOrientationHandler() .getUpDirection(mIsRtl); // The task can be dragged up to dismiss it mAllowGoingUp = true; // The task can be dragged down to open it if: // - It's the current page // - We support gestures to enter overview // - It's the focused task if in grid view // - The task is snapped mAllowGoingDown = i == mRecentsView.getCurrentPage() && SysUINavigationMode.getMode(mActivity).hasGestures && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask()) && mRecentsView.isTaskInExpectedScrollPosition(i); directionsToDetectScroll = mAllowGoingDown ? DIRECTION_BOTH : upDirection; break; } } if (mTaskBeingDragged == null) { 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 (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) { // No need to init return; } if ((goingUp && !mAllowGoingUp) || (!goingUp && !mAllowGoingDown)) { // Trying to re-init in an unsupported direction. return; } if (mCurrentAnimation != null) { mCurrentAnimation.setPlayFraction(0); mCurrentAnimation.getTarget().removeListener(this); mCurrentAnimation.dispatchOnCancel(); } PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler(); mCurrentAnimationIsGoingUp = goingUp; BaseDragLayer dl = mActivity.getDragLayer(); final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl); long maxDuration = 2 * secondaryLayerDimension; int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl); int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged); // The interpolator controlling the most prominent visual movement. We use this to determine // whether we passed SUCCESS_TRANSITION_PROGRESS. final Interpolator currentInterpolator; PendingAnimation pa; if (goingUp) { currentInterpolator = Interpolators.LINEAR; pa = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged, true /* animateTaskView */, true /* removeTask */, maxDuration, false /* dismissingForSplitSelection*/); mEndDisplacement = -secondaryTaskDimension; } else { currentInterpolator = Interpolators.ZOOM_IN; pa = mRecentsView.createTaskLaunchAnimation( mTaskBeingDragged, maxDuration, currentInterpolator); // Since the thumbnail is what is filling the screen, based the end displacement on it. View thumbnailView = mTaskBeingDragged.getThumbnail(); mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView); dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords); mEndDisplacement = secondaryLayerDimension - mTempCords[1]; } mEndDisplacement *= verticalFactor; mCurrentAnimation = pa.createPlaybackController(); // Setting this interpolator doesn't affect the visual motion, but is used to determine // whether we successfully reached the target state in onDragEnd(). mCurrentAnimation.getTarget().setInterpolator(currentInterpolator); onUserControlledAnimationCreated(mCurrentAnimation); mCurrentAnimation.getTarget().addListener(this); mCurrentAnimation.dispatchOnStart(); mProgressMultiplier = 1 / mEndDisplacement; } @Override public void onDragStart(boolean start, float startDisplacement) { PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler(); if (mCurrentAnimation == null) { reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl)); mDisplacementShift = 0; } else { mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier; mCurrentAnimation.pause(); } mFlingBlockCheck.unblockFling(); mOverrideVelocity = null; } @Override public boolean onDrag(float displacement) { PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler(); float totalDisplacement = displacement + mDisplacementShift; boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : orientationHandler.isGoingUp(totalDisplacement, mIsRtl); if (isGoingUp != mCurrentAnimationIsGoingUp) { reInitAnimationController(isGoingUp); mFlingBlockCheck.blockFling(); } else { mFlingBlockCheck.onEvent(); } if (isGoingUp) { if (mCurrentAnimation.getProgressFraction() < ANIMATION_PROGRESS_FRACTION_MIDPOINT) { // Halve the value when dismissing, as we are animating the drag across the full // length for only the first half of the progress mCurrentAnimation.setPlayFraction( Utilities.boundToRange(totalDisplacement * mProgressMultiplier / 2, 0, 1)); } else { // Set mOverrideVelocity to control task dismiss velocity in onDragEnd int velocityDimenId = R.dimen.default_task_dismiss_drag_velocity; if (mRecentsView.showAsGrid()) { if (mTaskBeingDragged.isFocusedTask()) { velocityDimenId = R.dimen.default_task_dismiss_drag_velocity_grid_focus_task; } else { velocityDimenId = R.dimen.default_task_dismiss_drag_velocity_grid; } } mOverrideVelocity = -mTaskBeingDragged.getResources().getDimension(velocityDimenId); // Once halfway through task dismissal interpolation, switch from reversible // dragging-task animation to playing the remaining task translation animations final long now = SystemClock.uptimeMillis(); MotionEvent upAction = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 0.0f, 0.0f, 0); mDetector.onTouchEvent(upAction); upAction.recycle(); } } else { mCurrentAnimation.setPlayFraction( Utilities.boundToRange(totalDisplacement * mProgressMultiplier, 0, 1)); } return true; } @Override public void onDragEnd(float velocity) { if (mOverrideVelocity != null) { velocity = mOverrideVelocity; mOverrideVelocity = null; } // Limit velocity, as very large scalar values make animations play too quickly float maxTaskDismissDragVelocity = mTaskBeingDragged.getResources().getDimension( R.dimen.max_task_dismiss_drag_velocity); velocity = Utilities.boundToRange(velocity, -maxTaskDismissDragVelocity, maxTaskDismissDragVelocity); boolean fling = mDetector.isFling(velocity); final boolean goingToEnd; boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); if (blockedFling) { fling = false; } PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler(); boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl); float progress = mCurrentAnimation.getProgressFraction(); float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress(); if (fling) { goingToEnd = goingUp == mCurrentAnimationIsGoingUp; } else { goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS; } long animationDuration = BaseSwipeDetector.calculateDuration( velocity, goingToEnd ? (1 - progress) : progress); if (blockedFling && !goingToEnd) { animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity); } // Due to very high or low velocity dismissals, animation durations can be inconsistently // long or short. Bound the duration for animation of task translations for a more // standardized feel. animationDuration = Utilities.boundToRange(animationDuration, MIN_TASK_DISMISS_ANIMATION_DURATION, MAX_TASK_DISMISS_ANIMATION_DURATION); mCurrentAnimation.setEndAction(this::clearState); mCurrentAnimation.startWithVelocity(mActivity, goingToEnd, velocity * orientationHandler.getSecondaryTranslationDirectionFactor(), mEndDisplacement, animationDuration); if (goingUp && goingToEnd && !mIsDismissHapticRunning) { VibratorWrapper.INSTANCE.get(mActivity).vibrate(TASK_DISMISS_VIBRATION_PRIMITIVE, TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE, TASK_DISMISS_VIBRATION_FALLBACK); mIsDismissHapticRunning = true; } } private void clearState() { mDetector.finishedScrolling(); mDetector.setDetectableScrollConditions(0, false); mTaskBeingDragged = null; mCurrentAnimation = null; mIsDismissHapticRunning = false; } }