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