blob: 9ad063837d8f4c46a22e54d6041d7dc8215c292f [file] [log] [blame]
/*
* 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();
}
}