diff options
Diffstat (limited to 'quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java')
-rw-r--r-- | quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java | 473 |
1 files changed, 473 insertions, 0 deletions
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java new file mode 100644 index 000000000..76374af6a --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.uioverrides.touchcontrollers; + +import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; +import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_PAUSE_TO_OVERVIEW_ANIM; +import static com.android.launcher3.LauncherState.HOTSEAT_ICONS; +import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.LauncherState.QUICK_SWITCH; +import static com.android.launcher3.LauncherStateManager.ANIM_ALL; +import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE; +import static com.android.launcher3.anim.AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW; +import static com.android.launcher3.anim.Interpolators.ACCEL_0_75; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.launcher3.anim.Interpolators.DEACCEL_5; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; +import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT; +import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP; +import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs; +import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; +import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.CANCEL; +import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE; +import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK; +import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.graphics.PointF; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.Interpolator; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.LauncherStateManager.AnimationConfig; +import com.android.launcher3.QuickstepAppTransitionManagerImpl; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.graphics.OverviewScrim; +import com.android.launcher3.touch.BaseSwipeDetector; +import com.android.launcher3.touch.BothAxesSwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.TouchController; +import com.android.launcher3.util.VibratorWrapper; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.util.MotionPauseDetector; +import com.android.quickstep.util.ShelfPeekAnim; +import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState; +import com.android.quickstep.util.StaggeredWorkspaceAnim; +import com.android.quickstep.views.LauncherRecentsView; + +/** + * Handles quick switching to a recent task from the home screen. To give as much flexibility to + * the user as possible, also handles swipe up and hold to go to overview and swiping back home. + */ +public class NoButtonQuickSwitchTouchController implements TouchController, + BothAxesSwipeDetector.Listener, MotionPauseDetector.OnMotionPauseListener { + + /** The minimum progress of the scale/translationY animation until drag end. */ + private static final float Y_ANIM_MIN_PROGRESS = 0.15f; + private static final Interpolator FADE_OUT_INTERPOLATOR = DEACCEL_5; + private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75; + private static final Interpolator SCALE_DOWN_INTERPOLATOR = DEACCEL; + + private final Launcher mLauncher; + private final BothAxesSwipeDetector mSwipeDetector; + private final float mXRange; + private final float mYRange; + private final MotionPauseDetector mMotionPauseDetector; + private final float mMotionPauseMinDisplacement; + + private boolean mNoIntercept; + private LauncherState mStartState; + + private ShelfPeekAnim mShelfPeekAnim; + private boolean mIsHomeScreenVisible = true; + + // As we drag, we control 3 animations: one to get non-overview components out of the way, + // and the other two to set overview properties based on x and y progress. + private AnimatorPlaybackController mNonOverviewAnim; + private AnimatorPlaybackController mXOverviewAnim; + private AnimatorPlaybackController mYOverviewAnim; + + public NoButtonQuickSwitchTouchController(Launcher launcher) { + mLauncher = launcher; + mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this); + mXRange = mLauncher.getDeviceProfile().widthPx / 2f; + mYRange = LayoutUtils.getShelfTrackingDistance(mLauncher, mLauncher.getDeviceProfile()); + mMotionPauseDetector = new MotionPauseDetector(mLauncher); + mMotionPauseMinDisplacement = mLauncher.getResources().getDimension( + R.dimen.motion_pause_detector_min_displacement_from_app); + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mNoIntercept = !canInterceptTouch(ev); + if (mNoIntercept) { + return false; + } + + // Only detect horizontal swipe for intercept, then we will allow swipe up as well. + mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT, + false /* ignoreSlopWhenSettling */); + } + + if (mNoIntercept) { + return false; + } + + onControllerTouchEvent(ev); + return mSwipeDetector.isDraggingOrSettling(); + } + + @Override + public boolean onControllerTouchEvent(MotionEvent ev) { + return mSwipeDetector.onTouchEvent(ev); + } + + private boolean canInterceptTouch(MotionEvent ev) { + if (!mLauncher.isInState(LauncherState.NORMAL)) { + return false; + } + if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) { + return false; + } + int stateFlags = OverviewInteractionState.INSTANCE.get(mLauncher).getSystemUiStateFlags(); + if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) { + return false; + } + return true; + } + + @Override + public void onDragStart(boolean start) { + mMotionPauseDetector.clear(); + if (start) { + mShelfPeekAnim = ((QuickstepAppTransitionManagerImpl) mLauncher + .getAppTransitionManager()).getShelfPeekAnim(); + + mStartState = mLauncher.getStateManager().getState(); + + mMotionPauseDetector.setOnMotionPauseListener(this); + + // We have detected horizontal drag start, now allow swipe up as well. + mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT | DIRECTION_UP, + false /* ignoreSlopWhenSettling */); + + setupAnimators(); + } + } + + @Override + public void onMotionPauseChanged(boolean isPaused) { + ShelfAnimState shelfState = isPaused ? PEEK : HIDE; + if (shelfState == PEEK) { + // Some shelf elements (e.g. qsb) were hidden, but we need them visible when peeking. + AnimatorSetBuilder builder = new AnimatorSetBuilder(); + AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); + allAppsController.setAlphas(NORMAL.getVisibleElements(mLauncher), + new AnimationConfig(), builder); + builder.build().setDuration(0).start(); + + if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) { + // Hotseat was hidden, but we need it visible when peeking. + mLauncher.getHotseat().setAlpha(1); + } + } + mShelfPeekAnim.setShelfState(shelfState, ShelfPeekAnim.INTERPOLATOR, + ShelfPeekAnim.DURATION); + VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC); + } + + private void setupAnimators() { + // Animate the non-overview components (e.g. workspace, shelf) out of the way. + AnimatorSetBuilder nonOverviewBuilder = new AnimatorSetBuilder(); + nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_FADE, FADE_OUT_INTERPOLATOR); + nonOverviewBuilder.setInterpolator(ANIM_ALL_APPS_FADE, FADE_OUT_INTERPOLATOR); + nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, TRANSLATE_OUT_INTERPOLATOR); + nonOverviewBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, TRANSLATE_OUT_INTERPOLATOR); + updateNonOverviewAnim(QUICK_SWITCH, nonOverviewBuilder, ANIM_ALL); + mNonOverviewAnim.dispatchOnStart(); + + setupOverviewAnimators(); + } + + /** Create state animation to control non-overview components. */ + private void updateNonOverviewAnim(LauncherState toState, AnimatorSetBuilder builder, + @LauncherStateManager.AnimationComponents int animComponents) { + builder.addFlag(FLAG_DONT_ANIMATE_OVERVIEW); + long accuracy = (long) (Math.max(mXRange, mYRange) * 2); + mNonOverviewAnim = mLauncher.getStateManager().createAnimationToNewWorkspace(toState, + builder, accuracy, this::clearState, animComponents); + } + + private void setupOverviewAnimators() { + final LauncherState fromState = QUICK_SWITCH; + final LauncherState toState = OVERVIEW; + LauncherState.ScaleAndTranslation fromScaleAndTranslation = fromState + .getOverviewScaleAndTranslation(mLauncher); + LauncherState.ScaleAndTranslation toScaleAndTranslation = toState + .getOverviewScaleAndTranslation(mLauncher); + // Update RecentView's translationX to have it start offscreen. + LauncherRecentsView recentsView = mLauncher.getOverviewPanel(); + float startScale = Utilities.mapRange( + SCALE_DOWN_INTERPOLATOR.getInterpolation(Y_ANIM_MIN_PROGRESS), + fromScaleAndTranslation.scale, + toScaleAndTranslation.scale); + fromScaleAndTranslation.translationX = recentsView.getOffscreenTranslationX(startScale); + + // Set RecentView's initial properties. + recentsView.setScaleX(fromScaleAndTranslation.scale); + recentsView.setScaleY(fromScaleAndTranslation.scale); + recentsView.setTranslationX(fromScaleAndTranslation.translationX); + recentsView.setTranslationY(fromScaleAndTranslation.translationY); + recentsView.setContentAlpha(1); + + // As we drag right, animate the following properties: + // - RecentsView translationX + // - OverviewScrim + AnimatorSet xOverviewAnim = new AnimatorSet(); + xOverviewAnim.play(ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_X, + toScaleAndTranslation.translationX)); + xOverviewAnim.play(ObjectAnimator.ofFloat( + mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS, + toState.getOverviewScrimAlpha(mLauncher))); + long xAccuracy = (long) (mXRange * 2); + xOverviewAnim.setDuration(xAccuracy); + mXOverviewAnim = AnimatorPlaybackController.wrap(xOverviewAnim, xAccuracy); + mXOverviewAnim.dispatchOnStart(); + + // As we drag up, animate the following properties: + // - RecentsView translationY + // - RecentsView scale + // - RecentsView fullscreenProgress + AnimatorSet yAnimation = new AnimatorSet(); + Animator translateYAnim = ObjectAnimator.ofFloat(recentsView, View.TRANSLATION_Y, + toScaleAndTranslation.translationY); + Animator scaleAnim = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, + toScaleAndTranslation.scale); + Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, + fromState.getOverviewFullscreenProgress(), toState.getOverviewFullscreenProgress()); + scaleAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR); + fullscreenProgressAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR); + yAnimation.play(translateYAnim); + yAnimation.play(scaleAnim); + yAnimation.play(fullscreenProgressAnim); + long yAccuracy = (long) (mYRange * 2); + yAnimation.setDuration(yAccuracy); + mYOverviewAnim = AnimatorPlaybackController.wrap(yAnimation, yAccuracy); + mYOverviewAnim.dispatchOnStart(); + } + + @Override + public boolean onDrag(PointF displacement, MotionEvent ev) { + float xProgress = Math.max(0, displacement.x) / mXRange; + float yProgress = Math.max(0, -displacement.y) / mYRange; + yProgress = Utilities.mapRange(yProgress, Y_ANIM_MIN_PROGRESS, 1f); + + boolean wasHomeScreenVisible = mIsHomeScreenVisible; + if (wasHomeScreenVisible && mNonOverviewAnim != null) { + mNonOverviewAnim.setPlayFraction(xProgress); + } + mIsHomeScreenVisible = FADE_OUT_INTERPOLATOR.getInterpolation(xProgress) + <= 1 - ALPHA_CUTOFF_THRESHOLD; + + if (wasHomeScreenVisible && !mIsHomeScreenVisible) { + // Get the shelf all the way offscreen so it pops up when we decide to peek it. + mShelfPeekAnim.setShelfState(HIDE, LINEAR, 0); + } + + // Only allow motion pause if the home screen is invisible, since some + // home screen elements will appear in the shelf on motion pause. + mMotionPauseDetector.setDisallowPause(mIsHomeScreenVisible + || -displacement.y < mMotionPauseMinDisplacement); + mMotionPauseDetector.addPosition(displacement.y, ev.getEventTime()); + + if (mIsHomeScreenVisible) { + // Cancel the shelf anim so it doesn't clobber mNonOverviewAnim. + mShelfPeekAnim.setShelfState(CANCEL, LINEAR, 0); + } + + if (mXOverviewAnim != null) { + mXOverviewAnim.setPlayFraction(xProgress); + } + if (mYOverviewAnim != null) { + mYOverviewAnim.setPlayFraction(yProgress); + } + return true; + } + + @Override + public void onDragEnd(PointF velocity) { + boolean horizontalFling = mSwipeDetector.isFling(velocity.x); + boolean verticalFling = mSwipeDetector.isFling(velocity.y); + boolean noFling = !horizontalFling && !verticalFling; + int logAction = noFling ? Touch.SWIPE : Touch.FLING; + if (mMotionPauseDetector.isPaused() && noFling) { + cancelAnimations(); + + Animator overviewAnim = mLauncher.getAppTransitionManager().createStateElementAnimation( + INDEX_PAUSE_TO_OVERVIEW_ANIM); + overviewAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onAnimationToStateCompleted(OVERVIEW, logAction); + } + }); + overviewAnim.start(); + return; + } + + final LauncherState targetState; + if (horizontalFling && verticalFling) { + if (velocity.x < 0) { + // Flinging left and up or down both go back home. + targetState = NORMAL; + } else { + if (velocity.y > 0) { + // Flinging right and down goes to quick switch. + targetState = QUICK_SWITCH; + } else { + // Flinging up and right could go either home or to quick switch. + // Determine the target based on the higher velocity. + targetState = Math.abs(velocity.x) > Math.abs(velocity.y) + ? QUICK_SWITCH : NORMAL; + } + } + } else if (horizontalFling) { + targetState = velocity.x > 0 ? QUICK_SWITCH : NORMAL; + } else if (verticalFling) { + targetState = velocity.y > 0 ? QUICK_SWITCH : NORMAL; + } else { + // If user isn't flinging, just snap to the closest state based on x progress. + boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f; + targetState = passedHorizontalThreshold ? QUICK_SWITCH : NORMAL; + } + + // Animate the various components to the target state. + + float xProgress = mXOverviewAnim.getProgressFraction(); + float startXProgress = Utilities.boundToRange(xProgress + + velocity.x * getSingleFrameMs(mLauncher) / mXRange, 0f, 1f); + final float endXProgress = targetState == NORMAL ? 0 : 1; + long xDuration = BaseSwipeDetector.calculateDuration(velocity.x, + Math.abs(endXProgress - startXProgress)); + ValueAnimator xOverviewAnim = mXOverviewAnim.getAnimationPlayer(); + xOverviewAnim.setFloatValues(startXProgress, endXProgress); + xOverviewAnim.setDuration(xDuration) + .setInterpolator(scrollInterpolatorForVelocity(velocity.x)); + mXOverviewAnim.dispatchOnStartWithVelocity(endXProgress, velocity.x); + + boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL; + + float yProgress = mYOverviewAnim.getProgressFraction(); + float startYProgress = Utilities.boundToRange(yProgress + - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, 1f); + final float endYProgress; + if (flingUpToNormal) { + endYProgress = 1; + } else if (targetState == NORMAL) { + // Keep overview at its current scale/translationY as it slides off the screen. + endYProgress = startYProgress; + } else { + endYProgress = 0; + } + long yDuration = BaseSwipeDetector.calculateDuration(velocity.y, + Math.abs(endYProgress - startYProgress)); + ValueAnimator yOverviewAnim = mYOverviewAnim.getAnimationPlayer(); + yOverviewAnim.setFloatValues(startYProgress, endYProgress); + yOverviewAnim.setDuration(yDuration); + mYOverviewAnim.dispatchOnStartWithVelocity(endYProgress, velocity.y); + + ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer(); + if (flingUpToNormal && !mIsHomeScreenVisible) { + // We are flinging to home while workspace is invisible, run the same staggered + // animation as from an app. + // Update mNonOverviewAnim to do nothing so it doesn't interfere. + updateNonOverviewAnim(targetState, new AnimatorSetBuilder(), 0 /* animComponents */); + nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer(); + + new StaggeredWorkspaceAnim(mLauncher, velocity.y, false /* animateOverviewScrim */) + .start(); + } else { + boolean canceled = targetState == NORMAL; + if (canceled) { + // Let the state manager know that the animation didn't go to the target state, + // but don't clean up yet (we already clean up when the animation completes). + mNonOverviewAnim.dispatchOnCancelWithoutCancelRunnable(); + } + float startProgress = mNonOverviewAnim.getProgressFraction(); + float endProgress = canceled ? 0 : 1; + nonOverviewAnim.setFloatValues(startProgress, endProgress); + mNonOverviewAnim.dispatchOnStartWithVelocity(endProgress, + horizontalFling ? velocity.x : velocity.y); + } + + nonOverviewAnim.setDuration(Math.max(xDuration, yDuration)); + mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState, logAction)); + + cancelAnimations(); + xOverviewAnim.start(); + yOverviewAnim.start(); + nonOverviewAnim.start(); + } + + private void onAnimationToStateCompleted(LauncherState targetState, int logAction) { + mLauncher.getUserEventDispatcher().logStateChangeAction(logAction, + getDirectionForLog(), mSwipeDetector.getDownX(), mSwipeDetector.getDownY(), + LauncherLogProto.ContainerType.NAVBAR, + mStartState.containerType, + targetState.containerType, + mLauncher.getWorkspace().getCurrentPage()); + mLauncher.getStateManager().goToState(targetState, false, this::clearState); + } + + private int getDirectionForLog() { + return Utilities.isRtl(mLauncher.getResources()) ? Direction.LEFT : Direction.RIGHT; + } + + private void cancelAnimations() { + if (mNonOverviewAnim != null) { + mNonOverviewAnim.getAnimationPlayer().cancel(); + } + if (mXOverviewAnim != null) { + mXOverviewAnim.getAnimationPlayer().cancel(); + } + if (mYOverviewAnim != null) { + mYOverviewAnim.getAnimationPlayer().cancel(); + } + mShelfPeekAnim.setShelfState(ShelfAnimState.CANCEL, LINEAR, 0); + mMotionPauseDetector.clear(); + } + + private void clearState() { + cancelAnimations(); + mNonOverviewAnim = null; + mXOverviewAnim = null; + mYOverviewAnim = null; + mIsHomeScreenVisible = true; + mSwipeDetector.finishedScrolling(); + } +} |