Files
lawnchair/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
Luca Zuccarini 1c89ee8cb1 Additional polish for the NORMAL<->ALL_APPS transitions.
The depth of ALL_APPS is set to 2 because in the legacy transitions the
swap happens at 0.5. The depth controller bounds the value to 1, so for
the second half of the animation the value is always 1. Because of this,
in order to have the right timing during the blur and scale we cap the
interpolator to 0.5.

Workspace fade is also removed, per spec.

Bug: 232418544
Test: manual
Change-Id: Idfd92c8b2b571ad911a0a5a59d39c5911e4131e4
2022-05-13 22:23:42 +00:00

333 lines
15 KiB
Java

/*
* Copyright (C) 2018 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.AbstractFloatingView.TYPE_ACCESSIBLE;
import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
import static com.android.launcher3.anim.Interpolators.INSTANT;
import static com.android.launcher3.anim.Interpolators.LINEAR;
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_HOTSEAT_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE;
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 android.view.MotionEvent;
import android.view.animation.Interpolator;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.touch.AbstractStateChangeTouchController;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.uioverrides.states.OverviewState;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
/**
* Touch controller for handling various state transitions in portrait UI.
*/
public class PortraitStatesTouchController extends AbstractStateChangeTouchController {
private static final String TAG = "PortraitStatesTouchCtrl";
/**
* The progress at which all apps content will be fully visible.
*/
public static final float ALL_APPS_CONTENT_FADE_MAX_CLAMPING_THRESHOLD = 0.8f;
/**
* Minimum clamping progress for fading in all apps content
*/
public static final float ALL_APPS_CONTENT_FADE_MIN_CLAMPING_THRESHOLD = 0.5f;
/**
* Minimum clamping progress for fading in all apps scrim
*/
public static final float ALL_APPS_SCRIM_VISIBLE_THRESHOLD = .1f;
/**
* Maximum clamping progress for opaque all apps scrim
*/
public static final float ALL_APPS_SCRIM_OPAQUE_THRESHOLD = .5f;
// Custom timing for NORMAL -> ALL_APPS on phones only.
private static final float ALL_APPS_STATE_TRANSITION = 0.4f;
private static final float ALL_APPS_FULL_DEPTH_PROGRESS = 0.5f;
// Custom interpolators for NORMAL -> ALL_APPS on phones only.
private static final Interpolator LINEAR_EARLY =
Interpolators.clampToProgress(LINEAR, 0f, ALL_APPS_STATE_TRANSITION);
private static final Interpolator STEP_TRANSITION =
Interpolators.clampToProgress(FINAL_FRAME, 0f, ALL_APPS_STATE_TRANSITION);
// The blur to and from All Apps is set to be complete when the interpolator is at 0.5.
public static final Interpolator BLUR =
Interpolators.clampToProgress(
Interpolators.mapToProgress(LINEAR, 0f, ALL_APPS_FULL_DEPTH_PROGRESS),
0f, ALL_APPS_STATE_TRANSITION);
public static final Interpolator WORKSPACE_FADE = STEP_TRANSITION;
public static final Interpolator WORKSPACE_SCALE = LINEAR_EARLY;
public static final Interpolator HOTSEAT_FADE = STEP_TRANSITION;
public static final Interpolator HOTSEAT_SCALE = LINEAR_EARLY;
public static final Interpolator HOTSEAT_TRANSLATE = STEP_TRANSITION;
public static final Interpolator SCRIM_FADE = LINEAR_EARLY;
public static final Interpolator ALL_APPS_FADE =
Interpolators.clampToProgress(LINEAR, ALL_APPS_STATE_TRANSITION, 1f);
public static final Interpolator ALL_APPS_VERTICAL_PROGRESS =
Interpolators.clampToProgress(
Interpolators.mapToProgress(LINEAR, ALL_APPS_STATE_TRANSITION, 1f),
ALL_APPS_STATE_TRANSITION, 1f);
private final PortraitOverviewStateTouchHelper mOverviewPortraitStateTouchHelper;
public PortraitStatesTouchController(Launcher l) {
super(l, SingleAxisSwipeDetector.VERTICAL);
mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l);
}
@Override
protected boolean canInterceptTouch(MotionEvent ev) {
// If we are swiping to all apps instead of overview, allow it from anywhere.
boolean interceptAnywhere = mLauncher.isInState(NORMAL);
if (mCurrentAnimation != null) {
AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()
|| interceptAnywhere) {
// If we are already animating from a previous state, we can intercept as long as
// the touch is below the current all apps progress (to allow for double swipe).
return true;
}
// Otherwise, don't intercept so they can scroll recents, dismiss a task, etc.
return false;
}
if (mLauncher.isInState(ALL_APPS)) {
// In all-apps only listen if the container cannot scroll itself
if (!mLauncher.getAppsView().shouldContainerScroll(ev)) {
return false;
}
} else if (mLauncher.isInState(OVERVIEW)) {
if (!mOverviewPortraitStateTouchHelper.canInterceptTouch(ev)) {
return false;
}
} else {
// For non-normal states, only listen if the event originated below the hotseat height
if (!interceptAnywhere && !isTouchOverHotseat(mLauncher, ev)) {
return false;
}
}
if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) {
return false;
}
return true;
}
@Override
protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
if (fromState == ALL_APPS && !isDragTowardPositive) {
return NORMAL;
} else if (fromState == OVERVIEW) {
return isDragTowardPositive ? OVERVIEW : NORMAL;
} else if (fromState == NORMAL && isDragTowardPositive) {
return ALL_APPS;
}
return fromState;
}
private StateAnimationConfig getNormalToAllAppsAnimation() {
StateAnimationConfig builder = new StateAnimationConfig();
if (mLauncher.getDeviceProfile().isTablet) {
builder.setInterpolator(ANIM_ALL_APPS_FADE, INSTANT);
builder.setInterpolator(ANIM_SCRIM_FADE,
Interpolators.clampToProgress(LINEAR,
ALL_APPS_SCRIM_VISIBLE_THRESHOLD,
ALL_APPS_SCRIM_OPAQUE_THRESHOLD));
} else {
// TODO(b/231682175): centralize this setup in AllAppsSwipeController.
builder.setInterpolator(ANIM_DEPTH, BLUR);
builder.setInterpolator(ANIM_WORKSPACE_FADE, WORKSPACE_FADE);
builder.setInterpolator(ANIM_WORKSPACE_SCALE, WORKSPACE_SCALE);
builder.setInterpolator(ANIM_HOTSEAT_FADE, HOTSEAT_FADE);
builder.setInterpolator(ANIM_HOTSEAT_SCALE, HOTSEAT_SCALE);
builder.setInterpolator(ANIM_HOTSEAT_TRANSLATE, HOTSEAT_TRANSLATE);
builder.setInterpolator(ANIM_SCRIM_FADE, SCRIM_FADE);
builder.setInterpolator(ANIM_ALL_APPS_FADE, ALL_APPS_FADE);
builder.setInterpolator(ANIM_VERTICAL_PROGRESS, ALL_APPS_VERTICAL_PROGRESS);
}
return builder;
}
private StateAnimationConfig getAllAppsToNormalAnimation() {
StateAnimationConfig builder = new StateAnimationConfig();
if (mLauncher.getDeviceProfile().isTablet) {
builder.setInterpolator(ANIM_ALL_APPS_FADE, FINAL_FRAME);
builder.setInterpolator(ANIM_SCRIM_FADE,
Interpolators.clampToProgress(LINEAR,
1 - ALL_APPS_SCRIM_OPAQUE_THRESHOLD,
1 - ALL_APPS_SCRIM_VISIBLE_THRESHOLD));
} else {
// These interpolators are the reverse of the ones used above, so swiping out of All
// Apps feels the same as swiping into it.
// TODO(b/231682175): centralize this setup in AllAppsSwipeController.
builder.setInterpolator(ANIM_DEPTH, Interpolators.reverse(BLUR));
builder.setInterpolator(ANIM_WORKSPACE_FADE, Interpolators.reverse(WORKSPACE_FADE));
builder.setInterpolator(ANIM_WORKSPACE_SCALE, Interpolators.reverse(WORKSPACE_SCALE));
builder.setInterpolator(ANIM_HOTSEAT_FADE, Interpolators.reverse(HOTSEAT_FADE));
builder.setInterpolator(ANIM_HOTSEAT_SCALE, Interpolators.reverse(HOTSEAT_SCALE));
builder.setInterpolator(ANIM_HOTSEAT_TRANSLATE,
Interpolators.reverse(HOTSEAT_TRANSLATE));
builder.setInterpolator(ANIM_SCRIM_FADE, Interpolators.reverse(SCRIM_FADE));
builder.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.reverse(ALL_APPS_FADE));
builder.setInterpolator(ANIM_VERTICAL_PROGRESS,
Interpolators.reverse(ALL_APPS_VERTICAL_PROGRESS));
}
return builder;
}
@Override
protected StateAnimationConfig getConfigForStates(
LauncherState fromState, LauncherState toState) {
final StateAnimationConfig config;
if (fromState == NORMAL && toState == ALL_APPS) {
config = getNormalToAllAppsAnimation();
} else if (fromState == ALL_APPS && toState == NORMAL) {
config = getAllAppsToNormalAnimation();
} else {
config = new StateAnimationConfig();
}
return config;
}
@Override
protected float initCurrentAnimation() {
float range = getShiftRange();
long maxAccuracy = (long) (2 * range);
float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
float totalShift = endVerticalShift - startVerticalShift;
final StateAnimationConfig config = totalShift == 0 ? new StateAnimationConfig()
: getConfigForStates(mFromState, mToState);
config.duration = maxAccuracy;
if (mCurrentAnimation != null) {
mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
mCurrentAnimation.dispatchOnCancel();
}
mGoingBetweenStates = true;
if (mFromState == OVERVIEW && mToState == NORMAL
&& mOverviewPortraitStateTouchHelper.shouldSwipeDownReturnToApp()) {
// Reset the state manager, when changing the interaction mode
mLauncher.getStateManager().goToState(OVERVIEW, false /* animate */);
mGoingBetweenStates = false;
mCurrentAnimation = mOverviewPortraitStateTouchHelper
.createSwipeDownToTaskAppAnimation(maxAccuracy, Interpolators.LINEAR)
.createPlaybackController();
mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation);
RecentsView recentsView = mLauncher.getOverviewPanel();
totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher,
mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler());
} else {
mCurrentAnimation = mLauncher.getStateManager()
.createAnimationToNewWorkspace(mToState, config);
}
mCurrentAnimation.getTarget().addListener(mClearStateOnCancelListener);
if (totalShift == 0) {
totalShift = Math.signum(mFromState.ordinal - mToState.ordinal)
* OverviewState.getDefaultSwipeHeight(mLauncher);
}
return 1 / totalShift;
}
@Override
protected void onSwipeInteractionCompleted(LauncherState targetState) {
super.onSwipeInteractionCompleted(targetState);
if (mStartState == NORMAL && targetState == OVERVIEW) {
SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
}
}
/**
* Whether the motion event is over the hotseat.
*
* @param launcher the launcher activity
* @param ev the event to check
* @return true if the event is over the hotseat
*/
static boolean isTouchOverHotseat(Launcher launcher, MotionEvent ev) {
DeviceProfile dp = launcher.getDeviceProfile();
int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
return (ev.getY() >= (launcher.getDragLayer().getHeight() - hotseatHeight));
}
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
InteractionJankMonitorWrapper.begin(
mLauncher.getRootView(), InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
InteractionJankMonitorWrapper.cancel(
InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
break;
}
return super.onControllerInterceptTouchEvent(ev);
}
@Override
protected void onReinitToState(LauncherState newToState) {
super.onReinitToState(newToState);
if (newToState != ALL_APPS) {
InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
}
}
@Override
protected void onReachedFinalState(LauncherState toState) {
super.onReachedFinalState(toState);
if (toState == ALL_APPS) {
InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
}
}
@Override
protected void clearState() {
super.clearState();
InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
}
}