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; 17 18 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; 19 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.ValueAnimator; 24 import android.view.MotionEvent; 25 26 import com.android.launcher3.AbstractFloatingView; 27 import com.android.launcher3.BaseDraggingActivity; 28 import com.android.launcher3.LauncherAnimUtils; 29 import com.android.launcher3.Utilities; 30 import com.android.launcher3.anim.AnimatorPlaybackController; 31 import com.android.launcher3.anim.Interpolators; 32 import com.android.launcher3.touch.SwipeDetector; 33 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 34 import com.android.launcher3.util.FlingBlockCheck; 35 import com.android.launcher3.util.PendingAnimation; 36 import com.android.launcher3.util.TouchController; 37 import com.android.launcher3.views.BaseDragLayer; 38 import com.android.quickstep.OverviewInteractionState; 39 import com.android.quickstep.views.RecentsView; 40 import com.android.quickstep.views.TaskView; 41 42 /** 43 * Touch controller for handling task view card swipes 44 */ 45 public abstract class TaskViewTouchController<T extends BaseDraggingActivity> 46 extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener { 47 48 private static final String TAG = "OverviewSwipeController"; 49 50 // Progress after which the transition is assumed to be a success in case user does not fling 51 private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; 52 53 protected final T mActivity; 54 private final SwipeDetector mDetector; 55 private final RecentsView mRecentsView; 56 private final int[] mTempCords = new int[2]; 57 58 private PendingAnimation mPendingAnimation; 59 private AnimatorPlaybackController mCurrentAnimation; 60 private boolean mCurrentAnimationIsGoingUp; 61 62 private boolean mNoIntercept; 63 64 private float mDisplacementShift; 65 private float mProgressMultiplier; 66 private float mEndDisplacement; 67 private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); 68 69 private TaskView mTaskBeingDragged; 70 TaskViewTouchController(T activity)71 public TaskViewTouchController(T activity) { 72 mActivity = activity; 73 mRecentsView = activity.getOverviewPanel(); 74 mDetector = new SwipeDetector(activity, this, SwipeDetector.VERTICAL); 75 } 76 canInterceptTouch()77 private boolean canInterceptTouch() { 78 if (mCurrentAnimation != null) { 79 // If we are already animating from a previous state, we can intercept. 80 return true; 81 } 82 if (AbstractFloatingView.getTopOpenView(mActivity) != null) { 83 return false; 84 } 85 return isRecentsInteractive(); 86 } 87 isRecentsInteractive()88 protected abstract boolean isRecentsInteractive(); 89 onUserControlledAnimationCreated(AnimatorPlaybackController animController)90 protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { 91 } 92 93 @Override onAnimationCancel(Animator animation)94 public void onAnimationCancel(Animator animation) { 95 if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) { 96 clearState(); 97 } 98 } 99 100 @Override onControllerInterceptTouchEvent(MotionEvent ev)101 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 102 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 103 mNoIntercept = !canInterceptTouch(); 104 if (mNoIntercept) { 105 return false; 106 } 107 108 // Now figure out which direction scroll events the controller will start 109 // calling the callbacks. 110 int directionsToDetectScroll = 0; 111 boolean ignoreSlopWhenSettling = false; 112 if (mCurrentAnimation != null) { 113 directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; 114 ignoreSlopWhenSettling = true; 115 } else { 116 mTaskBeingDragged = null; 117 118 for (int i = 0; i < mRecentsView.getChildCount(); i++) { 119 TaskView view = mRecentsView.getPageAt(i); 120 if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer() 121 .isEventOverView(view, ev)) { 122 mTaskBeingDragged = view; 123 if (!OverviewInteractionState.getInstance(mActivity) 124 .isSwipeUpGestureEnabled()) { 125 // Don't allow swipe down to open if we don't support swipe up 126 // to enter overview. 127 directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE; 128 } else { 129 // The task can be dragged up to dismiss it, 130 // and down to open if it's the current page. 131 directionsToDetectScroll = i == mRecentsView.getCurrentPage() 132 ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE; 133 } 134 break; 135 } 136 } 137 if (mTaskBeingDragged == null) { 138 mNoIntercept = true; 139 return false; 140 } 141 } 142 143 mDetector.setDetectableScrollConditions( 144 directionsToDetectScroll, ignoreSlopWhenSettling); 145 } 146 147 if (mNoIntercept) { 148 return false; 149 } 150 151 onControllerTouchEvent(ev); 152 return mDetector.isDraggingOrSettling(); 153 } 154 155 @Override onControllerTouchEvent(MotionEvent ev)156 public boolean onControllerTouchEvent(MotionEvent ev) { 157 return mDetector.onTouchEvent(ev); 158 } 159 reInitAnimationController(boolean goingUp)160 private void reInitAnimationController(boolean goingUp) { 161 if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) { 162 // No need to init 163 return; 164 } 165 int scrollDirections = mDetector.getScrollDirections(); 166 if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0) 167 || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) { 168 // Trying to re-init in an unsupported direction. 169 return; 170 } 171 if (mCurrentAnimation != null) { 172 mCurrentAnimation.setPlayFraction(0); 173 } 174 if (mPendingAnimation != null) { 175 mPendingAnimation.finish(false, Touch.SWIPE); 176 mPendingAnimation = null; 177 } 178 179 mCurrentAnimationIsGoingUp = goingUp; 180 BaseDragLayer dl = mActivity.getDragLayer(); 181 long maxDuration = (long) (2 * dl.getHeight()); 182 183 if (goingUp) { 184 mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged, 185 true /* animateTaskView */, true /* removeTask */, maxDuration); 186 187 mEndDisplacement = -mTaskBeingDragged.getHeight(); 188 } else { 189 mPendingAnimation = mRecentsView.createTaskLauncherAnimation( 190 mTaskBeingDragged, maxDuration); 191 mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN); 192 193 mTempCords[1] = mTaskBeingDragged.getHeight(); 194 dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords); 195 mEndDisplacement = dl.getHeight() - mTempCords[1]; 196 } 197 198 if (mCurrentAnimation != null) { 199 mCurrentAnimation.setOnCancelRunnable(null); 200 } 201 mCurrentAnimation = AnimatorPlaybackController 202 .wrap(mPendingAnimation.anim, maxDuration, this::clearState); 203 onUserControlledAnimationCreated(mCurrentAnimation); 204 mCurrentAnimation.getTarget().addListener(this); 205 mCurrentAnimation.dispatchOnStart(); 206 mProgressMultiplier = 1 / mEndDisplacement; 207 } 208 209 @Override onDragStart(boolean start)210 public void onDragStart(boolean start) { 211 if (mCurrentAnimation == null) { 212 reInitAnimationController(mDetector.wasInitialTouchPositive()); 213 mDisplacementShift = 0; 214 } else { 215 mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier; 216 mCurrentAnimation.pause(); 217 } 218 mFlingBlockCheck.unblockFling(); 219 } 220 221 @Override onDrag(float displacement, float velocity)222 public boolean onDrag(float displacement, float velocity) { 223 float totalDisplacement = displacement + mDisplacementShift; 224 boolean isGoingUp = 225 totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0; 226 if (isGoingUp != mCurrentAnimationIsGoingUp) { 227 reInitAnimationController(isGoingUp); 228 mFlingBlockCheck.blockFling(); 229 } else { 230 mFlingBlockCheck.onEvent(); 231 } 232 mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier); 233 return true; 234 } 235 236 @Override 237 public void onDragEnd(float velocity, boolean fling) { 238 final boolean goingToEnd; 239 final int logAction; 240 boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); 241 if (blockedFling) { 242 fling = false; 243 } 244 if (fling) { 245 logAction = Touch.FLING; 246 boolean goingUp = velocity < 0; 247 goingToEnd = goingUp == mCurrentAnimationIsGoingUp; 248 } else { 249 logAction = Touch.SWIPE; 250 goingToEnd = mCurrentAnimation.getProgressFraction() > SUCCESS_TRANSITION_PROGRESS; 251 } 252 253 float progress = mCurrentAnimation.getProgressFraction(); 254 long animationDuration = SwipeDetector.calculateDuration( 255 velocity, goingToEnd ? (1 - progress) : progress); 256 if (blockedFling && !goingToEnd) { 257 animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity); 258 } 259 260 float nextFrameProgress = Utilities.boundToRange( 261 progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f); 262 263 mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction)); 264 265 ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); 266 anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f); 267 anim.setDuration(animationDuration); 268 anim.setInterpolator(scrollInterpolatorForVelocity(velocity)); 269 anim.start(); 270 } 271 onCurrentAnimationEnd(boolean wasSuccess, int logAction)272 private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) { 273 if (mPendingAnimation != null) { 274 mPendingAnimation.finish(wasSuccess, logAction); 275 mPendingAnimation = null; 276 } 277 clearState(); 278 } 279 clearState()280 private void clearState() { 281 mDetector.finishedScrolling(); 282 mDetector.setDetectableScrollConditions(0, false); 283 mTaskBeingDragged = null; 284 mCurrentAnimation = null; 285 if (mPendingAnimation != null) { 286 mPendingAnimation.finish(false, Touch.SWIPE); 287 mPendingAnimation = null; 288 } 289 } 290 } 291