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