1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.uioverrides.touchcontrollers; 17 18 import static com.android.launcher3.AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT; 19 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; 20 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.content.Context; 25 import android.os.VibrationEffect; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.animation.Interpolator; 29 30 import com.android.app.animation.Interpolators; 31 import com.android.launcher3.AbstractFloatingView; 32 import com.android.launcher3.LauncherAnimUtils; 33 import com.android.launcher3.R; 34 import com.android.launcher3.Utilities; 35 import com.android.launcher3.anim.AnimatorPlaybackController; 36 import com.android.launcher3.anim.PendingAnimation; 37 import com.android.launcher3.touch.BaseSwipeDetector; 38 import com.android.launcher3.touch.SingleAxisSwipeDetector; 39 import com.android.launcher3.util.DisplayController; 40 import com.android.launcher3.util.FlingBlockCheck; 41 import com.android.launcher3.util.TouchController; 42 import com.android.launcher3.util.VibratorWrapper; 43 import com.android.launcher3.views.BaseDragLayer; 44 import com.android.quickstep.orientation.RecentsPagedOrientationHandler; 45 import com.android.quickstep.util.VibrationConstants; 46 import com.android.quickstep.views.RecentsView; 47 import com.android.quickstep.views.RecentsViewContainer; 48 import com.android.quickstep.views.TaskView; 49 50 /** 51 * Touch controller for handling task view card swipes 52 */ 53 public abstract class TaskViewTouchController<CONTAINER extends Context & RecentsViewContainer> 54 extends AnimatorListenerAdapter implements TouchController, 55 SingleAxisSwipeDetector.Listener { 56 57 private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f; 58 private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300; 59 private static final long MAX_TASK_DISMISS_ANIMATION_DURATION = 600; 60 61 public static final int TASK_DISMISS_VIBRATION_PRIMITIVE = 62 VibrationEffect.Composition.PRIMITIVE_TICK; 63 public static final float TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE = 1f; 64 public static final VibrationEffect TASK_DISMISS_VIBRATION_FALLBACK = 65 VibrationConstants.EFFECT_TEXTURE_TICK; 66 67 protected final CONTAINER mContainer; 68 private final SingleAxisSwipeDetector mDetector; 69 private final RecentsView mRecentsView; 70 private final int[] mTempCords = new int[2]; 71 private final boolean mIsRtl; 72 73 private AnimatorPlaybackController mCurrentAnimation; 74 private boolean mCurrentAnimationIsGoingUp; 75 private boolean mAllowGoingUp; 76 private boolean mAllowGoingDown; 77 78 private boolean mNoIntercept; 79 80 private float mDisplacementShift; 81 private float mProgressMultiplier; 82 private float mEndDisplacement; 83 private boolean mDraggingEnabled = true; 84 private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); 85 private Float mOverrideVelocity = null; 86 87 private TaskView mTaskBeingDragged; 88 89 private boolean mIsDismissHapticRunning = false; 90 TaskViewTouchController(CONTAINER container)91 public TaskViewTouchController(CONTAINER container) { 92 mContainer = container; 93 mRecentsView = container.getOverviewPanel(); 94 mIsRtl = Utilities.isRtl(container.getResources()); 95 SingleAxisSwipeDetector.Direction dir = 96 mRecentsView.getPagedOrientationHandler().getUpDownSwipeDirection(); 97 mDetector = new SingleAxisSwipeDetector(container, this, dir); 98 } 99 canInterceptTouch(MotionEvent ev)100 private boolean canInterceptTouch(MotionEvent ev) { 101 if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0) { 102 // Don't intercept swipes on the nav bar, as user might be trying to go home 103 // during a task dismiss animation. 104 if (mCurrentAnimation != null) { 105 mCurrentAnimation.getAnimationPlayer().end(); 106 } 107 return false; 108 } 109 if (mCurrentAnimation != null) { 110 mCurrentAnimation.forceFinishIfCloseToEnd(); 111 } 112 if (mCurrentAnimation != null) { 113 // If we are already animating from a previous state, we can intercept. 114 return true; 115 } 116 if (AbstractFloatingView.getTopOpenViewWithType( 117 mContainer, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null) { 118 return false; 119 } 120 return isRecentsInteractive(); 121 } 122 isRecentsInteractive()123 protected abstract boolean isRecentsInteractive(); 124 125 /** Is recents view showing a single task in a modal way. */ isRecentsModal()126 protected abstract boolean isRecentsModal(); 127 onUserControlledAnimationCreated(AnimatorPlaybackController animController)128 protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { 129 } 130 131 @Override onAnimationCancel(Animator animation)132 public void onAnimationCancel(Animator animation) { 133 if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) { 134 clearState(); 135 } 136 } 137 138 @Override onControllerInterceptTouchEvent(MotionEvent ev)139 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 140 if ((ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) 141 && mCurrentAnimation == null) { 142 clearState(); 143 } 144 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 145 mNoIntercept = !canInterceptTouch(ev); 146 if (mNoIntercept) { 147 return false; 148 } 149 150 // Now figure out which direction scroll events the controller will start 151 // calling the callbacks. 152 int directionsToDetectScroll = 0; 153 boolean ignoreSlopWhenSettling = false; 154 if (mCurrentAnimation != null) { 155 directionsToDetectScroll = DIRECTION_BOTH; 156 ignoreSlopWhenSettling = true; 157 } else { 158 mTaskBeingDragged = null; 159 160 for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) { 161 TaskView view = mRecentsView.getTaskViewAt(i); 162 163 if (mRecentsView.isTaskViewVisible(view) && mContainer.getDragLayer() 164 .isEventOverView(view, ev)) { 165 // Disable swiping up and down if the task overlay is modal. 166 if (isRecentsModal()) { 167 mTaskBeingDragged = null; 168 break; 169 } 170 mTaskBeingDragged = view; 171 int upDirection = mRecentsView.getPagedOrientationHandler() 172 .getUpDirection(mIsRtl); 173 174 // The task can be dragged up to dismiss it 175 mAllowGoingUp = true; 176 177 // The task can be dragged down to open it if: 178 // - It's the current page 179 // - We support gestures to enter overview 180 // - It's the focused task if in grid view 181 // - The task is snapped 182 mAllowGoingDown = i == mRecentsView.getCurrentPage() 183 && DisplayController.getNavigationMode(mContainer).hasGestures 184 && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask()) 185 && mRecentsView.isTaskInExpectedScrollPosition(i); 186 187 directionsToDetectScroll = mAllowGoingDown ? DIRECTION_BOTH : upDirection; 188 break; 189 } 190 } 191 if (mTaskBeingDragged == null) { 192 mNoIntercept = true; 193 return false; 194 } 195 } 196 197 mDetector.setDetectableScrollConditions( 198 directionsToDetectScroll, ignoreSlopWhenSettling); 199 } 200 201 if (mNoIntercept) { 202 return false; 203 } 204 205 onControllerTouchEvent(ev); 206 return mDetector.isDraggingOrSettling(); 207 } 208 209 @Override onControllerTouchEvent(MotionEvent ev)210 public boolean onControllerTouchEvent(MotionEvent ev) { 211 return mDetector.onTouchEvent(ev); 212 } 213 reInitAnimationController(boolean goingUp)214 private void reInitAnimationController(boolean goingUp) { 215 if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) { 216 // No need to init 217 return; 218 } 219 if ((goingUp && !mAllowGoingUp) || (!goingUp && !mAllowGoingDown)) { 220 // Trying to re-init in an unsupported direction. 221 return; 222 } 223 if (mCurrentAnimation != null) { 224 mCurrentAnimation.setPlayFraction(0); 225 mCurrentAnimation.getTarget().removeListener(this); 226 mCurrentAnimation.dispatchOnCancel(); 227 } 228 229 RecentsPagedOrientationHandler orientationHandler = 230 mRecentsView.getPagedOrientationHandler(); 231 mCurrentAnimationIsGoingUp = goingUp; 232 BaseDragLayer dl = mContainer.getDragLayer(); 233 final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl); 234 long maxDuration = 2 * secondaryLayerDimension; 235 int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl); 236 int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged); 237 // The interpolator controlling the most prominent visual movement. We use this to determine 238 // whether we passed SUCCESS_TRANSITION_PROGRESS. 239 final Interpolator currentInterpolator; 240 PendingAnimation pa; 241 if (goingUp) { 242 currentInterpolator = Interpolators.LINEAR; 243 pa = new PendingAnimation(maxDuration); 244 mRecentsView.createTaskDismissAnimation(pa, mTaskBeingDragged, 245 true /* animateTaskView */, true /* removeTask */, maxDuration, 246 false /* dismissingForSplitSelection*/); 247 248 mEndDisplacement = -secondaryTaskDimension; 249 } else { 250 currentInterpolator = Interpolators.ZOOM_IN; 251 pa = mRecentsView.createTaskLaunchAnimation( 252 mTaskBeingDragged, maxDuration, currentInterpolator); 253 254 // Since the thumbnail is what is filling the screen, based the end displacement on it. 255 View thumbnailView = mTaskBeingDragged.getFirstThumbnailViewDeprecated(); 256 mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView); 257 dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords); 258 mEndDisplacement = secondaryLayerDimension - mTempCords[1]; 259 } 260 mEndDisplacement *= verticalFactor; 261 mCurrentAnimation = pa.createPlaybackController(); 262 263 // Setting this interpolator doesn't affect the visual motion, but is used to determine 264 // whether we successfully reached the target state in onDragEnd(). 265 mCurrentAnimation.getTarget().setInterpolator(currentInterpolator); 266 onUserControlledAnimationCreated(mCurrentAnimation); 267 mCurrentAnimation.getTarget().addListener(this); 268 mCurrentAnimation.dispatchOnStart(); 269 mProgressMultiplier = 1 / mEndDisplacement; 270 } 271 272 @Override onDragStart(boolean start, float startDisplacement)273 public void onDragStart(boolean start, float startDisplacement) { 274 if (!mDraggingEnabled) return; 275 276 RecentsPagedOrientationHandler orientationHandler = 277 mRecentsView.getPagedOrientationHandler(); 278 if (mCurrentAnimation == null) { 279 reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl)); 280 mDisplacementShift = 0; 281 } else { 282 mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier; 283 mCurrentAnimation.pause(); 284 } 285 mFlingBlockCheck.unblockFling(); 286 mOverrideVelocity = null; 287 } 288 289 @Override onDrag(float displacement)290 public boolean onDrag(float displacement) { 291 if (!mDraggingEnabled) return true; 292 293 RecentsPagedOrientationHandler orientationHandler = 294 mRecentsView.getPagedOrientationHandler(); 295 float totalDisplacement = displacement + mDisplacementShift; 296 boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : 297 orientationHandler.isGoingUp(totalDisplacement, mIsRtl); 298 if (isGoingUp != mCurrentAnimationIsGoingUp) { 299 reInitAnimationController(isGoingUp); 300 mFlingBlockCheck.blockFling(); 301 } else { 302 mFlingBlockCheck.onEvent(); 303 } 304 305 if (isGoingUp) { 306 if (mCurrentAnimation.getProgressFraction() < ANIMATION_PROGRESS_FRACTION_MIDPOINT) { 307 // Halve the value when dismissing, as we are animating the drag across the full 308 // length for only the first half of the progress 309 mCurrentAnimation.setPlayFraction( 310 Utilities.boundToRange(totalDisplacement * mProgressMultiplier / 2, 0, 1)); 311 } else { 312 // Set mOverrideVelocity to control task dismiss velocity in onDragEnd 313 int velocityDimenId = R.dimen.default_task_dismiss_drag_velocity; 314 if (mRecentsView.showAsGrid()) { 315 if (mTaskBeingDragged.isFocusedTask()) { 316 velocityDimenId = 317 R.dimen.default_task_dismiss_drag_velocity_grid_focus_task; 318 } else { 319 velocityDimenId = R.dimen.default_task_dismiss_drag_velocity_grid; 320 } 321 } 322 mOverrideVelocity = -mTaskBeingDragged.getResources().getDimension(velocityDimenId); 323 324 // Once halfway through task dismissal interpolation, switch from reversible 325 // dragging-task animation to playing the remaining task translation animations, 326 // while this is in progress disable dragging. 327 mDraggingEnabled = false; 328 } 329 } else { 330 mCurrentAnimation.setPlayFraction( 331 Utilities.boundToRange(totalDisplacement * mProgressMultiplier, 0, 1)); 332 } 333 334 return true; 335 } 336 337 @Override onDragEnd(float velocity)338 public void onDragEnd(float velocity) { 339 if (mOverrideVelocity != null) { 340 velocity = mOverrideVelocity; 341 mOverrideVelocity = null; 342 } 343 // Limit velocity, as very large scalar values make animations play too quickly 344 float maxTaskDismissDragVelocity = mTaskBeingDragged.getResources().getDimension( 345 R.dimen.max_task_dismiss_drag_velocity); 346 velocity = Utilities.boundToRange(velocity, -maxTaskDismissDragVelocity, 347 maxTaskDismissDragVelocity); 348 boolean fling = mDraggingEnabled && mDetector.isFling(velocity); 349 final boolean goingToEnd; 350 boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); 351 if (blockedFling) { 352 fling = false; 353 } 354 RecentsPagedOrientationHandler orientationHandler = 355 mRecentsView.getPagedOrientationHandler(); 356 boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl); 357 float progress = mCurrentAnimation.getProgressFraction(); 358 float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress(); 359 if (fling) { 360 goingToEnd = goingUp == mCurrentAnimationIsGoingUp; 361 } else { 362 goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS; 363 } 364 long animationDuration = BaseSwipeDetector.calculateDuration( 365 velocity, goingToEnd ? (1 - progress) : progress); 366 if (blockedFling && !goingToEnd) { 367 animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity); 368 } 369 // Due to very high or low velocity dismissals, animation durations can be inconsistently 370 // long or short. Bound the duration for animation of task translations for a more 371 // standardized feel. 372 animationDuration = Utilities.boundToRange(animationDuration, 373 MIN_TASK_DISMISS_ANIMATION_DURATION, MAX_TASK_DISMISS_ANIMATION_DURATION); 374 375 mCurrentAnimation.setEndAction(this::clearState); 376 mCurrentAnimation.startWithVelocity(mContainer, goingToEnd, Math.abs(velocity), 377 mEndDisplacement, animationDuration); 378 if (goingUp && goingToEnd && !mIsDismissHapticRunning) { 379 VibratorWrapper.INSTANCE.get(mContainer).vibrate(TASK_DISMISS_VIBRATION_PRIMITIVE, 380 TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE, TASK_DISMISS_VIBRATION_FALLBACK); 381 mIsDismissHapticRunning = true; 382 } 383 384 mDraggingEnabled = true; 385 } 386 clearState()387 private void clearState() { 388 mDetector.finishedScrolling(); 389 mDetector.setDetectableScrollConditions(0, false); 390 mDraggingEnabled = true; 391 mTaskBeingDragged = null; 392 mCurrentAnimation = null; 393 mIsDismissHapticRunning = false; 394 } 395 } 396