Instead of calculating an overall distance for tasks to translate based on RecentsView width, calculate the distance for the tasks to the left and right of the midpoint based on how far the first adjacent tasks in those directions are from being offscreen. Changes made to make "distance to offscreen" calculations possible: - Update TaskView curve scale to reach final scale as soon as it is completely offscreen. Before, it would reach its final scale just shy of that point (calculations were off). - As we update RecentsView scale, calculate how much the new scale will push out tasks that are just offscreen. - With both above, we can calculate the scale and position of a TaskView such that it is just offscreen, and interpolate between its current position and that position. Tests: - Task comes in immediately when quick switching from home, and doesn't shift as you swipe directly upwards. - When swiping far up from an app, tasks come in from all the way offscreen, and cover distance appropriately (e.g. if you're scrolled a bit to the right when you pause, the left adjacent app will move faster to cover the farther distance). - Task modalness: entering Select mode now animates adjacent tasks at the same rate as the scaling up, because they move only the distance needed to get offscreen (before they moved way too far and thus seemed to be much faster than the rest of the animation). Bug: 149934536 Change-Id: Ie3fffe0e5c304cb16e7637f058f5ce72cee40aeb Merged-In: Ie3fffe0e5c304cb16e7637f058f5ce72cee40aeb
413 lines
17 KiB
Java
413 lines
17 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.quickstep;
|
|
|
|
import static com.android.launcher3.anim.Interpolators.ACCEL_2;
|
|
import static com.android.launcher3.anim.Interpolators.INSTANT;
|
|
import static com.android.launcher3.anim.Interpolators.LINEAR;
|
|
import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
|
|
import static com.android.quickstep.BaseSwipeUpHandlerV2.RECENTS_ATTACH_DURATION;
|
|
import static com.android.quickstep.SysUINavigationMode.getMode;
|
|
import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
|
|
import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
|
|
import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM;
|
|
import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
|
|
import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
|
|
import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
|
|
import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
|
|
|
|
import android.animation.Animator;
|
|
import android.annotation.TargetApi;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Rect;
|
|
import android.os.Build;
|
|
import android.view.MotionEvent;
|
|
import android.view.animation.Interpolator;
|
|
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.UiThread;
|
|
|
|
import com.android.launcher3.DeviceProfile;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.anim.AnimatorPlaybackController;
|
|
import com.android.launcher3.anim.PendingAnimation;
|
|
import com.android.launcher3.statehandlers.DepthController;
|
|
import com.android.launcher3.statemanager.BaseState;
|
|
import com.android.launcher3.statemanager.StatefulActivity;
|
|
import com.android.launcher3.touch.PagedOrientationHandler;
|
|
import com.android.launcher3.util.WindowBounds;
|
|
import com.android.quickstep.SysUINavigationMode.Mode;
|
|
import com.android.quickstep.util.ActivityInitListener;
|
|
import com.android.quickstep.util.ShelfPeekAnim;
|
|
import com.android.quickstep.util.SplitScreenBounds;
|
|
import com.android.quickstep.views.RecentsView;
|
|
import com.android.systemui.shared.recents.model.ThumbnailData;
|
|
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
|
|
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Predicate;
|
|
|
|
/**
|
|
* Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
|
|
*/
|
|
@TargetApi(Build.VERSION_CODES.P)
|
|
public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
|
|
ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>> {
|
|
|
|
public final boolean rotationSupportedByActivity;
|
|
|
|
private final STATE_TYPE mOverviewState, mBackgroundState;
|
|
|
|
protected BaseActivityInterface(boolean rotationSupportedByActivity,
|
|
STATE_TYPE overviewState, STATE_TYPE backgroundState) {
|
|
this.rotationSupportedByActivity = rotationSupportedByActivity;
|
|
mOverviewState = overviewState;
|
|
mBackgroundState = backgroundState;
|
|
}
|
|
|
|
public void onTransitionCancelled(boolean activityVisible) {
|
|
ACTIVITY_TYPE activity = getCreatedActivity();
|
|
if (activity == null) {
|
|
return;
|
|
}
|
|
STATE_TYPE startState = activity.getStateManager().getRestState();
|
|
activity.getStateManager().goToState(startState, activityVisible);
|
|
}
|
|
|
|
public abstract int getSwipeUpDestinationAndLength(
|
|
DeviceProfile dp, Context context, Rect outRect,
|
|
PagedOrientationHandler orientationHandler);
|
|
|
|
public void onSwipeUpToRecentsComplete() {
|
|
// Re apply state in case we did something funky during the transition.
|
|
ACTIVITY_TYPE activity = getCreatedActivity();
|
|
if (activity == null) {
|
|
return;
|
|
}
|
|
activity.getStateManager().reapplyState();
|
|
}
|
|
|
|
public abstract void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState);
|
|
|
|
public abstract void onAssistantVisibilityChanged(float visibility);
|
|
|
|
public abstract AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
|
|
boolean activityVisible, Consumer<AnimatorPlaybackController> callback);
|
|
|
|
public abstract ActivityInitListener createActivityInitListener(
|
|
Predicate<Boolean> onInitListener);
|
|
|
|
/**
|
|
* Sets a callback to be run when an activity launch happens while launcher is not yet resumed.
|
|
*/
|
|
public void setOnDeferredActivityLaunchCallback(Runnable r) {}
|
|
|
|
@Nullable
|
|
public abstract ACTIVITY_TYPE getCreatedActivity();
|
|
|
|
@Nullable
|
|
public DepthController getDepthController() {
|
|
return null;
|
|
}
|
|
|
|
public final boolean isResumed() {
|
|
ACTIVITY_TYPE activity = getCreatedActivity();
|
|
return activity != null && activity.hasBeenResumed();
|
|
}
|
|
|
|
public final boolean isStarted() {
|
|
ACTIVITY_TYPE activity = getCreatedActivity();
|
|
return activity != null && activity.isStarted();
|
|
}
|
|
|
|
@UiThread
|
|
@Nullable
|
|
public abstract <T extends RecentsView> T getVisibleRecentsView();
|
|
|
|
@UiThread
|
|
public abstract boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
|
|
|
|
public abstract Rect getOverviewWindowBounds(
|
|
Rect homeBounds, RemoteAnimationTargetCompat target);
|
|
|
|
public abstract boolean allowMinimizeSplitScreen();
|
|
|
|
public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
|
|
return deviceState.isInDeferredGestureRegion(ev);
|
|
}
|
|
|
|
public abstract void onExitOverview(RotationTouchHelper deviceState,
|
|
Runnable exitRunnable);
|
|
|
|
/**
|
|
* Updates the prediction state to the overview state.
|
|
*/
|
|
public void updateOverviewPredictionState() {
|
|
// By public overview predictions are not supported
|
|
}
|
|
|
|
/**
|
|
* Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
|
|
*/
|
|
public abstract int getContainerType();
|
|
|
|
public abstract boolean isInLiveTileMode();
|
|
|
|
public abstract void onLaunchTaskFailed();
|
|
|
|
public void onLaunchTaskSuccess() {
|
|
ACTIVITY_TYPE activity = getCreatedActivity();
|
|
if (activity == null) {
|
|
return;
|
|
}
|
|
activity.getStateManager().moveToRestState();
|
|
}
|
|
|
|
public void closeOverlay() { }
|
|
|
|
public void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData, Runnable runnable) {
|
|
ACTIVITY_TYPE activity = getCreatedActivity();
|
|
if (activity == null) {
|
|
return;
|
|
}
|
|
RecentsView recentsView = activity.getOverviewPanel();
|
|
if (recentsView == null) {
|
|
if (runnable != null) {
|
|
runnable.run();
|
|
}
|
|
return;
|
|
}
|
|
recentsView.switchToScreenshot(thumbnailData, runnable);
|
|
}
|
|
|
|
/**
|
|
* Calculates the taskView size for the provided device configuration
|
|
*/
|
|
public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect,
|
|
PagedOrientationHandler orientedState) {
|
|
calculateTaskSize(context, dp, getExtraSpace(context, dp, orientedState),
|
|
outRect, orientedState);
|
|
}
|
|
|
|
protected abstract float getExtraSpace(Context context, DeviceProfile dp,
|
|
PagedOrientationHandler orientedState);
|
|
|
|
private void calculateTaskSize(
|
|
Context context, DeviceProfile dp, float extraVerticalSpace, Rect outRect,
|
|
PagedOrientationHandler orientationHandler) {
|
|
Resources res = context.getResources();
|
|
final boolean showLargeTaskSize = showOverviewActions(context) ||
|
|
hideShelfInTwoButtonLandscape(context, orientationHandler);
|
|
|
|
final int paddingResId;
|
|
if (dp.isMultiWindowMode) {
|
|
paddingResId = R.dimen.multi_window_task_card_horz_space;
|
|
} else if (dp.isVerticalBarLayout()) {
|
|
paddingResId = R.dimen.landscape_task_card_horz_space;
|
|
} else if (showLargeTaskSize) {
|
|
paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
|
|
} else {
|
|
paddingResId = R.dimen.portrait_task_card_horz_space;
|
|
}
|
|
float paddingHorz = res.getDimension(paddingResId);
|
|
float paddingVert = showLargeTaskSize
|
|
? 0 : res.getDimension(R.dimen.task_card_vert_space);
|
|
|
|
calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
|
|
res.getDimension(R.dimen.task_thumbnail_top_margin), outRect);
|
|
}
|
|
|
|
private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
|
|
float extraVerticalSpace, float paddingHorz, float paddingVert, float topIconMargin,
|
|
Rect outRect) {
|
|
float taskWidth, taskHeight;
|
|
Rect insets = dp.getInsets();
|
|
if (dp.isMultiWindowMode) {
|
|
WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(context);
|
|
taskWidth = bounds.availableSize.x;
|
|
taskHeight = bounds.availableSize.y;
|
|
} else {
|
|
taskWidth = dp.availableWidthPx;
|
|
taskHeight = dp.availableHeightPx;
|
|
}
|
|
|
|
// Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless
|
|
// we override the insets ourselves.
|
|
int launcherVisibleWidth = dp.widthPx - insets.left - insets.right;
|
|
int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
|
|
|
|
float availableHeight = launcherVisibleHeight
|
|
- topIconMargin - extraVerticalSpace - paddingVert;
|
|
float availableWidth = launcherVisibleWidth - paddingHorz;
|
|
|
|
float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
|
|
float outWidth = scale * taskWidth;
|
|
float outHeight = scale * taskHeight;
|
|
|
|
// Center in the visible space
|
|
float x = insets.left + (launcherVisibleWidth - outWidth) / 2;
|
|
float y = insets.top + Math.max(topIconMargin,
|
|
(launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
|
|
outRect.set(Math.round(x), Math.round(y),
|
|
Math.round(x) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
|
|
}
|
|
|
|
/**
|
|
* Calculates the modal taskView size for the provided device configuration
|
|
*/
|
|
public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect) {
|
|
float paddingHorz = context.getResources().getDimension(dp.isMultiWindowMode
|
|
? R.dimen.multi_window_task_card_horz_space
|
|
: dp.isVerticalBarLayout()
|
|
? R.dimen.landscape_task_card_horz_space
|
|
: R.dimen.portrait_modal_task_card_horz_space);
|
|
float extraVerticalSpace = getOverviewActionsHeight(context);
|
|
float paddingVert = 0;
|
|
float topIconMargin = 0;
|
|
calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
|
|
topIconMargin, outRect);
|
|
}
|
|
|
|
/** Gets the space that the overview actions will take, including margins. */
|
|
public final float getOverviewActionsHeight(Context context) {
|
|
Resources res = context.getResources();
|
|
float actionsBottomMargin = 0;
|
|
if (getMode(context) == Mode.THREE_BUTTONS) {
|
|
actionsBottomMargin = res.getDimensionPixelSize(
|
|
R.dimen.overview_actions_bottom_margin_three_button);
|
|
} else {
|
|
actionsBottomMargin = res.getDimensionPixelSize(
|
|
R.dimen.overview_actions_bottom_margin_gesture);
|
|
}
|
|
float overviewActionsHeight = actionsBottomMargin
|
|
+ res.getDimensionPixelSize(R.dimen.overview_actions_height);
|
|
return overviewActionsHeight;
|
|
}
|
|
|
|
public interface AnimationFactory {
|
|
|
|
void createActivityInterface(long transitionLength);
|
|
|
|
default void onTransitionCancelled() { }
|
|
|
|
default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
|
|
Interpolator interpolator, long duration) { }
|
|
|
|
/**
|
|
* @param attached Whether to show RecentsView alongside the app window. If false, recents
|
|
* will be hidden by some property we can animate, e.g. alpha.
|
|
* @param animate Whether to animate recents to/from its new attached state.
|
|
*/
|
|
default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
|
|
}
|
|
|
|
class DefaultAnimationFactory implements AnimationFactory {
|
|
|
|
protected final ACTIVITY_TYPE mActivity;
|
|
private final STATE_TYPE mStartState;
|
|
private final Consumer<AnimatorPlaybackController> mCallback;
|
|
|
|
private boolean mIsAttachedToWindow;
|
|
|
|
DefaultAnimationFactory(Consumer<AnimatorPlaybackController> callback) {
|
|
mCallback = callback;
|
|
|
|
mActivity = getCreatedActivity();
|
|
mStartState = mActivity.getStateManager().getState();
|
|
}
|
|
|
|
protected ACTIVITY_TYPE initUI() {
|
|
STATE_TYPE resetState = mStartState;
|
|
if (mStartState.shouldDisableRestore()) {
|
|
resetState = mActivity.getStateManager().getRestState();
|
|
}
|
|
mActivity.getStateManager().setRestState(resetState);
|
|
mActivity.getStateManager().goToState(mBackgroundState, false);
|
|
return mActivity;
|
|
}
|
|
|
|
@Override
|
|
public void createActivityInterface(long transitionLength) {
|
|
PendingAnimation pa = new PendingAnimation(transitionLength * 2);
|
|
createBackgroundToOverviewAnim(mActivity, pa);
|
|
AnimatorPlaybackController controller = pa.createPlaybackController();
|
|
mActivity.getStateManager().setCurrentUserControlledAnimation(controller);
|
|
|
|
// Since we are changing the start position of the UI, reapply the state, at the end
|
|
controller.setEndAction(() -> mActivity.getStateManager().goToState(
|
|
controller.getInterpolatedProgress() > 0.5 ? mOverviewState : mBackgroundState,
|
|
false));
|
|
mCallback.accept(controller);
|
|
|
|
// 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(mActivity) == Mode.NO_BUTTON) {
|
|
setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTransitionCancelled() {
|
|
mActivity.getStateManager().goToState(mStartState, false /* animate */);
|
|
}
|
|
|
|
@Override
|
|
public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
|
|
if (mIsAttachedToWindow == attached && animate) {
|
|
return;
|
|
}
|
|
mIsAttachedToWindow = attached;
|
|
RecentsView recentsView = mActivity.getOverviewPanel();
|
|
Animator fadeAnim = mActivity.getStateManager()
|
|
.createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
|
|
|
|
float fromTranslation = attached ? 1 : 0;
|
|
float toTranslation = attached ? 0 : 1;
|
|
mActivity.getStateManager()
|
|
.cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
|
|
if (!recentsView.isShown() && animate) {
|
|
ADJACENT_PAGE_OFFSET.set(recentsView, fromTranslation);
|
|
} else {
|
|
fromTranslation = ADJACENT_PAGE_OFFSET.get(recentsView);
|
|
}
|
|
if (!animate) {
|
|
ADJACENT_PAGE_OFFSET.set(recentsView, toTranslation);
|
|
} else {
|
|
mActivity.getStateManager().createStateElementAnimation(
|
|
INDEX_RECENTS_TRANSLATE_X_ANIM,
|
|
fromTranslation, toTranslation).start();
|
|
}
|
|
|
|
fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
|
|
fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
|
|
}
|
|
|
|
protected void createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa) {
|
|
// Scale down recents from being full screen to being in overview.
|
|
RecentsView recentsView = activity.getOverviewPanel();
|
|
pa.addFloat(recentsView, RECENTS_SCALE_PROPERTY,
|
|
recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
|
|
pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
|
|
}
|
|
}
|
|
|
|
protected static boolean showOverviewActions(Context context) {
|
|
return ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context);
|
|
}
|
|
}
|