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.touch; 17 18 import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity; 19 import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; 20 import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS; 21 import static com.android.launcher3.LauncherAnimUtils.newCancelListener; 22 import static com.android.launcher3.LauncherState.ALL_APPS; 23 import static com.android.launcher3.LauncherState.NORMAL; 24 import static com.android.launcher3.LauncherState.OVERVIEW; 25 import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll; 26 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; 27 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS; 28 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; 29 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW; 30 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN; 31 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP; 32 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; 33 34 import android.animation.Animator.AnimatorListener; 35 import android.animation.ValueAnimator; 36 import android.view.MotionEvent; 37 38 import com.android.launcher3.Launcher; 39 import com.android.launcher3.LauncherAnimUtils; 40 import com.android.launcher3.LauncherState; 41 import com.android.launcher3.Utilities; 42 import com.android.launcher3.anim.AnimatorPlaybackController; 43 import com.android.launcher3.logger.LauncherAtom; 44 import com.android.launcher3.logging.StatsLogManager; 45 import com.android.launcher3.states.StateAnimationConfig; 46 import com.android.launcher3.util.FlingBlockCheck; 47 import com.android.launcher3.util.TouchController; 48 49 /** 50 * TouchController for handling state changes 51 */ 52 public abstract class AbstractStateChangeTouchController 53 implements TouchController, SingleAxisSwipeDetector.Listener { 54 55 protected final Launcher mLauncher; 56 protected final SingleAxisSwipeDetector mDetector; 57 protected final SingleAxisSwipeDetector.Direction mSwipeDirection; 58 59 protected final AnimatorListener mClearStateOnCancelListener = 60 newCancelListener(this::clearState, /* isSingleUse = */ false); 61 private final FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); 62 63 protected int mStartContainerType; 64 65 protected LauncherState mStartState; 66 protected LauncherState mFromState; 67 protected LauncherState mToState; 68 protected AnimatorPlaybackController mCurrentAnimation; 69 protected boolean mGoingBetweenStates = true; 70 // Ratio of transition process [0, 1] to drag displacement (px) 71 protected float mProgressMultiplier; 72 protected boolean mIsTrackpadReverseScroll; 73 74 private boolean mNoIntercept; 75 private boolean mIsLogContainerSet; 76 private float mStartProgress; 77 private float mDisplacementShift; 78 private boolean mCanBlockFling; 79 private boolean mAllAppsOvershootStarted; 80 AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir)81 public AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) { 82 mLauncher = l; 83 mDetector = new SingleAxisSwipeDetector(l, this, dir); 84 mSwipeDirection = dir; 85 } 86 canInterceptTouch(MotionEvent ev)87 protected abstract boolean canInterceptTouch(MotionEvent ev); 88 89 @Override onControllerInterceptTouchEvent(MotionEvent ev)90 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 91 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 92 mNoIntercept = !canInterceptTouch(ev); 93 if (mNoIntercept) { 94 return false; 95 } 96 97 mIsTrackpadReverseScroll = !mLauncher.isNaturalScrollingEnabled() 98 && isTrackpadScroll(ev); 99 100 // Now figure out which direction scroll events the controller will start 101 // calling the callbacks. 102 final int directionsToDetectScroll; 103 boolean ignoreSlopWhenSettling = false; 104 105 if (mCurrentAnimation != null) { 106 directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH; 107 ignoreSlopWhenSettling = true; 108 } else { 109 directionsToDetectScroll = getSwipeDirection(); 110 if (directionsToDetectScroll == 0) { 111 mNoIntercept = true; 112 return false; 113 } 114 } 115 mDetector.setDetectableScrollConditions( 116 directionsToDetectScroll, ignoreSlopWhenSettling); 117 } 118 119 if (mNoIntercept) { 120 return false; 121 } 122 123 onControllerTouchEvent(ev); 124 return mDetector.isDraggingOrSettling(); 125 } 126 getSwipeDirection()127 private int getSwipeDirection() { 128 LauncherState fromState = mLauncher.getStateManager().getState(); 129 int swipeDirection = 0; 130 if (getTargetState(fromState, true /* isDragTowardPositive */) != fromState) { 131 swipeDirection |= SingleAxisSwipeDetector.DIRECTION_POSITIVE; 132 } 133 if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) { 134 swipeDirection |= SingleAxisSwipeDetector.DIRECTION_NEGATIVE; 135 } 136 return swipeDirection; 137 } 138 139 @Override onControllerTouchEvent(MotionEvent ev)140 public final boolean onControllerTouchEvent(MotionEvent ev) { 141 return mDetector.onTouchEvent(ev); 142 } 143 getShiftRange()144 protected float getShiftRange() { 145 return mLauncher.getAllAppsController().getShiftRange(); 146 } 147 148 /** 149 * Returns the state to go to from fromState given the drag direction. If there is no state in 150 * that direction, returns fromState. 151 */ getTargetState(LauncherState fromState, boolean isDragTowardPositive)152 protected abstract LauncherState getTargetState(LauncherState fromState, 153 boolean isDragTowardPositive); 154 initCurrentAnimation()155 protected abstract float initCurrentAnimation(); 156 reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive)157 private boolean reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive) { 158 LauncherState newFromState = mFromState == null ? mLauncher.getStateManager().getState() 159 : reachedToState ? mToState : mFromState; 160 LauncherState newToState = getTargetState(newFromState, isDragTowardPositive); 161 162 onReinitToState(newToState); 163 164 if (newFromState == mFromState && newToState == mToState || (newFromState == newToState)) { 165 return false; 166 } 167 168 mFromState = newFromState; 169 mToState = newToState; 170 171 mStartProgress = 0; 172 if (mCurrentAnimation != null) { 173 mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener); 174 } 175 mProgressMultiplier = initCurrentAnimation(); 176 mCurrentAnimation.dispatchOnStart(); 177 return true; 178 } 179 onReinitToState(LauncherState newToState)180 protected void onReinitToState(LauncherState newToState) { 181 } 182 onReachedFinalState(LauncherState newToState)183 protected void onReachedFinalState(LauncherState newToState) { 184 } 185 186 @Override onDragStart(boolean start, float startDisplacement)187 public void onDragStart(boolean start, float startDisplacement) { 188 mStartState = mLauncher.getStateManager().getState(); 189 mIsLogContainerSet = false; 190 191 if (mCurrentAnimation == null) { 192 mFromState = mStartState; 193 mToState = null; 194 cancelAnimationControllers(); 195 reinitCurrentAnimation(false, mDetector.wasInitialTouchPositive()); 196 mDisplacementShift = 0; 197 } else { 198 mCurrentAnimation.pause(); 199 mStartProgress = mCurrentAnimation.getProgressFraction(); 200 } 201 mCanBlockFling = mFromState == NORMAL; 202 mFlingBlockCheck.unblockFling(); 203 } 204 205 @Override onDrag(float displacement)206 public boolean onDrag(float displacement) { 207 float deltaProgress = mProgressMultiplier * (displacement - mDisplacementShift); 208 float progress = deltaProgress + mStartProgress; 209 updateProgress(progress); 210 boolean isDragTowardPositive = mSwipeDirection.isPositive( 211 displacement - mDisplacementShift); 212 if (progress <= 0) { 213 if (reinitCurrentAnimation(false, isDragTowardPositive)) { 214 mDisplacementShift = displacement; 215 if (mCanBlockFling) { 216 mFlingBlockCheck.blockFling(); 217 } 218 } 219 if (mFromState == LauncherState.ALL_APPS) { 220 mAllAppsOvershootStarted = true; 221 mLauncher.getAppsView().onPull(-progress , -progress); 222 } 223 } else if (progress >= 1) { 224 if (reinitCurrentAnimation(true, isDragTowardPositive)) { 225 mDisplacementShift = displacement; 226 if (mCanBlockFling) { 227 mFlingBlockCheck.blockFling(); 228 } 229 } 230 if (mToState == LauncherState.ALL_APPS) { 231 mAllAppsOvershootStarted = true; 232 // 1f, value when all apps container hit the top 233 mLauncher.getAppsView().onPull(progress - 1f, progress - 1f); 234 } 235 236 } else { 237 mFlingBlockCheck.onEvent(); 238 239 } 240 241 return true; 242 } 243 244 @Override onDrag(float displacement, MotionEvent ev)245 public boolean onDrag(float displacement, MotionEvent ev) { 246 if (!mIsLogContainerSet) { 247 if (mStartState == ALL_APPS) { 248 mStartContainerType = LAUNCHER_STATE_ALLAPPS; 249 } else if (mStartState == NORMAL) { 250 mStartContainerType = LAUNCHER_STATE_HOME; 251 } else if (mStartState == OVERVIEW) { 252 mStartContainerType = LAUNCHER_STATE_OVERVIEW; 253 } 254 mIsLogContainerSet = true; 255 } 256 // Only reverse the gesture to open all apps (not close) when trackpad reverse scrolling is 257 // on. 258 if (mIsTrackpadReverseScroll && mStartState == NORMAL) { 259 displacement = -displacement; 260 } 261 return onDrag(displacement); 262 } 263 updateProgress(float fraction)264 protected void updateProgress(float fraction) { 265 if (mCurrentAnimation == null) { 266 return; 267 } 268 mCurrentAnimation.setPlayFraction(fraction); 269 } 270 271 /** 272 * Returns animation config for state transition between provided states 273 */ getConfigForStates( LauncherState fromState, LauncherState toState)274 protected StateAnimationConfig getConfigForStates( 275 LauncherState fromState, LauncherState toState) { 276 return new StateAnimationConfig(); 277 } 278 279 @Override onDragEnd(float velocity)280 public void onDragEnd(float velocity) { 281 if (mCurrentAnimation == null) { 282 // Unlikely, but we may have been canceled just before onDragEnd(). We assume whoever 283 // canceled us will handle a new state transition to clean up. 284 return; 285 } 286 287 // Only reverse the gesture to open all apps (not close) when trackpad reverse scrolling is 288 // on. 289 if (mIsTrackpadReverseScroll && mStartState == NORMAL) { 290 velocity = -velocity; 291 } 292 boolean fling = mDetector.isFling(velocity); 293 294 boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); 295 if (blockedFling) { 296 fling = false; 297 } 298 299 final LauncherState targetState; 300 final float progress = mCurrentAnimation.getProgressFraction(); 301 final float progressVelocity = velocity * mProgressMultiplier; 302 final float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress(); 303 if (fling) { 304 targetState = 305 Float.compare(Math.signum(velocity), Math.signum(mProgressMultiplier)) == 0 306 ? mToState : mFromState; 307 // snap to top or bottom using the release velocity 308 } else { 309 float successTransitionProgress = SUCCESS_TRANSITION_PROGRESS; 310 if (mLauncher.getDeviceProfile().isTablet 311 && (mToState == ALL_APPS || mFromState == ALL_APPS)) { 312 successTransitionProgress = TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS; 313 } else if (!mLauncher.getDeviceProfile().isTablet 314 && mToState == ALL_APPS && mFromState == NORMAL) { 315 successTransitionProgress = AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL; 316 } else if (!mLauncher.getDeviceProfile().isTablet 317 && mToState == NORMAL && mFromState == ALL_APPS) { 318 successTransitionProgress = 319 1 - AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL; 320 } 321 targetState = 322 (interpolatedProgress > successTransitionProgress) ? mToState : mFromState; 323 } 324 325 final float endProgress; 326 final float startProgress; 327 final long duration; 328 // Increase the duration if we prevented the fling, as we are going against a high velocity. 329 final int durationMultiplier = blockedFling && targetState == mFromState 330 ? LauncherAnimUtils.blockedFlingDurationFactor(velocity) : 1; 331 332 if (targetState == mToState) { 333 endProgress = 1; 334 if (progress >= 1) { 335 duration = 0; 336 startProgress = 1; 337 } else { 338 startProgress = Utilities.boundToRange(progress 339 + progressVelocity * getSingleFrameMs(mLauncher), 0f, 1f); 340 duration = BaseSwipeDetector.calculateDuration(velocity, 341 endProgress - Math.max(progress, 0)) * durationMultiplier; 342 } 343 } else { 344 // Let the state manager know that the animation didn't go to the target state, 345 // but don't cancel ourselves (we already clean up when the animation completes). 346 mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener); 347 mCurrentAnimation.dispatchOnCancel(); 348 349 endProgress = 0; 350 if (progress <= 0) { 351 duration = 0; 352 startProgress = 0; 353 } else { 354 startProgress = Utilities.boundToRange(progress 355 + progressVelocity * getSingleFrameMs(mLauncher), 0f, 1f); 356 duration = BaseSwipeDetector.calculateDuration(velocity, 357 Math.min(progress, 1) - endProgress) * durationMultiplier; 358 } 359 } 360 mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState)); 361 ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); 362 anim.setFloatValues(startProgress, endProgress); 363 updateSwipeCompleteAnimation(anim, duration, targetState, velocity, fling); 364 mCurrentAnimation.dispatchOnStart(); 365 if (targetState == LauncherState.ALL_APPS) { 366 if (mAllAppsOvershootStarted) { 367 mLauncher.getAppsView().onRelease(); 368 mAllAppsOvershootStarted = false; 369 } else { 370 mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity, progress); 371 } 372 } 373 anim.start(); 374 } 375 updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling)376 protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, 377 LauncherState targetState, float velocity, boolean isFling) { 378 animator.setDuration(expectedDuration) 379 .setInterpolator(scrollInterpolatorForVelocity(velocity)); 380 } 381 onSwipeInteractionCompleted(LauncherState targetState)382 protected void onSwipeInteractionCompleted(LauncherState targetState) { 383 onReachedFinalState(mToState); 384 clearState(); 385 boolean shouldGoToTargetState = mGoingBetweenStates || (mToState != targetState); 386 if (shouldGoToTargetState) { 387 goToTargetState(targetState); 388 } else { 389 logReachedState(mToState); 390 } 391 } 392 goToTargetState(LauncherState targetState)393 protected void goToTargetState(LauncherState targetState) { 394 if (!mLauncher.isInState(targetState)) { 395 // If we're already in the target state, don't jump to it at the end of the animation in 396 // case the user started interacting with it before the animation finished. 397 mLauncher.getStateManager().goToState(targetState, false /* animated */, 398 forEndCallback(() -> logReachedState(targetState))); 399 } else { 400 logReachedState(targetState); 401 } 402 mLauncher.getRootView().getSysUiScrim().getSysUIMultiplier().animateToValue(1f) 403 .setDuration(0).start(); 404 } 405 logReachedState(LauncherState targetState)406 private void logReachedState(LauncherState targetState) { 407 if (mStartState == targetState) { 408 return; 409 } 410 // Transition complete. log the action 411 mLauncher.getStatsLogManager().logger() 412 .withSrcState(mStartState.statsLogOrdinal) 413 .withDstState(targetState.statsLogOrdinal) 414 .withContainerInfo(getContainerInfo(targetState)) 415 .log(StatsLogManager.getLauncherAtomEvent(mStartState.statsLogOrdinal, 416 targetState.statsLogOrdinal, mToState.ordinal > mFromState.ordinal 417 ? LAUNCHER_UNKNOWN_SWIPEUP 418 : LAUNCHER_UNKNOWN_SWIPEDOWN)); 419 } 420 getContainerInfo(LauncherState targetState)421 private LauncherAtom.ContainerInfo getContainerInfo(LauncherState targetState) { 422 if (targetState.isRecentsViewVisible) { 423 return LauncherAtom.ContainerInfo.newBuilder() 424 .setTaskSwitcherContainer( 425 LauncherAtom.TaskSwitcherContainer.getDefaultInstance() 426 ) 427 .build(); 428 } 429 430 return LauncherAtom.ContainerInfo.newBuilder() 431 .setWorkspace( 432 LauncherAtom.WorkspaceContainer.newBuilder() 433 .setPageIndex(mLauncher.getWorkspace().getCurrentPage())) 434 .build(); 435 } 436 clearState()437 protected void clearState() { 438 cancelAnimationControllers(); 439 mGoingBetweenStates = true; 440 mDetector.finishedScrolling(); 441 mDetector.setDetectableScrollConditions(0, false); 442 mIsTrackpadReverseScroll = false; 443 } 444 cancelAnimationControllers()445 private void cancelAnimationControllers() { 446 mCurrentAnimation = null; 447 } 448 shouldOpenAllApps(boolean isDragTowardPositive)449 protected boolean shouldOpenAllApps(boolean isDragTowardPositive) { 450 return (isDragTowardPositive && !mIsTrackpadReverseScroll) 451 || (!isDragTowardPositive && mIsTrackpadReverseScroll); 452 } 453 } 454