blob: 0e8dea60eadfdb4b59ee4a61c0323677fe36d7ec [file] [log] [blame]
/*
* 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.LauncherState.OVERVIEW;
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.quickstep.QuickScrubController.QUICK_SWITCH_START_DURATION;
import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB;
import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SWITCH;
import static com.android.quickstep.TouchConsumer.isInteractionQuick;
import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.RectEvaluator;
import android.annotation.TargetApi;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver.OnDrawListener;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherState;
import com.android.launcher3.MainThreadExecutor;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
import com.android.launcher3.util.TraceHelper;
import com.android.quickstep.TouchConsumer.InteractionType;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.InputConsumerController;
import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import com.android.systemui.shared.system.TransactionCompat;
import com.android.systemui.shared.system.WindowManagerWrapper;
import java.util.StringJoiner;
@TargetApi(Build.VERSION_CODES.O)
public class WindowTransformSwipeHandler extends BaseSwipeInteractionHandler {
private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
private static final boolean DEBUG_STATES = false;
// Launcher UI related states
private static final int STATE_LAUNCHER_PRESENT = 1 << 0;
private static final int STATE_LAUNCHER_DRAWN = 1 << 1;
private static final int STATE_ACTIVITY_MULTIPLIER_COMPLETE = 1 << 2;
// Internal initialization states
private static final int STATE_APP_CONTROLLER_RECEIVED = 1 << 3;
// Interaction finish states
private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 4;
private static final int STATE_SCALED_CONTROLLER_APP = 1 << 5;
private static final int STATE_HANDLER_INVALIDATED = 1 << 6;
private static final int STATE_GESTURE_STARTED = 1 << 7;
// States for quick switch/scrub
private static final int STATE_SWITCH_TO_SCREENSHOT_COMPLETE = 1 << 8;
private static final int STATE_QUICK_SWITCH = 1 << 9;
private static final int STATE_QUICK_SCRUB_START = 1 << 10;
private static final int STATE_QUICK_SCRUB_END = 1 << 11;
private static final int LAUNCHER_UI_STATES =
STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE;
// For debugging, keep in sync with above states
private static final String[] STATES = new String[] {
"STATE_LAUNCHER_PRESENT",
"STATE_LAUNCHER_DRAWN",
"STATE_ACTIVITY_MULTIPLIER_COMPLETE",
"STATE_APP_CONTROLLER_RECEIVED",
"STATE_SCALED_CONTROLLER_RECENTS",
"STATE_SCALED_CONTROLLER_APP",
"STATE_HANDLER_INVALIDATED",
"STATE_GESTURE_STARTED",
"STATE_SWITCH_TO_SCREENSHOT_COMPLETE",
"STATE_QUICK_SWITCH",
"STATE_QUICK_SCRUB_START",
"STATE_QUICK_SCRUB_END"
};
private static final long MAX_SWIPE_DURATION = 200;
private static final long MIN_SWIPE_DURATION = 80;
private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
// The bounds of the source app in device coordinates
private final Rect mSourceStackBounds = new Rect();
// The insets of the source app
private final Rect mSourceInsets = new Rect();
// The source app bounds with the source insets applied, in the source app window coordinates
private final Rect mSourceRect = new Rect();
// The insets to be used for clipping the app window, which can be larger than mSourceInsets
// if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
// app window coordinates.
private final Rect mSourceWindowClipInsets = new Rect();
// The bounds of launcher (not including insets) in device coordinates
private final Rect mHomeStackBounds = new Rect();
// The bounds of the task view in launcher window coordinates
private final Rect mTargetRect = new Rect();
// Doesn't change after initialized, used as an anchor when changing mTargetRect
private final Rect mInitialTargetRect = new Rect();
// The interpolated rect from the source app rect to the target rect
private final Rect mCurrentRect = new Rect();
// The clip rect in source app window coordinates
private final Rect mClipRect = new Rect();
private final RectEvaluator mRectEvaluator = new RectEvaluator(mCurrentRect);
private DeviceProfile mDp;
private int mTransitionDragLength;
// Shift in the range of [0, 1].
// 0 => preview snapShot is completely visible, and hotseat is completely translated down
// 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
// visible.
private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
private final MainThreadExecutor mMainExecutor = new MainThreadExecutor();
private final Context mContext;
private final int mRunningTaskId;
private MultiStateCallback mStateCallback;
private AnimatorPlaybackController mLauncherTransitionController;
private Launcher mLauncher;
private LauncherLayoutListener mLauncherLayoutListener;
private RecentsView mRecentsView;
private QuickScrubController mQuickScrubController;
private Runnable mLauncherDrawnCallback;
private boolean mWasLauncherAlreadyVisible;
private float mCurrentDisplacement;
private boolean mGestureStarted;
private @InteractionType int mInteractionType = INTERACTION_NORMAL;
private InputConsumerController mInputConsumer =
InputConsumerController.getRecentsAnimationInputConsumer();
private final RecentsAnimationWrapper mRecentsAnimationWrapper = new RecentsAnimationWrapper();
private Matrix mTmpMatrix = new Matrix();
WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context) {
mContext = context;
mRunningTaskId = runningTaskInfo.id;
mInputConsumer.registerInputConsumer();
initStateCallbacks();
}
private void initStateCallbacks() {
mStateCallback = new MultiStateCallback() {
@Override
public void setState(int stateFlag) {
debugNewState(stateFlag);
super.setState(stateFlag);
}
};
mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
this::initializeLauncherAnimationController);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
this::launcherFrameDrawn);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
this::notifyGestureStarted);
mStateCallback.addCallback(STATE_SCALED_CONTROLLER_APP | STATE_APP_CONTROLLER_RECEIVED,
this::resumeLastTask);
mStateCallback.addCallback(STATE_SCALED_CONTROLLER_RECENTS
| STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_APP_CONTROLLER_RECEIVED,
this::switchToScreenshot);
mStateCallback.addCallback(STATE_SCALED_CONTROLLER_RECENTS
| STATE_ACTIVITY_MULTIPLIER_COMPLETE,
this::setupLauncherUiAfterSwipeUpAnimation);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP,
this::reset);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_RECENTS,
this::reset);
mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
this::invalidateHandlerWithLauncher);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SWITCH,
this::onQuickInteractionStart);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SCRUB_START,
this::onQuickInteractionStart);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
| STATE_QUICK_SWITCH, this::switchToFinalAppAfterQuickSwitch);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
| STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub);
}
private void setStateOnUiThread(int stateFlag) {
Handler handler = mMainExecutor.getHandler();
if (Looper.myLooper() == handler.getLooper()) {
mStateCallback.setState(stateFlag);
} else {
postAtFrontOfQueueAsynchronously(handler, () -> mStateCallback.setState(stateFlag));
}
}
private void initTransitionEndpoints(DeviceProfile dp) {
mDp = dp;
mSourceRect.set(0, 0, dp.widthPx - mSourceInsets.left - mSourceInsets.right,
dp.heightPx - mSourceInsets.top - mSourceInsets.bottom);
RecentsView.getPageRect(dp, mContext, mTargetRect);
mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
mHomeStackBounds.top - mSourceStackBounds.top);
mInitialTargetRect.set(mTargetRect);
// Calculate the clip based on the target rect (since the content insets and the
// launcher insets may differ, so the aspect ratio of the target rect can differ
// from the source rect. The difference between the target rect (scaled to the
// source rect) is the amount to clip on each edge.
Rect scaledTargetRect = new Rect(mTargetRect);
Utilities.scaleRectAboutCenter(scaledTargetRect,
(float) mSourceRect.width() / mTargetRect.width());
scaledTargetRect.offsetTo(mSourceInsets.left, mSourceInsets.top);
mSourceWindowClipInsets.set(scaledTargetRect.left, scaledTargetRect.top,
mDp.widthPx - scaledTargetRect.right,
mDp.heightPx - scaledTargetRect.bottom);
Rect targetInsets = dp.getInsets();
mTransitionDragLength = dp.hotseatBarSizePx;
if (dp.isVerticalBarLayout()) {
int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
mTransitionDragLength += dp.hotseatBarSidePaddingPx + hotseatInset;
} else {
mTransitionDragLength += targetInsets.bottom;
}
}
private long getFadeInDuration() {
if (mCurrentShift.getCurrentAnimation() != null) {
ObjectAnimator anim = mCurrentShift.getCurrentAnimation();
long theirDuration = anim.getDuration() - anim.getCurrentPlayTime();
// TODO: Find a better heuristic
return Math.min(MAX_SWIPE_DURATION, Math.max(theirDuration, MIN_SWIPE_DURATION));
} else {
return MAX_SWIPE_DURATION;
}
}
@Override
protected boolean init(final Launcher launcher, boolean alreadyOnHome) {
if (launcher == mLauncher) {
return true;
}
if (mLauncher != null) {
// The launcher may have been recreated as a result of device rotation.
int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES;
initStateCallbacks();
mStateCallback.setState(oldState);
mLauncherLayoutListener.setHandler(null);
}
mWasLauncherAlreadyVisible = alreadyOnHome;
mLauncher = launcher;
// For the duration of the gesture, lock the screen orientation to ensure that we do not
// rotate mid-quickscrub
mLauncher.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
LauncherState startState = mLauncher.getStateManager().getState();
if (startState.disableRestore) {
startState = mLauncher.getStateManager().getRestState();
}
mLauncher.getStateManager().setRestState(startState);
AbstractFloatingView.closeAllOpenViews(launcher, alreadyOnHome);
mRecentsView = mLauncher.getOverviewPanel();
mQuickScrubController = mRecentsView.getQuickScrubController();
mLauncherLayoutListener = new LauncherLayoutListener(mLauncher);
final int state;
if (mWasLauncherAlreadyVisible) {
DeviceProfile dp = mLauncher.getDeviceProfile();
long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
mLauncherTransitionController = launcher.getStateManager()
.createAnimationToNewWorkspace(OVERVIEW, accuracy);
mLauncherTransitionController.dispatchOnStart();
mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
state = STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_DRAWN
| STATE_LAUNCHER_PRESENT;
} else {
TraceHelper.beginSection("WTS-init");
launcher.getStateManager().goToState(OVERVIEW, false);
TraceHelper.partitionSection("WTS-init", "State changed");
// TODO: Implement a better animation for fading in
View rootView = launcher.getRootView();
rootView.setAlpha(0);
rootView.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
@Override
public void onDraw() {
TraceHelper.endSection("WTS-init", "Launcher frame is drawn");
rootView.post(() ->
rootView.getViewTreeObserver().removeOnDrawListener(this));
if (launcher != mLauncher) {
return;
}
mStateCallback.setState(STATE_LAUNCHER_DRAWN);
}
});
state = STATE_LAUNCHER_PRESENT;
// Optimization, hide the all apps view to prevent layout while initializing
mLauncher.getAppsView().setVisibility(View.GONE);
}
mRecentsView.showTask(mRunningTaskId);
mLauncherLayoutListener.open();
mStateCallback.setState(state);
return true;
}
public void setLauncherOnDrawCallback(Runnable callback) {
mLauncherDrawnCallback = callback;
}
private void launcherFrameDrawn() {
View rootView = mLauncher.getRootView();
if (rootView.getAlpha() < 1) {
if (mGestureStarted) {
final MultiStateCallback callback = mStateCallback;
rootView.animate().alpha(1)
.setDuration(getFadeInDuration())
.withEndAction(() -> callback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE));
} else {
rootView.setAlpha(1);
mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE);
}
}
if (mLauncherDrawnCallback != null) {
mLauncherDrawnCallback.run();
}
}
private void initializeLauncherAnimationController() {
mLauncherLayoutListener.setHandler(this);
onLauncherLayoutChanged();
}
public void updateInteractionType(@InteractionType int interactionType) {
if (mInteractionType != INTERACTION_NORMAL) {
throw new IllegalArgumentException(
"Can't change interaction type from " + mInteractionType);
}
if (!isInteractionQuick(interactionType)) {
throw new IllegalArgumentException(
"Can't change interaction type to " + interactionType);
}
mInteractionType = interactionType;
setStateOnUiThread(interactionType == INTERACTION_QUICK_SWITCH
? STATE_QUICK_SWITCH : STATE_QUICK_SCRUB_START);
// Start the window animation without waiting for launcher.
animateToProgress(1f, QUICK_SWITCH_START_DURATION);
}
private void onQuickInteractionStart() {
mQuickScrubController.onQuickScrubStart(false);
}
@WorkerThread
public void updateDisplacement(float displacement) {
mCurrentDisplacement = displacement;
float translation = Utilities.boundToRange(-mCurrentDisplacement, 0, mTransitionDragLength);
float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
mCurrentShift.updateValue(shift);
}
/**
* Called by {@link #mLauncherLayoutListener} when launcher layout changes
*/
public void onLauncherLayoutChanged() {
initTransitionEndpoints(mLauncher.getDeviceProfile());
if (!mWasLauncherAlreadyVisible) {
float startProgress;
AllAppsTransitionController controller = mLauncher.getAllAppsController();
if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
startProgress = 1;
} else {
float scrollRange = Math.max(controller.getShiftRange(), 1);
startProgress = (mTransitionDragLength / scrollRange) + 1;
}
AnimatorSet anim = new AnimatorSet();
ObjectAnimator shiftAnim = ObjectAnimator.ofFloat(controller, ALL_APPS_PROGRESS,
startProgress, OVERVIEW.getVerticalProgress(mLauncher));
shiftAnim.setInterpolator(LINEAR);
anim.play(shiftAnim);
// TODO: Link this animation to state animation, so that it is cancelled
// automatically on state change
anim.setDuration(mTransitionDragLength * 2);
mLauncherTransitionController =
AnimatorPlaybackController.wrap(anim, mTransitionDragLength * 2);
mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
}
}
@WorkerThread
private void updateFinalShift() {
float shift = mCurrentShift.value;
synchronized (mRecentsAnimationWrapper) {
if (mRecentsAnimationWrapper.controller != null) {
synchronized (mTargetRect) {
mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
}
float scale = (float) mCurrentRect.width() / mSourceRect.width();
mClipRect.left = (int) (mSourceWindowClipInsets.left * shift);
mClipRect.top = (int) (mSourceWindowClipInsets.top * shift);
mClipRect.right = (int) (mDp.widthPx - (mSourceWindowClipInsets.right * shift));
mClipRect.bottom = (int) (mDp.heightPx - (mSourceWindowClipInsets.bottom * shift));
mTmpMatrix.setScale(scale, scale, 0, 0);
mTmpMatrix.postTranslate(mCurrentRect.left - mSourceInsets.left * scale * shift,
mCurrentRect.top - mSourceInsets.top * scale * shift);
TransactionCompat transaction = new TransactionCompat();
for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
if (app.mode == MODE_CLOSING) {
transaction.setMatrix(app.leash, mTmpMatrix)
.setWindowCrop(app.leash, mClipRect)
.show(app.leash);
}
}
transaction.apply();
}
}
if (mLauncherTransitionController != null) {
Runnable runOnUi = () -> {
if (mLauncherTransitionController == null) {
return;
}
mLauncherTransitionController.setPlayFraction(shift);
// Make sure the window follows the first task if it moves, e.g. during quick scrub.
int firstTaskIndex = mRecentsView.getFirstTaskIndex();
View firstTask = mRecentsView.getPageAt(firstTaskIndex);
int scrollForFirstTask = mRecentsView.getScrollForPage(firstTaskIndex);
int offsetFromFirstTask = (scrollForFirstTask - mRecentsView.getScrollX());
if (offsetFromFirstTask != 0) {
synchronized (mTargetRect) {
mTargetRect.set(mInitialTargetRect);
Utilities.scaleRectAboutCenter(mTargetRect, firstTask.getScaleX());
int offsetX = (int) (offsetFromFirstTask + firstTask.getTranslationX());
mTargetRect.offset(offsetX, 0);
}
}
};
if (Looper.getMainLooper() == Looper.myLooper()) {
runOnUi.run();
} else {
// The fling operation completed even before the launcher was drawn
mMainExecutor.execute(runOnUi);
}
}
}
public void setRecentsAnimation(RecentsAnimationControllerCompat controller,
RemoteAnimationTargetCompat[] apps, Rect homeContentInsets, Rect minimizedHomeBounds) {
if (apps != null) {
// Use the top closing app to determine the insets for the animation
for (RemoteAnimationTargetCompat target : apps) {
if (target.mode == MODE_CLOSING) {
DeviceProfile dp = LauncherAppState.getIDP(mContext).getDeviceProfile(mContext);
if (minimizedHomeBounds != null) {
mHomeStackBounds.set(minimizedHomeBounds);
dp = dp.getMultiWindowProfile(mContext,
new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height()));
dp.updateInsets(homeContentInsets);
} else {
mHomeStackBounds.set(new Rect(0, 0, dp.widthPx, dp.heightPx));
// TODO: Workaround for an existing issue where the home content insets are
// not valid immediately after rotation, just use the stable insets for now
Rect insets = new Rect();
WindowManagerWrapper.getInstance().getStableInsets(insets);
dp.updateInsets(insets);
}
// Initialize the start and end animation bounds
// TODO: Remove once platform is updated
try {
mSourceInsets.set(target.getContentInsets());
} catch (Error e) {
// TODO: Remove once platform is updated, use stable insets as fallback
WindowManagerWrapper.getInstance().getStableInsets(mSourceInsets);
}
mSourceStackBounds.set(target.sourceContainerBounds);
initTransitionEndpoints(dp);
break;
}
}
}
mRecentsAnimationWrapper.setController(controller, apps);
setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
}
public void onGestureStarted() {
if (mLauncher != null) {
notifyGestureStarted();
}
setStateOnUiThread(STATE_GESTURE_STARTED);
mGestureStarted = true;
mRecentsAnimationWrapper.enableInputConsumer();
}
/**
* Notifies the launcher that the swipe gesture has started. This can be called multiple times
* on both background and UI threads
*/
private void notifyGestureStarted() {
mLauncher.onQuickstepGestureStarted(mWasLauncherAlreadyVisible);
mMainExecutor.execute(() -> {
// Prepare to animate the first icon.
View currentRecentsPage = mRecentsView.getPageAt(mRecentsView.getCurrentPage());
if (currentRecentsPage instanceof TaskView) {
((TaskView) currentRecentsPage).setIconScale(0f);
}
});
}
@WorkerThread
public void onGestureEnded(float endVelocity) {
Resources res = mContext.getResources();
float flingThreshold = res.getDimension(R.dimen.quickstep_fling_threshold_velocity);
boolean isFling = Math.abs(endVelocity) > flingThreshold;
long duration = MAX_SWIPE_DURATION;
final float endShift;
if (!isFling) {
endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? 1 : 0;
} else {
endShift = endVelocity < 0 ? 1 : 0;
float minFlingVelocity = res.getDimension(R.dimen.quickstep_fling_min_velocity);
if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
float distanceToTravel = (endShift - mCurrentShift.value) * mTransitionDragLength;
// we want the page's snap velocity to approximately match the velocity at
// which the user flings, so we scale the duration by a value near to the
// derivative of the scroll interpolator at zero, ie. 5.
duration = 5 * Math.round(1000 * Math.abs(distanceToTravel / endVelocity));
}
}
animateToProgress(endShift, duration);
int direction = Direction.UP;
if (mLauncher.getDeviceProfile().isLandscape) {
direction = Direction.LEFT;
if (mLauncher.getDeviceProfile().isSeascape()) {
direction = Direction.RIGHT;
}
}
int dstContainerType = LauncherLogProto.ContainerType.TASKSWITCHER;
if (Float.compare(endShift, 0) == 0) {
direction = Direction.DOWN;
if (mLauncher.getDeviceProfile().isLandscape) {
direction = Direction.RIGHT;
if (mLauncher.getDeviceProfile().isSeascape()) {
direction = Direction.LEFT;
}
}
dstContainerType = LauncherLogProto.ContainerType.APP;
}
mLauncher.getUserEventDispatcher().logStateChangeAction(
isFling ? Touch.FLING : Touch.SWIPE, direction,
LauncherLogProto.ContainerType.NAVBAR,
LauncherLogProto.ContainerType.APP,
dstContainerType,
0);
}
/** Animates to the given progress, where 0 is the current app and 1 is overview. */
private void animateToProgress(float progress, long duration) {
ObjectAnimator anim = mCurrentShift.animateToValue(progress).setDuration(duration);
anim.setInterpolator(Interpolators.SCROLL);
anim.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
setStateOnUiThread((Float.compare(mCurrentShift.value, 0) == 0)
? STATE_SCALED_CONTROLLER_APP : STATE_SCALED_CONTROLLER_RECENTS);
}
});
anim.start();
}
@UiThread
private void resumeLastTask() {
mRecentsAnimationWrapper.finish(false /* toHome */, null);
}
public void reset() {
if (mInteractionType != INTERACTION_QUICK_SCRUB) {
// Only invalidate the handler if we are not quick scrubbing, otherwise, it will be
// invalidated after the quick scrub ends
setStateOnUiThread(STATE_HANDLER_INVALIDATED);
}
}
private void invalidateHandler() {
mCurrentShift.cancelAnimation();
if (mGestureEndCallback != null) {
mGestureEndCallback.accept(this);
}
clearReference();
mInputConsumer.unregisterInputConsumer();
}
private void invalidateHandlerWithLauncher() {
mLauncherTransitionController = null;
mLauncherLayoutListener.setHandler(null);
mLauncherLayoutListener.close(false);
// Restore the requested orientation to the user preference after the gesture has ended
mLauncher.updateRequestedOrientation();
}
public void layoutListenerClosed() {
if (mWasLauncherAlreadyVisible && mLauncherTransitionController != null) {
mLauncherTransitionController.setPlayFraction(1);
}
}
private void switchToScreenshot() {
synchronized (mRecentsAnimationWrapper) {
if (mRecentsAnimationWrapper.controller != null) {
TransactionCompat transaction = new TransactionCompat();
for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
if (app.mode == MODE_CLOSING) {
// Update the screenshot of the task
final ThumbnailData thumbnail =
mRecentsAnimationWrapper.controller.screenshotTask(app.taskId);
mRecentsView.updateThumbnail(app.taskId, thumbnail);
}
}
transaction.apply();
}
}
mRecentsAnimationWrapper.finish(true /* toHome */,
() -> setStateOnUiThread(STATE_SWITCH_TO_SCREENSHOT_COMPLETE));
}
private void setupLauncherUiAfterSwipeUpAnimation() {
// Re apply state in case we did something funky during the transition.
mLauncher.getStateManager().reapplyState();
// Animate the first icon.
View currentRecentsPage = mRecentsView.getPageAt(mRecentsView.getCurrentPage());
if (currentRecentsPage instanceof TaskView) {
((TaskView) currentRecentsPage).animateIconToScale(1f);
}
}
public void onQuickScrubEnd() {
setStateOnUiThread(STATE_QUICK_SCRUB_END);
}
private void switchToFinalAppAfterQuickSwitch() {
mQuickScrubController.onQuickSwitch();
}
private void switchToFinalAppAfterQuickScrub() {
mQuickScrubController.onQuickScrubEnd();
// Normally this is handled in reset(), but since we are still scrubbing after the
// transition into recents, we need to defer the handler invalidation for quick scrub until
// after the gesture ends
setStateOnUiThread(STATE_HANDLER_INVALIDATED);
}
public void onQuickScrubProgress(float progress) {
if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null) {
// TODO: We can still get progress events while launcher is not ready on the worker
// thread. Keep track of last received progress and apply that progress when launcher
// is ready
return;
}
mQuickScrubController.onQuickScrubProgress(progress);
}
private void debugNewState(int stateFlag) {
if (!DEBUG_STATES) {
return;
}
int state = mStateCallback.getState();
StringJoiner currentStateStr = new StringJoiner(", ", "[", "]");
String stateFlagStr = "Unknown-" + stateFlag;
for (int i = 0; i < STATES.length; i++) {
if ((state & (i << i)) != 0) {
currentStateStr.add(STATES[i]);
}
if (stateFlag == (1 << i)) {
stateFlagStr = STATES[i] + " (" + stateFlag + ")";
}
}
Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding " + stateFlagStr + " to "
+ currentStateStr);
}
}