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