a04e746e84
When ending in recents, we reapply the state - therefore, it's important to make sure we are in OverviewState before onSwipeUpComplete(). This is done by mLauncherTransitionController, which sets OverviewState on end. We already force mLauncherTransitionController to end before calling onSwipeUpComplete(), but in this case we were calling cancel() and setting mLauncherTransitionController = null, which meant we could never call end() on it. Instead, we should always call end() if we set it to null. Also ensure mLauncherTransitionController is created even if the gesture is completed, if an existing controller isn't already running. This can happen if you swipe up quickly enough that we get onGestureEnd before launcher is drawn, and in that case we still want the launcher component to animate once its ready. This is even more important for 2-button mode, because again, we rely on mLauncherTransitionController to set the state to OverviewState before we reapply it. Finally, clarified some methods by renaming "swipeUp" to "swipeUpToRecents". Bug: 132757019 Change-Id: Ieb24a4f36a39780e5d64d7bc312791608db474d1
446 lines
19 KiB
Java
446 lines
19 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.quickstep;
|
|
|
|
import static android.view.View.TRANSLATION_Y;
|
|
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
|
|
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
|
|
import static com.android.launcher3.LauncherState.NORMAL;
|
|
import static com.android.launcher3.LauncherState.OVERVIEW;
|
|
import static com.android.launcher3.allapps.AllAppsTransitionController.SPRING_DAMPING_RATIO;
|
|
import static com.android.launcher3.allapps.AllAppsTransitionController.SPRING_STIFFNESS;
|
|
import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
|
|
import static com.android.launcher3.anim.Interpolators.LINEAR;
|
|
import static com.android.quickstep.WindowTransformSwipeHandler.RECENTS_ATTACH_DURATION;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.AnimatorSet;
|
|
import android.animation.ObjectAnimator;
|
|
import android.animation.TimeInterpolator;
|
|
import android.content.Context;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.graphics.Region;
|
|
import android.os.UserHandle;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.animation.Interpolator;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.UiThread;
|
|
|
|
import com.android.launcher3.DeviceProfile;
|
|
import com.android.launcher3.Launcher;
|
|
import com.android.launcher3.LauncherAppState;
|
|
import com.android.launcher3.LauncherInitListenerEx;
|
|
import com.android.launcher3.LauncherState;
|
|
import com.android.launcher3.allapps.DiscoveryBounce;
|
|
import com.android.launcher3.anim.AnimatorPlaybackController;
|
|
import com.android.launcher3.anim.SpringObjectAnimator;
|
|
import com.android.launcher3.compat.AccessibilityManagerCompat;
|
|
import com.android.launcher3.userevent.nano.LauncherLogProto;
|
|
import com.android.launcher3.views.FloatingIconView;
|
|
import com.android.quickstep.SysUINavigationMode.Mode;
|
|
import com.android.quickstep.util.LayoutUtils;
|
|
import com.android.quickstep.util.StaggeredWorkspaceAnim;
|
|
import com.android.quickstep.views.RecentsView;
|
|
import com.android.quickstep.views.TaskView;
|
|
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
|
|
|
|
import java.util.function.BiPredicate;
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* {@link ActivityControlHelper} for the in-launcher recents.
|
|
*/
|
|
public final class LauncherActivityControllerHelper implements ActivityControlHelper<Launcher> {
|
|
|
|
private Runnable mAdjustInterpolatorsRunnable;
|
|
|
|
@Override
|
|
public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
|
|
LayoutUtils.calculateLauncherTaskSize(context, dp, outRect);
|
|
if (dp.isVerticalBarLayout() && SysUINavigationMode.getMode(context) != Mode.NO_BUTTON) {
|
|
Rect targetInsets = dp.getInsets();
|
|
int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
|
|
return dp.hotseatBarSizePx + hotseatInset;
|
|
} else {
|
|
return LayoutUtils.getShelfTrackingDistance(context, dp);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTransitionCancelled(Launcher activity, boolean activityVisible) {
|
|
LauncherState startState = activity.getStateManager().getRestState();
|
|
activity.getStateManager().goToState(startState, activityVisible);
|
|
}
|
|
|
|
@Override
|
|
public void onSwipeUpToRecentsComplete(Launcher activity) {
|
|
// Re apply state in case we did something funky during the transition.
|
|
activity.getStateManager().reapplyState();
|
|
DiscoveryBounce.showForOverviewIfNeeded(activity);
|
|
}
|
|
|
|
@Override
|
|
public void onAssistantVisibilityChanged(float visibility) {
|
|
Launcher launcher = getCreatedActivity();
|
|
if (launcher != null) {
|
|
launcher.onAssistantVisibilityChanged(visibility);
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public HomeAnimationFactory prepareHomeUI(Launcher activity) {
|
|
final DeviceProfile dp = activity.getDeviceProfile();
|
|
final RecentsView recentsView = activity.getOverviewPanel();
|
|
final TaskView runningTaskView = recentsView.getRunningTaskView();
|
|
final View workspaceView;
|
|
if (runningTaskView != null && runningTaskView.getTask().key.getComponent() != null) {
|
|
workspaceView = activity.getWorkspace().getFirstMatchForAppClose(
|
|
runningTaskView.getTask().key.getComponent().getPackageName(),
|
|
UserHandle.of(runningTaskView.getTask().key.userId));
|
|
} else {
|
|
workspaceView = null;
|
|
}
|
|
final RectF iconLocation = new RectF();
|
|
boolean canUseWorkspaceView = workspaceView != null && workspaceView.isAttachedToWindow();
|
|
FloatingIconView floatingIconView = canUseWorkspaceView
|
|
? FloatingIconView.getFloatingIconView(activity, workspaceView,
|
|
true /* hideOriginal */, iconLocation, false /* isOpening */)
|
|
: null;
|
|
|
|
return new HomeAnimationFactory() {
|
|
@Nullable
|
|
@Override
|
|
public View getFloatingView() {
|
|
return floatingIconView;
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public RectF getWindowTargetRect() {
|
|
final int halfIconSize = dp.iconSizePx / 2;
|
|
final float targetCenterX = dp.availableWidthPx / 2f;
|
|
final float targetCenterY = dp.availableHeightPx - dp.hotseatBarSizePx;
|
|
|
|
if (canUseWorkspaceView) {
|
|
return iconLocation;
|
|
} else {
|
|
// Fallback to animate to center of screen.
|
|
return new RectF(targetCenterX - halfIconSize, targetCenterY - halfIconSize,
|
|
targetCenterX + halfIconSize, targetCenterY + halfIconSize);
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public AnimatorPlaybackController createActivityAnimationToHome() {
|
|
// Return an empty APC here since we have an non-user controlled animation to home.
|
|
long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
|
|
AnimatorSet as = new AnimatorSet();
|
|
as.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
activity.getStateManager().goToState(NORMAL, false);
|
|
}
|
|
});
|
|
return AnimatorPlaybackController.wrap(as, accuracy);
|
|
}
|
|
|
|
@Override
|
|
public void playAtomicAnimation(float velocity) {
|
|
new StaggeredWorkspaceAnim(activity, workspaceView, velocity).start();
|
|
}
|
|
};
|
|
}
|
|
|
|
@Override
|
|
public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible,
|
|
boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
|
|
final LauncherState startState = activity.getStateManager().getState();
|
|
|
|
LauncherState resetState = startState;
|
|
if (startState.disableRestore) {
|
|
resetState = activity.getStateManager().getRestState();
|
|
}
|
|
activity.getStateManager().setRestState(resetState);
|
|
|
|
final LauncherState fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
|
|
activity.getStateManager().goToState(fromState, false);
|
|
// Since all apps is not visible, we can safely reset the scroll position.
|
|
// This ensures then the next swipe up to all-apps starts from scroll 0.
|
|
activity.getAppsView().reset(false /* animate */);
|
|
|
|
// Optimization, hide the all apps view to prevent layout while initializing
|
|
activity.getAppsView().getContentView().setVisibility(View.GONE);
|
|
|
|
AccessibilityManagerCompat.sendStateEventToTest(activity, fromState.ordinal);
|
|
|
|
return new AnimationFactory() {
|
|
private Animator mShelfAnim;
|
|
private ShelfAnimState mShelfState;
|
|
private Animator mAttachToWindowAnim;
|
|
private boolean mIsAttachedToWindow;
|
|
|
|
@Override
|
|
public void createActivityController(long transitionLength) {
|
|
createActivityControllerInternal(activity, fromState, transitionLength, callback);
|
|
// Creating the activity controller animation sometimes reapplies the launcher state
|
|
// (because we set the animation as the current state animation), so we reapply the
|
|
// attached state here as well to ensure recents is shown/hidden appropriately.
|
|
if (SysUINavigationMode.getMode(activity) == Mode.NO_BUTTON) {
|
|
setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void adjustActivityControllerInterpolators() {
|
|
if (mAdjustInterpolatorsRunnable != null) {
|
|
mAdjustInterpolatorsRunnable.run();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTransitionCancelled() {
|
|
activity.getStateManager().goToState(startState, false /* animate */);
|
|
}
|
|
|
|
@Override
|
|
public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
|
|
long duration) {
|
|
if (mShelfState == shelfState) {
|
|
return;
|
|
}
|
|
mShelfState = shelfState;
|
|
if (mShelfAnim != null) {
|
|
mShelfAnim.cancel();
|
|
}
|
|
if (mShelfState == ShelfAnimState.CANCEL) {
|
|
return;
|
|
}
|
|
float shelfHiddenProgress = BACKGROUND_APP.getVerticalProgress(activity);
|
|
float shelfOverviewProgress = OVERVIEW.getVerticalProgress(activity);
|
|
float shelfPeekingProgress = shelfHiddenProgress
|
|
- (shelfHiddenProgress - shelfOverviewProgress) * 0.25f;
|
|
float toProgress = mShelfState == ShelfAnimState.HIDE
|
|
? shelfHiddenProgress
|
|
: mShelfState == ShelfAnimState.PEEK
|
|
? shelfPeekingProgress
|
|
: shelfOverviewProgress;
|
|
mShelfAnim = createShelfAnim(activity, toProgress);
|
|
mShelfAnim.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mShelfAnim = null;
|
|
}
|
|
});
|
|
mShelfAnim.setInterpolator(interpolator);
|
|
mShelfAnim.setDuration(duration);
|
|
mShelfAnim.start();
|
|
}
|
|
|
|
@Override
|
|
public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
|
|
if (mIsAttachedToWindow == attached && animate) {
|
|
return;
|
|
}
|
|
mIsAttachedToWindow = attached;
|
|
if (mAttachToWindowAnim != null) {
|
|
mAttachToWindowAnim.cancel();
|
|
}
|
|
mAttachToWindowAnim = ObjectAnimator.ofFloat(activity.getOverviewPanel(),
|
|
RecentsView.CONTENT_ALPHA, attached ? 1 : 0);
|
|
mAttachToWindowAnim.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mAttachToWindowAnim = null;
|
|
}
|
|
});
|
|
mAttachToWindowAnim.setInterpolator(ACCEL_DEACCEL);
|
|
mAttachToWindowAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0);
|
|
mAttachToWindowAnim.start();
|
|
}
|
|
};
|
|
}
|
|
|
|
private void createActivityControllerInternal(Launcher activity, LauncherState fromState,
|
|
long transitionLength, Consumer<AnimatorPlaybackController> callback) {
|
|
LauncherState endState = OVERVIEW;
|
|
if (fromState == endState) {
|
|
return;
|
|
}
|
|
|
|
AnimatorSet anim = new AnimatorSet();
|
|
if (!activity.getDeviceProfile().isVerticalBarLayout()
|
|
&& SysUINavigationMode.getMode(activity) != Mode.NO_BUTTON) {
|
|
// Don't animate the shelf when the mode is NO_BUTTON, because we update it atomically.
|
|
Animator shiftAnim = createShelfAnim(activity,
|
|
fromState.getVerticalProgress(activity),
|
|
endState.getVerticalProgress(activity));
|
|
anim.play(shiftAnim);
|
|
}
|
|
playScaleDownAnim(anim, activity, fromState, endState);
|
|
|
|
anim.setDuration(transitionLength * 2);
|
|
anim.setInterpolator(LINEAR);
|
|
AnimatorPlaybackController controller =
|
|
AnimatorPlaybackController.wrap(anim, transitionLength * 2);
|
|
activity.getStateManager().setCurrentUserControlledAnimation(controller);
|
|
|
|
// Since we are changing the start position of the UI, reapply the state, at the end
|
|
controller.setEndAction(() -> {
|
|
activity.getStateManager().goToState(
|
|
controller.getInterpolatedProgress() > 0.5 ? endState : fromState, false);
|
|
});
|
|
callback.accept(controller);
|
|
}
|
|
|
|
private Animator createShelfAnim(Launcher activity, float ... progressValues) {
|
|
Animator shiftAnim = new SpringObjectAnimator<>(activity.getAllAppsController(),
|
|
"allAppsSpringFromACH", activity.getAllAppsController().getShiftRange(),
|
|
SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
|
|
return shiftAnim;
|
|
}
|
|
|
|
/**
|
|
* Scale down recents from the center task being full screen to being in overview.
|
|
*/
|
|
private void playScaleDownAnim(AnimatorSet anim, Launcher launcher, LauncherState fromState,
|
|
LauncherState endState) {
|
|
RecentsView recentsView = launcher.getOverviewPanel();
|
|
TaskView v = recentsView.getTaskViewAt(recentsView.getCurrentPage());
|
|
if (v == null) {
|
|
return;
|
|
}
|
|
|
|
LauncherState.ScaleAndTranslation fromScaleAndTranslation
|
|
= fromState.getOverviewScaleAndTranslation(launcher);
|
|
LauncherState.ScaleAndTranslation endScaleAndTranslation
|
|
= endState.getOverviewScaleAndTranslation(launcher);
|
|
float fromTranslationY = fromScaleAndTranslation.translationY;
|
|
float endTranslationY = endScaleAndTranslation.translationY;
|
|
float fromFullscreenProgress = fromState.getOverviewFullscreenProgress();
|
|
float endFullscreenProgress = endState.getOverviewFullscreenProgress();
|
|
|
|
Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
|
|
fromScaleAndTranslation.scale, endScaleAndTranslation.scale);
|
|
Animator translateY = ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y,
|
|
fromTranslationY, endTranslationY);
|
|
Animator applyFullscreenProgress = ObjectAnimator.ofFloat(recentsView,
|
|
RecentsView.FULLSCREEN_PROGRESS, fromFullscreenProgress, endFullscreenProgress);
|
|
anim.playTogether(scale, translateY, applyFullscreenProgress);
|
|
|
|
mAdjustInterpolatorsRunnable = () -> {
|
|
// Adjust the translateY interpolator to account for the running task's top inset.
|
|
// When progress <= 1, this is handled by each task view as they set their fullscreen
|
|
// progress. However, once we go to progress > 1, fullscreen progress stays at 0, so
|
|
// recents as a whole needs to translate further to keep up with the app window.
|
|
TaskView runningTaskView = recentsView.getRunningTaskView();
|
|
if (runningTaskView == null) {
|
|
runningTaskView = recentsView.getTaskViewAt(recentsView.getCurrentPage());
|
|
}
|
|
TimeInterpolator oldInterpolator = translateY.getInterpolator();
|
|
Rect fallbackInsets = launcher.getDeviceProfile().getInsets();
|
|
float extraTranslationY = runningTaskView.getThumbnail().getInsets(fallbackInsets).top;
|
|
float normalizedTranslationY = extraTranslationY / (fromTranslationY - endTranslationY);
|
|
translateY.setInterpolator(t -> {
|
|
float newT = oldInterpolator.getInterpolation(t);
|
|
return newT <= 1f ? newT : newT + normalizedTranslationY * (newT - 1);
|
|
});
|
|
};
|
|
}
|
|
|
|
@Override
|
|
public ActivityInitListener createActivityInitListener(
|
|
BiPredicate<Launcher, Boolean> onInitListener) {
|
|
return new LauncherInitListenerEx(onInitListener);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public Launcher getCreatedActivity() {
|
|
LauncherAppState app = LauncherAppState.getInstanceNoCreate();
|
|
if (app == null) {
|
|
return null;
|
|
}
|
|
return (Launcher) app.getModel().getCallback();
|
|
}
|
|
|
|
@Nullable
|
|
@UiThread
|
|
private Launcher getVisibleLauncher() {
|
|
Launcher launcher = getCreatedActivity();
|
|
return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ?
|
|
launcher : null;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public RecentsView getVisibleRecentsView() {
|
|
Launcher launcher = getVisibleLauncher();
|
|
return launcher != null && launcher.getStateManager().getState().overviewUi
|
|
? launcher.getOverviewPanel() : null;
|
|
}
|
|
|
|
@Override
|
|
public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
|
|
Launcher launcher = getVisibleLauncher();
|
|
if (launcher == null) {
|
|
return false;
|
|
}
|
|
|
|
launcher.getUserEventDispatcher().logActionCommand(
|
|
LauncherLogProto.Action.Command.RECENTS_BUTTON,
|
|
getContainerType(),
|
|
LauncherLogProto.ContainerType.TASKSWITCHER);
|
|
launcher.getStateManager().goToState(OVERVIEW,
|
|
launcher.getStateManager().shouldAnimateStateChange(), onCompleteCallback);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean deferStartingActivity(Region activeNavBarRegion, MotionEvent ev) {
|
|
return activeNavBarRegion.contains((int) ev.getX(), (int) ev.getY());
|
|
}
|
|
|
|
@Override
|
|
public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
|
|
return homeBounds;
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldMinimizeSplitScreen() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public int getContainerType() {
|
|
final Launcher launcher = getVisibleLauncher();
|
|
return launcher != null ? launcher.getStateManager().getState().containerType
|
|
: LauncherLogProto.ContainerType.APP;
|
|
}
|
|
|
|
@Override
|
|
public boolean isInLiveTileMode() {
|
|
Launcher launcher = getCreatedActivity();
|
|
return launcher != null && launcher.getStateManager().getState() == OVERVIEW &&
|
|
launcher.isStarted();
|
|
}
|
|
} |