/* * 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(); } }