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