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.AbstractFloatingView.TYPE_ALL_APPS_EDU; 20 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType; 21 import static com.android.launcher3.LauncherState.ALL_APPS; 22 import static com.android.launcher3.LauncherState.NORMAL; 23 import static com.android.launcher3.LauncherState.OVERVIEW; 24 import static com.android.launcher3.anim.Interpolators.ACCEL; 25 import static com.android.launcher3.anim.Interpolators.DEACCEL; 26 import static com.android.launcher3.anim.Interpolators.LINEAR; 27 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS; 28 import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS; 29 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE; 30 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE; 31 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS; 32 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview; 33 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; 34 35 import android.animation.TimeInterpolator; 36 import android.animation.ValueAnimator; 37 import android.util.Log; 38 import android.view.MotionEvent; 39 import android.view.animation.Interpolator; 40 41 import com.android.launcher3.DeviceProfile; 42 import com.android.launcher3.Launcher; 43 import com.android.launcher3.LauncherState; 44 import com.android.launcher3.allapps.AllAppsTransitionController; 45 import com.android.launcher3.anim.Interpolators; 46 import com.android.launcher3.states.StateAnimationConfig; 47 import com.android.launcher3.states.StateAnimationConfig.AnimationFlags; 48 import com.android.launcher3.testing.TestProtocol; 49 import com.android.launcher3.touch.AbstractStateChangeTouchController; 50 import com.android.launcher3.touch.SingleAxisSwipeDetector; 51 import com.android.launcher3.uioverrides.states.OverviewState; 52 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 53 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; 54 import com.android.quickstep.SystemUiProxy; 55 import com.android.quickstep.TouchInteractionService; 56 import com.android.quickstep.util.LayoutUtils; 57 import com.android.quickstep.views.RecentsView; 58 59 /** 60 * Touch controller for handling various state transitions in portrait UI. 61 */ 62 public class PortraitStatesTouchController extends AbstractStateChangeTouchController { 63 64 private static final String TAG = "PortraitStatesTouchCtrl"; 65 66 /** 67 * The progress at which all apps content will be fully visible when swiping up from overview. 68 */ 69 protected static final float ALL_APPS_CONTENT_FADE_THRESHOLD = 0.08f; 70 71 /** 72 * The progress at which recents will begin fading out when swiping up from overview. 73 */ 74 private static final float RECENTS_FADE_THRESHOLD = 0.88f; 75 76 private final PortraitOverviewStateTouchHelper mOverviewPortraitStateTouchHelper; 77 78 private final InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper(); 79 80 private final boolean mAllowDragToOverview; 81 82 // If true, we will finish the current animation instantly on second touch. 83 private boolean mFinishFastOnSecondTouch; 84 PortraitStatesTouchController(Launcher l, boolean allowDragToOverview)85 public PortraitStatesTouchController(Launcher l, boolean allowDragToOverview) { 86 super(l, SingleAxisSwipeDetector.VERTICAL); 87 mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l); 88 mAllowDragToOverview = allowDragToOverview; 89 } 90 91 @Override canInterceptTouch(MotionEvent ev)92 protected boolean canInterceptTouch(MotionEvent ev) { 93 if (mCurrentAnimation != null) { 94 if (mFinishFastOnSecondTouch) { 95 mCurrentAnimation.getAnimationPlayer().end(); 96 } 97 98 AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); 99 if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()) { 100 // If we are already animating from a previous state, we can intercept as long as 101 // the touch is below the current all apps progress (to allow for double swipe). 102 return true; 103 } 104 // Otherwise, make sure everything is settled and don't intercept so they can scroll 105 // recents, dismiss a task, etc. 106 if (mAtomicAnim != null) { 107 mAtomicAnim.end(); 108 } 109 return false; 110 } 111 if (mLauncher.isInState(ALL_APPS)) { 112 // In all-apps only listen if the container cannot scroll itself 113 if (!mLauncher.getAppsView().shouldContainerScroll(ev)) { 114 return false; 115 } 116 } else if (mLauncher.isInState(OVERVIEW)) { 117 if (!mOverviewPortraitStateTouchHelper.canInterceptTouch(ev)) { 118 return false; 119 } 120 } else { 121 // If we are swiping to all apps instead of overview, allow it from anywhere. 122 boolean interceptAnywhere = mLauncher.isInState(NORMAL) && !mAllowDragToOverview; 123 // For all other states, only listen if the event originated below the hotseat height 124 if (!interceptAnywhere && !isTouchOverHotseat(mLauncher, ev)) { 125 return false; 126 } 127 } 128 if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) { 129 return false; 130 } 131 return true; 132 } 133 134 @Override getTargetState(LauncherState fromState, boolean isDragTowardPositive)135 protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { 136 if (TestProtocol.sDebugTracing) { 137 Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "PortraitStatesTouchController.getTargetState"); 138 } 139 if (fromState == ALL_APPS && !isDragTowardPositive) { 140 // Should swipe down go to OVERVIEW instead? 141 if (TestProtocol.sDebugTracing) { 142 Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, 143 "PortraitStatesTouchController.getTargetState 1"); 144 } 145 return TouchInteractionService.isConnected() ? 146 mLauncher.getStateManager().getLastState() : NORMAL; 147 } else if (fromState == OVERVIEW) { 148 if (TestProtocol.sDebugTracing) { 149 Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, 150 "PortraitStatesTouchController.getTargetState 2"); 151 } 152 LauncherState positiveDragTarget = ALL_APPS; 153 if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(mLauncher)) { 154 // Don't allow swiping up to all apps. 155 positiveDragTarget = OVERVIEW; 156 } 157 return isDragTowardPositive ? positiveDragTarget : NORMAL; 158 } else if (fromState == NORMAL && isDragTowardPositive) { 159 if (TestProtocol.sDebugTracing) { 160 Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, 161 "PortraitStatesTouchController.getTargetState 3"); 162 } 163 int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags(); 164 return mAllowDragToOverview && TouchInteractionService.isConnected() 165 && (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0 166 ? OVERVIEW : ALL_APPS; 167 } 168 return fromState; 169 } 170 171 @Override getLogContainerTypeForNormalState(MotionEvent ev)172 protected int getLogContainerTypeForNormalState(MotionEvent ev) { 173 return isTouchOverHotseat(mLauncher, ev) ? ContainerType.HOTSEAT : ContainerType.WORKSPACE; 174 } 175 getNormalToOverviewAnimation()176 private StateAnimationConfig getNormalToOverviewAnimation() { 177 mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR; 178 179 StateAnimationConfig builder = new StateAnimationConfig(); 180 builder.setInterpolator(ANIM_VERTICAL_PROGRESS, mAllAppsInterpolatorWrapper); 181 return builder; 182 } 183 getOverviewToAllAppsAnimation()184 private static StateAnimationConfig getOverviewToAllAppsAnimation() { 185 StateAnimationConfig builder = new StateAnimationConfig(); 186 builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL, 187 0, ALL_APPS_CONTENT_FADE_THRESHOLD)); 188 builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(DEACCEL, 189 RECENTS_FADE_THRESHOLD, 1)); 190 return builder; 191 } 192 getAllAppsToOverviewAnimation()193 private StateAnimationConfig getAllAppsToOverviewAnimation() { 194 StateAnimationConfig builder = new StateAnimationConfig(); 195 builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL, 196 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1)); 197 builder.setInterpolator(ANIM_OVERVIEW_FADE, Interpolators.clampToProgress(ACCEL, 198 0f, 1 - RECENTS_FADE_THRESHOLD)); 199 return builder; 200 } 201 getNormalToAllAppsAnimation()202 private StateAnimationConfig getNormalToAllAppsAnimation() { 203 StateAnimationConfig builder = new StateAnimationConfig(); 204 builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL, 205 0, ALL_APPS_CONTENT_FADE_THRESHOLD)); 206 return builder; 207 } 208 getAllAppsToNormalAnimation()209 private StateAnimationConfig getAllAppsToNormalAnimation() { 210 StateAnimationConfig builder = new StateAnimationConfig(); 211 builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(DEACCEL, 212 1 - ALL_APPS_CONTENT_FADE_THRESHOLD, 1)); 213 return builder; 214 } 215 216 @Override getConfigForStates( LauncherState fromState, LauncherState toState)217 protected StateAnimationConfig getConfigForStates( 218 LauncherState fromState, LauncherState toState) { 219 final StateAnimationConfig config; 220 if (fromState == NORMAL && toState == OVERVIEW) { 221 config = getNormalToOverviewAnimation(); 222 } else if (fromState == OVERVIEW && toState == ALL_APPS) { 223 config = getOverviewToAllAppsAnimation(); 224 } else if (fromState == ALL_APPS && toState == OVERVIEW) { 225 config = getAllAppsToOverviewAnimation(); 226 } else if (fromState == NORMAL && toState == ALL_APPS) { 227 config = getNormalToAllAppsAnimation(); 228 } else if (fromState == ALL_APPS && toState == NORMAL) { 229 config = getAllAppsToNormalAnimation(); 230 } else { 231 config = new StateAnimationConfig(); 232 } 233 return config; 234 } 235 236 @Override initCurrentAnimation(@nimationFlags int animFlags)237 protected float initCurrentAnimation(@AnimationFlags int animFlags) { 238 float range = getShiftRange(); 239 long maxAccuracy = (long) (2 * range); 240 241 float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range; 242 float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range; 243 244 float totalShift = endVerticalShift - startVerticalShift; 245 246 final StateAnimationConfig config = totalShift == 0 ? new StateAnimationConfig() 247 : getConfigForStates(mFromState, mToState); 248 config.animFlags = updateAnimComponentsOnReinit(animFlags); 249 config.duration = maxAccuracy; 250 251 cancelPendingAnim(); 252 253 if (mFromState == OVERVIEW && mToState == NORMAL 254 && mOverviewPortraitStateTouchHelper.shouldSwipeDownReturnToApp()) { 255 // Reset the state manager, when changing the interaction mode 256 mLauncher.getStateManager().goToState(OVERVIEW, false /* animate */); 257 mPendingAnimation = mOverviewPortraitStateTouchHelper 258 .createSwipeDownToTaskAppAnimation(maxAccuracy, Interpolators.LINEAR); 259 Runnable onCancelRunnable = () -> { 260 cancelPendingAnim(); 261 clearState(); 262 }; 263 mCurrentAnimation = mPendingAnimation.createPlaybackController() 264 .setOnCancelRunnable(onCancelRunnable); 265 mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation); 266 RecentsView recentsView = mLauncher.getOverviewPanel(); 267 totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher, 268 mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler()); 269 } else { 270 mCurrentAnimation = mLauncher.getStateManager() 271 .createAnimationToNewWorkspace(mToState, config) 272 .setOnCancelRunnable(this::clearState); 273 } 274 275 if (totalShift == 0) { 276 totalShift = Math.signum(mFromState.ordinal - mToState.ordinal) 277 * OverviewState.getDefaultSwipeHeight(mLauncher); 278 } 279 return 1 / totalShift; 280 } 281 282 /** 283 * Give subclasses the chance to update the animation when we re-initialize towards a new state. 284 */ 285 @AnimationFlags updateAnimComponentsOnReinit(@nimationFlags int animComponents)286 protected int updateAnimComponentsOnReinit(@AnimationFlags int animComponents) { 287 return animComponents; 288 } 289 cancelPendingAnim()290 private void cancelPendingAnim() { 291 if (mPendingAnimation != null) { 292 mPendingAnimation.finish(false, Touch.SWIPE); 293 mPendingAnimation = null; 294 } 295 } 296 297 @Override updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling)298 protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, 299 LauncherState targetState, float velocity, boolean isFling) { 300 super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, 301 velocity, isFling); 302 handleFirstSwipeToOverview(animator, expectedDuration, targetState, velocity, isFling); 303 } 304 handleFirstSwipeToOverview(final ValueAnimator animator, final long expectedDuration, final LauncherState targetState, final float velocity, final boolean isFling)305 private void handleFirstSwipeToOverview(final ValueAnimator animator, 306 final long expectedDuration, final LauncherState targetState, final float velocity, 307 final boolean isFling) { 308 if (UNSTABLE_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS 309 && targetState == OVERVIEW) { 310 mFinishFastOnSecondTouch = true; 311 } else if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) { 312 mFinishFastOnSecondTouch = true; 313 if (isFling && expectedDuration != 0) { 314 // Update all apps interpolator to add a bit of overshoot starting from currFraction 315 final float currFraction = mCurrentAnimation.getProgressFraction(); 316 mAllAppsInterpolatorWrapper.baseInterpolator = Interpolators.clampToProgress( 317 Interpolators.overshootInterpolatorForVelocity(velocity), currFraction, 1); 318 animator.setDuration(Math.min(expectedDuration, ATOMIC_DURATION)) 319 .setInterpolator(LINEAR); 320 } 321 } else { 322 mFinishFastOnSecondTouch = false; 323 } 324 } 325 326 @Override onSwipeInteractionCompleted(LauncherState targetState, int logAction)327 protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { 328 super.onSwipeInteractionCompleted(targetState, logAction); 329 if (mStartState == NORMAL && targetState == OVERVIEW) { 330 SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG); 331 } 332 } 333 334 /** 335 * Whether the motion event is over the hotseat. 336 * 337 * @param launcher the launcher activity 338 * @param ev the event to check 339 * @return true if the event is over the hotseat 340 */ isTouchOverHotseat(Launcher launcher, MotionEvent ev)341 static boolean isTouchOverHotseat(Launcher launcher, MotionEvent ev) { 342 return (ev.getY() >= getHotseatTop(launcher)); 343 } 344 getHotseatTop(Launcher launcher)345 public static int getHotseatTop(Launcher launcher) { 346 DeviceProfile dp = launcher.getDeviceProfile(); 347 int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom; 348 return launcher.getDragLayer().getHeight() - hotseatHeight; 349 } 350 351 private static class InterpolatorWrapper implements Interpolator { 352 353 public TimeInterpolator baseInterpolator = LINEAR; 354 355 @Override getInterpolation(float v)356 public float getInterpolation(float v) { 357 return baseInterpolator.getInterpolation(v); 358 } 359 } 360 } 361