Files
Lawnchair/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
T
Ats Jenk ce6cebd392 Disable quickswitch for desktop windowing
When desktop windowing prototypes are enabled, disable quickswitch for
now.
Quickswitch causes issues with how tasks are moved between desktop and
fullscreen in prototype 2. We are currently doing it based on shell
transitions. And whether there are desktop tasks visible or not.
Directly switching from a fullscreen task to desktop tasks does not fit
well into this model. And supporting this is not trivial.
As quickswitch is not a core user journey right now, will disable
quickswitch for the time being.
We can revisit this at a later date, when building out the production
version.

Bug: 268075592
Test: enable proto 1, try to quickswitch from home screen and from apps
Test: enable proto 2, try to quickswitch from home screen, from
  fullscreen app to desktop, fullscreen app to another fullscreen app,
  from desktop to fullscreen app
Change-Id: I06b3461f0134819cfa7c9e5f61df8ed84cc90eb8
2023-02-14 10:47:37 -08:00

476 lines
23 KiB
Java

/*
* 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.newCancelListener;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
import static com.android.launcher3.states.StateAnimationConfig.SKIP_ALL_ANIMATIONS;
import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_RIGHT;
import static com.android.launcher3.touch.BothAxesSwipeDetector.DIRECTION_UP;
import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.animation.Interpolator;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.touch.BaseSwipeDetector;
import com.android.launcher3.touch.BothAxesSwipeDetector;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.util.VibratorWrapper;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.AnimatorControllerWithResistance;
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.util.MotionPauseDetector;
import com.android.quickstep.util.WorkspaceRevealAnim;
import com.android.quickstep.views.DesktopTaskView;
import com.android.quickstep.views.LauncherRecentsView;
import com.android.quickstep.views.RecentsView;
/**
* 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 {
private static final float Y_ANIM_MIN_PROGRESS = 0.25f;
private static final Interpolator FADE_OUT_INTERPOLATOR = DEACCEL_3;
private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75;
private static final Interpolator SCALE_DOWN_INTERPOLATOR = LINEAR;
private static final long ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW = 300;
private final QuickstepLauncher mLauncher;
private final BothAxesSwipeDetector mSwipeDetector;
private final float mXRange;
private final float mYRange;
private final float mMaxYProgress;
private final MotionPauseDetector mMotionPauseDetector;
private final float mMotionPauseMinDisplacement;
private final LauncherRecentsView mRecentsView;
protected final AnimatorListener mClearStateOnCancelListener =
newCancelListener(this::clearState);
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 AnimatedFloat mYOverviewAnim;
public NoButtonQuickSwitchTouchController(QuickstepLauncher launcher) {
mLauncher = launcher;
mSwipeDetector = new BothAxesSwipeDetector(mLauncher, this);
mRecentsView = mLauncher.getOverviewPanel();
mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
mYRange = LayoutUtils.getShelfTrackingDistance(
mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange;
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;
}
if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
// TODO(b/268075592): add support for quickswitch to/from desktop
return false;
}
return true;
}
@Override
public void onDragStart(boolean start) {
mMotionPauseDetector.clear();
if (start) {
mStartState = mLauncher.getStateManager().getState();
mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
// We have detected horizontal drag start, now allow swipe up as well.
mSwipeDetector.setDetectableScrollConditions(DIRECTION_RIGHT | DIRECTION_UP,
false /* ignoreSlopWhenSettling */);
setupAnimators();
}
}
private void onMotionPauseDetected() {
VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
}
private void setupAnimators() {
// Animate the non-overview components (e.g. workspace, shelf) out of the way.
StateAnimationConfig nonOverviewBuilder = new StateAnimationConfig();
nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_FADE, FADE_OUT_INTERPOLATOR);
nonOverviewBuilder.setInterpolator(ANIM_ALL_APPS_FADE, FADE_OUT_INTERPOLATOR);
nonOverviewBuilder.setInterpolator(ANIM_WORKSPACE_SCALE, FADE_OUT_INTERPOLATOR);
nonOverviewBuilder.setInterpolator(ANIM_DEPTH, FADE_OUT_INTERPOLATOR);
nonOverviewBuilder.setInterpolator(ANIM_VERTICAL_PROGRESS, TRANSLATE_OUT_INTERPOLATOR);
updateNonOverviewAnim(QUICK_SWITCH_FROM_HOME, nonOverviewBuilder);
mNonOverviewAnim.dispatchOnStart();
if (mRecentsView.getTaskViewCount() == 0) {
mRecentsView.setOnEmptyMessageUpdatedListener(isEmpty -> {
if (!isEmpty && mSwipeDetector.isDraggingState()) {
// We have loaded tasks, update the animators to start at the correct scale etc.
setupOverviewAnimators();
}
});
}
setupOverviewAnimators();
}
/** Create state animation to control non-overview components. */
private void updateNonOverviewAnim(LauncherState toState, StateAnimationConfig config) {
config.duration = (long) (Math.max(mXRange, mYRange) * 2);
config.animFlags |= SKIP_OVERVIEW | SKIP_SCRIM;
mNonOverviewAnim = mLauncher.getStateManager()
.createAnimationToNewWorkspace(toState, config);
mNonOverviewAnim.getTarget().addListener(mClearStateOnCancelListener);
}
private void setupOverviewAnimators() {
final LauncherState fromState = QUICK_SWITCH_FROM_HOME;
final LauncherState toState = OVERVIEW;
// Set RecentView's initial properties.
RECENTS_SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, 1f);
TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, fromState.showTaskThumbnailSplash() ? 1f : 0);
mRecentsView.setContentAlpha(1);
mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
mLauncher.getActionsView().getVisibilityAlpha().setValue(
(fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f);
mRecentsView.setTaskIconScaledDown(true);
float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
// As we drag right, animate the following properties:
// - RecentsView translationX
// - OverviewScrim
// - RecentsView fade (if it's empty)
PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2));
xAnim.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1], LINEAR);
// Use QuickSwitchState instead of OverviewState to determine scrim color,
// since we need to take potential taskbar into account.
xAnim.setViewBackgroundColor(mLauncher.getScrimView(),
QUICK_SWITCH_FROM_HOME.getWorkspaceScrimColor(mLauncher), LINEAR);
if (mRecentsView.getTaskViewCount() == 0) {
xAnim.addFloat(mRecentsView, CONTENT_ALPHA, 0f, 1f, LINEAR);
}
mXOverviewAnim = xAnim.createPlaybackController();
mXOverviewAnim.dispatchOnStart();
// As we drag up, animate the following properties:
// - RecentsView scale
// - RecentsView fullscreenProgress
PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
yAnim.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
SCALE_DOWN_INTERPOLATOR);
yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
AnimatorPlaybackController yNormalController = yAnim.createPlaybackController();
AnimatorControllerWithResistance yAnimWithResistance = AnimatorControllerWithResistance
.createForRecents(yNormalController, mLauncher,
mRecentsView.getPagedViewOrientedState(), mLauncher.getDeviceProfile(),
mRecentsView, RECENTS_SCALE_PROPERTY, mRecentsView,
TASK_SECONDARY_TRANSLATION);
mYOverviewAnim = new AnimatedFloat(() -> {
if (mYOverviewAnim != null) {
yAnimWithResistance.setProgress(mYOverviewAnim.value, mMaxYProgress);
}
});
yNormalController.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;
mMotionPauseDetector.setDisallowPause(-displacement.y < mMotionPauseMinDisplacement);
mMotionPauseDetector.addPosition(ev);
if (mXOverviewAnim != null) {
mXOverviewAnim.setPlayFraction(xProgress);
}
if (mYOverviewAnim != null) {
mYOverviewAnim.updateValue(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;
if (mMotionPauseDetector.isPaused() && noFling) {
// Going to Overview.
cancelAnimations();
StateAnimationConfig config = new StateAnimationConfig();
config.duration = ATOMIC_DURATION_FROM_PAUSED_TO_OVERVIEW;
Animator overviewAnim = mLauncher.getStateManager().createAtomicAnimation(
mStartState, OVERVIEW, config);
overviewAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
onAnimationToStateCompleted(OVERVIEW);
// Animate the icon after onAnimationToStateCompleted() so it doesn't clobber.
mRecentsView.animateUpTaskIconScale();
}
});
overviewAnim.start();
// Create an empty state transition so StateListeners get onStateTransitionStart().
mLauncher.getStateManager().createAnimationToNewWorkspace(
OVERVIEW, config.duration, StateAnimationConfig.SKIP_ALL_ANIMATIONS)
.dispatchOnStart();
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_FROM_HOME;
} 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_FROM_HOME : NORMAL;
}
}
} else if (horizontalFling) {
targetState = velocity.x > 0 ? QUICK_SWITCH_FROM_HOME : NORMAL;
} else if (verticalFling) {
targetState = velocity.y > 0 ? QUICK_SWITCH_FROM_HOME : NORMAL;
} else {
// If user isn't flinging, just snap to the closest state.
boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f;
boolean passedVerticalThreshold = mYOverviewAnim.value > 1f;
targetState = passedHorizontalThreshold && !passedVerticalThreshold
? QUICK_SWITCH_FROM_HOME : 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.dispatchOnStart();
boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
float yProgress = mYOverviewAnim.value;
float startYProgress = Utilities.boundToRange(yProgress
- velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, mMaxYProgress);
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;
}
float yDistanceToCover = Math.abs(endYProgress - startYProgress) * mYRange;
long yDuration = (long) (yDistanceToCover / Math.max(1f, Math.abs(velocity.y)));
ValueAnimator yOverviewAnim = mYOverviewAnim.animateToValue(startYProgress, endYProgress);
yOverviewAnim.setDuration(yDuration);
mYOverviewAnim.updateValue(startYProgress);
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.
StateAnimationConfig config = new StateAnimationConfig();
// Update mNonOverviewAnim to do nothing so it doesn't interfere.
config.animFlags = SKIP_ALL_ANIMATIONS;
updateNonOverviewAnim(targetState, config);
nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
mNonOverviewAnim.dispatchOnStart();
new WorkspaceRevealAnim(mLauncher, 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.getTarget().removeListener(mClearStateOnCancelListener);
mNonOverviewAnim.dispatchOnCancel();
}
float startProgress = mNonOverviewAnim.getProgressFraction();
float endProgress = canceled ? 0 : 1;
nonOverviewAnim.setFloatValues(startProgress, endProgress);
mNonOverviewAnim.dispatchOnStart();
}
if (targetState == QUICK_SWITCH_FROM_HOME) {
// Navigating to quick switch, add scroll feedback since the first time is not
// considered a scroll by the RecentsView.
VibratorWrapper.INSTANCE.get(mLauncher).vibrate(
RecentsView.SCROLL_VIBRATION_PRIMITIVE,
RecentsView.SCROLL_VIBRATION_PRIMITIVE_SCALE,
RecentsView.SCROLL_VIBRATION_FALLBACK);
}
nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
mNonOverviewAnim.setEndAction(() -> onAnimationToStateCompleted(targetState));
cancelAnimations();
xOverviewAnim.start();
yOverviewAnim.start();
nonOverviewAnim.start();
}
private void onAnimationToStateCompleted(LauncherState targetState) {
mLauncher.getStatsLogManager().logger()
.withSrcState(LAUNCHER_STATE_HOME)
.withDstState(targetState.statsLogOrdinal)
.log(getLauncherAtomEvent(mStartState.statsLogOrdinal, targetState.statsLogOrdinal,
targetState == QUICK_SWITCH_FROM_HOME
? LAUNCHER_QUICKSWITCH_RIGHT
: targetState.ordinal > mStartState.ordinal
? LAUNCHER_UNKNOWN_SWIPEUP
: LAUNCHER_UNKNOWN_SWIPEDOWN));
mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState));
}
private void cancelAnimations() {
if (mNonOverviewAnim != null) {
mNonOverviewAnim.getAnimationPlayer().cancel();
}
if (mXOverviewAnim != null) {
mXOverviewAnim.getAnimationPlayer().cancel();
}
if (mYOverviewAnim != null) {
mYOverviewAnim.cancelAnimation();
}
mMotionPauseDetector.clear();
}
private void clearState() {
cancelAnimations();
mNonOverviewAnim = null;
mXOverviewAnim = null;
mYOverviewAnim = null;
mIsHomeScreenVisible = true;
mSwipeDetector.finishedScrolling();
mRecentsView.setOnEmptyMessageUpdatedListener(null);
}
}