| /* |
| * 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.BaseQuickstepLauncher; |
| import com.android.launcher3.LauncherState; |
| import com.android.launcher3.LauncherStateManager; |
| import com.android.launcher3.LauncherStateManager.AnimationConfig; |
| 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.SystemUiProxy; |
| 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 BaseQuickstepLauncher mLauncher; |
| private final BothAxesSwipeDetector mSwipeDetector; |
| private final ShelfPeekAnim mShelfPeekAnim; |
| private final float mXRange; |
| private final float mYRange; |
| private final MotionPauseDetector mMotionPauseDetector; |
| private final float mMotionPauseMinDisplacement; |
| |
| private boolean mNoIntercept; |
| private LauncherState mStartState; |
| |
| 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(BaseQuickstepLauncher launcher) { |
| mLauncher = launcher; |
| mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this); |
| mShelfPeekAnim = mLauncher.getShelfPeekAnim(); |
| 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 = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags(); |
| if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) { |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| public void onDragStart(boolean start) { |
| mMotionPauseDetector.clear(); |
| if (start) { |
| 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) { |
| // Flinging left and up, left and down, or right and up all go back home. |
| // Only flinging right and down goes to quick switch. |
| targetState = velocity.x < 0 || velocity.y < 0 ? NORMAL : QUICK_SWITCH; |
| } 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(); |
| } |
| } |