| /* |
| * 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.BaseActivity.INVISIBLE_BY_STATE_HANDLER; |
| import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; |
| import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; |
| import static com.android.launcher3.Utilities.postAsyncCallback; |
| import static com.android.launcher3.anim.Interpolators.DEACCEL; |
| import static com.android.launcher3.anim.Interpolators.LINEAR; |
| import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; |
| import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; |
| import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS; |
| import static com.android.launcher3.util.RaceConditionTracker.ENTER; |
| import static com.android.launcher3.util.RaceConditionTracker.EXIT; |
| import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION; |
| import static com.android.quickstep.QuickScrubController.QUICK_SWITCH_FROM_APP_START_DURATION; |
| import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; |
| import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; |
| import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ObjectAnimator; |
| import android.annotation.TargetApi; |
| import android.app.ActivityManager.RunningTaskInfo; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.util.Log; |
| import android.view.HapticFeedbackConstants; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewTreeObserver.OnDrawListener; |
| import android.view.WindowManager; |
| import android.view.animation.Interpolator; |
| |
| import androidx.annotation.AnyThread; |
| import androidx.annotation.UiThread; |
| import androidx.annotation.WorkerThread; |
| |
| import com.android.launcher3.AbstractFloatingView; |
| import com.android.launcher3.BaseDraggingActivity; |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.InvariantDeviceProfile; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.anim.AnimationSuccessListener; |
| import com.android.launcher3.anim.AnimatorPlaybackController; |
| import com.android.launcher3.anim.Interpolators; |
| import com.android.launcher3.config.FeatureFlags; |
| import com.android.launcher3.logging.UserEventDispatcher; |
| import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; |
| import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; |
| import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; |
| import com.android.launcher3.util.MultiValueAlpha; |
| import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; |
| import com.android.launcher3.util.RaceConditionTracker; |
| import com.android.launcher3.util.TraceHelper; |
| import com.android.quickstep.ActivityControlHelper.ActivityInitListener; |
| import com.android.quickstep.ActivityControlHelper.AnimationFactory; |
| import com.android.quickstep.ActivityControlHelper.LayoutListener; |
| import com.android.quickstep.TouchConsumer.InteractionType; |
| import com.android.quickstep.TouchInteractionService.OverviewTouchConsumer; |
| import com.android.quickstep.util.ClipAnimationHelper; |
| import com.android.quickstep.util.RemoteAnimationTargetSet; |
| import com.android.quickstep.util.TransformedRect; |
| import com.android.quickstep.views.RecentsView; |
| import com.android.quickstep.views.TaskView; |
| import com.android.systemui.shared.recents.model.ThumbnailData; |
| import com.android.systemui.shared.system.ActivityManagerWrapper; |
| import com.android.systemui.shared.system.InputConsumerController; |
| import com.android.systemui.shared.system.LatencyTrackerCompat; |
| import com.android.systemui.shared.system.RecentsAnimationControllerCompat; |
| import com.android.systemui.shared.system.RemoteAnimationTargetCompat; |
| import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; |
| import com.android.systemui.shared.system.WindowCallbacksCompat; |
| |
| import java.util.StringJoiner; |
| import java.util.function.BiFunction; |
| |
| @TargetApi(Build.VERSION_CODES.O) |
| public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { |
| 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_STARTED = 1 << 1; |
| private static final int STATE_LAUNCHER_DRAWN = 1 << 2; |
| private static final int STATE_ACTIVITY_MULTIPLIER_COMPLETE = 1 << 3; |
| |
| // Internal initialization states |
| private static final int STATE_APP_CONTROLLER_RECEIVED = 1 << 4; |
| |
| // Interaction finish states |
| private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 5; |
| private static final int STATE_SCALED_CONTROLLER_LAST_TASK = 1 << 6; |
| |
| private static final int STATE_HANDLER_INVALIDATED = 1 << 7; |
| private static final int STATE_GESTURE_STARTED_QUICKSTEP = 1 << 8; |
| private static final int STATE_GESTURE_STARTED_QUICKSCRUB = 1 << 9; |
| private static final int STATE_GESTURE_CANCELLED = 1 << 10; |
| private static final int STATE_GESTURE_COMPLETED = 1 << 11; |
| |
| // States for quick switch/scrub |
| private static final int STATE_CURRENT_TASK_FINISHED = 1 << 12; |
| private static final int STATE_QUICK_SCRUB_START = 1 << 13; |
| private static final int STATE_QUICK_SCRUB_END = 1 << 14; |
| |
| private static final int STATE_CAPTURE_SCREENSHOT = 1 << 15; |
| private static final int STATE_SCREENSHOT_CAPTURED = 1 << 16; |
| private static final int STATE_SCREENSHOT_VIEW_SHOWN = 1 << 17; |
| |
| private static final int STATE_RESUME_LAST_TASK = 1 << 18; |
| private static final int STATE_START_NEW_TASK = 1 << 19; |
| private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 20; |
| |
| |
| private static final int LAUNCHER_UI_STATES = |
| STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE |
| | STATE_LAUNCHER_STARTED; |
| |
| private static final int LONG_SWIPE_ENTER_STATE = |
| STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED |
| | STATE_APP_CONTROLLER_RECEIVED; |
| |
| private static final int LONG_SWIPE_START_STATE = |
| STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED |
| | STATE_APP_CONTROLLER_RECEIVED | STATE_SCREENSHOT_CAPTURED; |
| |
| private static final int QUICK_SCRUB_START_UI_STATE = STATE_LAUNCHER_STARTED |
| | STATE_QUICK_SCRUB_START | STATE_APP_CONTROLLER_RECEIVED; |
| |
| // For debugging, keep in sync with above states |
| private static final String[] STATES = new String[] { |
| "STATE_LAUNCHER_PRESENT", |
| "STATE_LAUNCHER_STARTED", |
| "STATE_LAUNCHER_DRAWN", |
| "STATE_ACTIVITY_MULTIPLIER_COMPLETE", |
| "STATE_APP_CONTROLLER_RECEIVED", |
| "STATE_SCALED_CONTROLLER_RECENTS", |
| "STATE_SCALED_CONTROLLER_LAST_TASK", |
| "STATE_HANDLER_INVALIDATED", |
| "STATE_GESTURE_STARTED_QUICKSTEP", |
| "STATE_GESTURE_STARTED_QUICKSCRUB", |
| "STATE_GESTURE_CANCELLED", |
| "STATE_GESTURE_COMPLETED", |
| "STATE_CURRENT_TASK_FINISHED", |
| "STATE_QUICK_SCRUB_START", |
| "STATE_QUICK_SCRUB_END", |
| "STATE_CAPTURE_SCREENSHOT", |
| "STATE_SCREENSHOT_CAPTURED", |
| "STATE_SCREENSHOT_VIEW_SHOWN", |
| "STATE_RESUME_LAST_TASK", |
| "STATE_START_NEW_TASK", |
| "STATE_ASSIST_DATA_RECEIVED", |
| }; |
| |
| public static final long MAX_SWIPE_DURATION = 350; |
| public static final long MIN_SWIPE_DURATION = 80; |
| public static final long MIN_OVERSHOOT_DURATION = 120; |
| |
| public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f; |
| private static final float SWIPE_DURATION_MULTIPLIER = |
| Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW)); |
| private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured"; |
| |
| private final ClipAnimationHelper mClipAnimationHelper; |
| private final ClipAnimationHelper.TransformParams mTransformParams; |
| |
| protected Runnable mGestureEndCallback; |
| protected boolean mIsGoingToRecents; |
| 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 boolean mDispatchedDownEvent; |
| // To avoid UI jump when gesture is started, we offset the animation by the threshold. |
| private float mShiftAtGestureStart = 0; |
| |
| private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); |
| |
| // An increasing identifier per single instance of OtherActivityTouchConsumer. Generally one |
| // instance of OtherActivityTouchConsumer will only have one swipe handle, but sometimes we can |
| // end up with multiple handlers if we get recents command in the middle of a swipe gesture. |
| // This is used to match the corresponding activity manager callbacks in |
| // OtherActivityTouchConsumer |
| public final int id; |
| private final Context mContext; |
| private final ActivityControlHelper<T> mActivityControlHelper; |
| private final ActivityInitListener mActivityInitListener; |
| private final TouchInteractionLog mTouchInteractionLog; |
| |
| private final int mRunningTaskId; |
| private final RunningTaskInfo mRunningTaskInfo; |
| private ThumbnailData mTaskSnapshot; |
| |
| private MultiStateCallback mStateCallback; |
| private AnimatorPlaybackController mLauncherTransitionController; |
| |
| private T mActivity; |
| private LayoutListener mLayoutListener; |
| private RecentsView mRecentsView; |
| private SyncRtSurfaceTransactionApplierCompat mSyncTransactionApplier; |
| private QuickScrubController mQuickScrubController; |
| private AnimationFactory mAnimationFactory = (t, i) -> { }; |
| |
| private Runnable mLauncherDrawnCallback; |
| |
| private boolean mWasLauncherAlreadyVisible; |
| |
| private boolean mPassedOverviewThreshold; |
| private boolean mGestureStarted; |
| private int mLogAction = Touch.SWIPE; |
| private float mCurrentQuickScrubProgress; |
| private boolean mQuickScrubBlocked; |
| |
| private @InteractionType int mInteractionType = INTERACTION_NORMAL; |
| |
| private final RecentsAnimationWrapper mRecentsAnimationWrapper; |
| |
| private final long mTouchTimeMs; |
| private long mLauncherFrameDrawnTime; |
| |
| private boolean mBgLongSwipeMode = false; |
| private boolean mUiLongSwipeMode = false; |
| private float mLongSwipeDisplacement = 0; |
| private LongSwipeHelper mLongSwipeController; |
| |
| private Bundle mAssistData; |
| |
| WindowTransformSwipeHandler(int id, RunningTaskInfo runningTaskInfo, Context context, |
| long touchTimeMs, ActivityControlHelper<T> controller, |
| InputConsumerController inputConsumer, TouchInteractionLog touchInteractionLog) { |
| this.id = id; |
| mContext = context; |
| mRunningTaskInfo = runningTaskInfo; |
| mRunningTaskId = runningTaskInfo.id; |
| mTouchTimeMs = touchTimeMs; |
| mActivityControlHelper = controller; |
| mActivityInitListener = mActivityControlHelper |
| .createActivityInitListener(this::onActivityInit); |
| mTouchInteractionLog = touchInteractionLog; |
| mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer, |
| this::createNewTouchProxyHandler); |
| mClipAnimationHelper = new ClipAnimationHelper(context); |
| mTransformParams = new ClipAnimationHelper.TransformParams(); |
| |
| initStateCallbacks(); |
| } |
| |
| private void initStateCallbacks() { |
| mStateCallback = new MultiStateCallback() { |
| @Override |
| public void setState(int stateFlag) { |
| debugNewState(stateFlag); |
| super.setState(stateFlag); |
| } |
| }; |
| |
| // Re-setup the recents UI when gesture starts, as the state could have been changed during |
| // that time by a previous window transition. |
| mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_GESTURE_STARTED_QUICKSTEP, |
| this::setupRecentsViewUi); |
| |
| mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED_QUICKSCRUB, |
| this::initializeLauncherAnimationController); |
| mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED_QUICKSTEP, |
| this::initializeLauncherAnimationController); |
| |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN, |
| this::launcherFrameDrawn); |
| |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED_QUICKSTEP, |
| this::notifyGestureStartedAsync); |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED_QUICKSCRUB, |
| this::notifyGestureStartedAsync); |
| |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED |
| | STATE_GESTURE_CANCELLED, |
| this::resetStateForAnimationCancel); |
| |
| mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED, |
| this::sendRemoteAnimationsToAnimationFactory); |
| |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_LAST_TASK, |
| this::resumeLastTaskForQuickstep); |
| mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED, |
| this::resumeLastTask); |
| mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_APP_CONTROLLER_RECEIVED, |
| this::startNewTask); |
| |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED |
| | STATE_ACTIVITY_MULTIPLIER_COMPLETE |
| | STATE_CAPTURE_SCREENSHOT, |
| this::switchToScreenshot); |
| |
| mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED |
| | STATE_SCALED_CONTROLLER_RECENTS, |
| this::finishCurrentTransitionToRecents); |
| |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED |
| | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS |
| | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED |
| | STATE_GESTURE_STARTED_QUICKSTEP, |
| this::setupLauncherUiAfterSwipeUpAnimation); |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED |
| | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS |
| | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED |
| | STATE_GESTURE_STARTED_QUICKSTEP | STATE_ASSIST_DATA_RECEIVED, |
| this::preloadAssistData); |
| |
| mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler); |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, |
| this::invalidateHandlerWithLauncher); |
| mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED |
| | STATE_SCALED_CONTROLLER_LAST_TASK, |
| this::notifyTransitionCancelled); |
| |
| mStateCallback.addCallback(QUICK_SCRUB_START_UI_STATE, this::onQuickScrubStartUi); |
| mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START |
| | STATE_SCALED_CONTROLLER_RECENTS, this::onFinishedTransitionToQuickScrub); |
| mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_CURRENT_TASK_FINISHED |
| | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub); |
| |
| mStateCallback.addCallback(LONG_SWIPE_ENTER_STATE, this::checkLongSwipeCanEnter); |
| mStateCallback.addCallback(LONG_SWIPE_START_STATE, this::checkLongSwipeCanStart); |
| |
| if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { |
| mStateCallback.addChangeHandler(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT |
| | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT, |
| (b) -> mRecentsView.setRunningTaskHidden(!b)); |
| } |
| } |
| |
| private void executeOnUiThread(Runnable action) { |
| if (Looper.myLooper() == mMainThreadHandler.getLooper()) { |
| action.run(); |
| } else { |
| postAsyncCallback(mMainThreadHandler, action); |
| } |
| } |
| |
| private void setStateOnUiThread(int stateFlag) { |
| if (Looper.myLooper() == mMainThreadHandler.getLooper()) { |
| mStateCallback.setState(stateFlag); |
| } else { |
| postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag)); |
| } |
| } |
| |
| private void initTransitionEndpoints(DeviceProfile dp) { |
| mDp = dp; |
| |
| TransformedRect tempRect = new TransformedRect(); |
| mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength( |
| dp, mContext, mInteractionType, tempRect); |
| mClipAnimationHelper.updateTargetRect(tempRect); |
| } |
| |
| 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; |
| } |
| } |
| |
| public void initWhenReady() { |
| mActivityInitListener.register(); |
| } |
| |
| private boolean onActivityInit(final T activity, Boolean alreadyOnHome) { |
| if (mActivity == activity) { |
| return true; |
| } |
| if (mActivity != null) { |
| // The launcher may have been recreated as a result of device rotation. |
| int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES; |
| initStateCallbacks(); |
| mStateCallback.setState(oldState); |
| mLayoutListener.setHandler(null); |
| } |
| mWasLauncherAlreadyVisible = alreadyOnHome; |
| mActivity = activity; |
| // Override the visibility of the activity until the gesture actually starts and we swipe |
| // up, or until we transition home and the home animation is composed |
| if (alreadyOnHome) { |
| mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); |
| } else { |
| mActivity.addForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); |
| } |
| |
| mRecentsView = activity.getOverviewPanel(); |
| SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, (applier) -> { |
| mSyncTransactionApplier = applier; |
| }); |
| mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { |
| if (!mBgLongSwipeMode) { |
| updateFinalShift(); |
| } |
| }); |
| mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper); |
| mRecentsView.setClipAnimationHelper(mClipAnimationHelper); |
| mQuickScrubController = mRecentsView.getQuickScrubController(); |
| mLayoutListener = mActivityControlHelper.createLayoutListener(mActivity); |
| |
| mStateCallback.setState(STATE_LAUNCHER_PRESENT); |
| if (alreadyOnHome) { |
| onLauncherStart(activity); |
| } else { |
| activity.setOnStartCallback(this::onLauncherStart); |
| } |
| return true; |
| } |
| |
| private void onLauncherStart(final T activity) { |
| if (mActivity != activity) { |
| return; |
| } |
| if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { |
| return; |
| } |
| |
| mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity, |
| mWasLauncherAlreadyVisible, true, this::onAnimatorPlaybackControllerCreated); |
| AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible); |
| |
| if (mWasLauncherAlreadyVisible) { |
| mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_DRAWN); |
| } else { |
| TraceHelper.beginSection("WTS-init"); |
| View dragLayer = activity.getDragLayer(); |
| mActivityControlHelper.getAlphaProperty(activity).setValue(0); |
| dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { |
| |
| @Override |
| public void onDraw() { |
| TraceHelper.endSection("WTS-init", "Launcher frame is drawn"); |
| dragLayer.post(() -> |
| dragLayer.getViewTreeObserver().removeOnDrawListener(this)); |
| if (activity != mActivity) { |
| return; |
| } |
| |
| mStateCallback.setState(STATE_LAUNCHER_DRAWN); |
| } |
| }); |
| } |
| |
| setupRecentsViewUi(); |
| mLayoutListener.open(); |
| mStateCallback.setState(STATE_LAUNCHER_STARTED); |
| } |
| |
| private void setupRecentsViewUi() { |
| mRecentsView.setEnableDrawingLiveTile(false); |
| mRecentsView.showTask(mRunningTaskId); |
| mRecentsView.setRunningTaskHidden(true); |
| mRecentsView.setRunningTaskIconScaledDown(true); |
| } |
| |
| public void setLauncherOnDrawCallback(Runnable callback) { |
| mLauncherDrawnCallback = callback; |
| } |
| |
| private void launcherFrameDrawn() { |
| AlphaProperty property = mActivityControlHelper.getAlphaProperty(mActivity); |
| if (property.getValue() < 1) { |
| if (mGestureStarted) { |
| final MultiStateCallback callback = mStateCallback; |
| ObjectAnimator animator = ObjectAnimator.ofFloat( |
| property, MultiValueAlpha.VALUE, 1); |
| animator.setDuration(getFadeInDuration()).addListener( |
| new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| callback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); |
| } |
| }); |
| animator.start(); |
| } else { |
| property.setValue(1); |
| mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); |
| } |
| } |
| if (mLauncherDrawnCallback != null) { |
| mLauncherDrawnCallback.run(); |
| } |
| mLauncherFrameDrawnTime = SystemClock.uptimeMillis(); |
| } |
| |
| private void sendRemoteAnimationsToAnimationFactory() { |
| mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet); |
| } |
| |
| private void initializeLauncherAnimationController() { |
| mLayoutListener.setHandler(this); |
| buildAnimationController(); |
| |
| if (LatencyTrackerCompat.isEnabled(mContext)) { |
| LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs)); |
| } |
| |
| // This method is only called when STATE_GESTURE_STARTED_QUICKSTEP/ |
| // STATE_GESTURE_STARTED_QUICKSCRUB is set, so we can enable the high-res thumbnail loader |
| // here once we are sure that we will end up in an overview state |
| RecentsModel.INSTANCE.get(mContext).getThumbnailCache() |
| .getHighResLoadingState().setVisible(true); |
| } |
| |
| private void shiftAnimationDestinationForQuickscrub() { |
| TransformedRect tempRect = new TransformedRect(); |
| mActivityControlHelper |
| .getSwipeUpDestinationAndLength(mDp, mContext, mInteractionType, tempRect); |
| mClipAnimationHelper.updateTargetRect(tempRect); |
| |
| float offsetY = |
| mActivityControlHelper.getTranslationYForQuickScrub(tempRect, mDp, mContext); |
| float scale, offsetX; |
| Resources res = mContext.getResources(); |
| |
| if (ActivityManagerWrapper.getInstance().getRecentTasks(2, UserHandle.myUserId()).size() |
| < 2) { |
| // There are not enough tasks, we don't need to shift |
| offsetX = 0; |
| scale = 1; |
| } else { |
| offsetX = res.getDimensionPixelSize(R.dimen.recents_page_spacing) |
| + tempRect.rect.width(); |
| scale = getTaskCurveScaleForOffsetX(offsetX, tempRect.rect.width()); |
| } |
| mClipAnimationHelper.offsetTarget(scale, Utilities.isRtl(res) ? -offsetX : offsetX, offsetY, |
| QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR); |
| } |
| |
| private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) { |
| float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 + |
| mContext.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing); |
| float interpolation = Math.min(1, offsetX / distanceToReachEdge); |
| return TaskView.getCurveScaleForInterpolation(interpolation); |
| } |
| |
| @WorkerThread |
| public void dispatchMotionEventToRecentsView(MotionEvent event) { |
| if (mRecentsView == null) { |
| return; |
| } |
| // Pass the motion events to RecentsView to allow scrolling during swipe up. |
| if (mDispatchedDownEvent) { |
| mRecentsView.dispatchTouchEvent(event); |
| } else { |
| // The first event we dispatch should be ACTION_DOWN. |
| mDispatchedDownEvent = true; |
| MotionEvent downEvent = MotionEvent.obtain(event); |
| downEvent.setAction(MotionEvent.ACTION_DOWN); |
| int flags = downEvent.getEdgeFlags(); |
| downEvent.setEdgeFlags(flags | TouchInteractionService.EDGE_NAV_BAR); |
| mRecentsView.dispatchTouchEvent(downEvent); |
| downEvent.recycle(); |
| } |
| } |
| |
| @WorkerThread |
| public void updateDisplacement(float displacement) { |
| // We are moving in the negative x/y direction |
| displacement = -displacement; |
| if (displacement > mTransitionDragLength && mTransitionDragLength > 0) { |
| mCurrentShift.updateValue(1); |
| |
| if (!mBgLongSwipeMode) { |
| mBgLongSwipeMode = true; |
| executeOnUiThread(this::onLongSwipeEnabledUi); |
| } |
| mLongSwipeDisplacement = displacement - mTransitionDragLength; |
| executeOnUiThread(this::onLongSwipeDisplacementUpdated); |
| } else { |
| if (mBgLongSwipeMode) { |
| mBgLongSwipeMode = false; |
| executeOnUiThread(this::onLongSwipeDisabledUi); |
| } |
| float translation = Math.max(displacement, 0); |
| float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; |
| mCurrentShift.updateValue(shift); |
| } |
| } |
| |
| /** |
| * Called by {@link #mLayoutListener} when launcher layout changes |
| */ |
| public void buildAnimationController() { |
| initTransitionEndpoints(mActivity.getDeviceProfile()); |
| mAnimationFactory.createActivityController(mTransitionDragLength, mInteractionType); |
| } |
| |
| private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) { |
| mLauncherTransitionController = anim; |
| mLauncherTransitionController.dispatchOnStart(); |
| updateLauncherTransitionProgress(); |
| } |
| |
| @WorkerThread |
| private void updateFinalShift() { |
| float shift = mCurrentShift.value; |
| |
| RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); |
| if (controller != null) { |
| float offsetX = 0; |
| if (mRecentsView != null && mInteractionType == INTERACTION_NORMAL) { |
| int startScroll = mRecentsView.getScrollForPage(mRecentsView.indexOfChild( |
| mRecentsView.getRunningTaskView())); |
| offsetX = startScroll - mRecentsView.getScrollX(); |
| offsetX *= mRecentsView.getScaleX(); |
| } |
| float offsetScale = getTaskCurveScaleForOffsetX(offsetX, |
| mClipAnimationHelper.getTargetRect().width()); |
| SyncRtSurfaceTransactionApplierCompat syncTransactionApplier |
| = Looper.myLooper() == mMainThreadHandler.getLooper() |
| ? mSyncTransactionApplier |
| : null; |
| mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale) |
| .setSyncTransactionApplier(syncTransactionApplier); |
| mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, |
| mTransformParams); |
| |
| boolean passedThreshold = shift > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD; |
| mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold); |
| if (mActivityControlHelper.shouldMinimizeSplitScreen()) { |
| mRecentsAnimationWrapper.setSplitScreenMinimizedForTransaction(passedThreshold); |
| } |
| } |
| |
| executeOnUiThread(this::updateFinalShiftUi); |
| } |
| |
| private void updateFinalShiftUi() { |
| if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { |
| if (mRecentsAnimationWrapper.getController() != null && mLayoutListener != null) { |
| mLayoutListener.open(); |
| mLayoutListener.update(mCurrentShift.value > 1, mUiLongSwipeMode, |
| mClipAnimationHelper.getCurrentRectWithInsets(), |
| mClipAnimationHelper.getCurrentCornerRadius()); |
| } |
| } |
| |
| final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW; |
| if (passed != mPassedOverviewThreshold) { |
| mPassedOverviewThreshold = passed; |
| if (mInteractionType == INTERACTION_NORMAL && mRecentsView != null) { |
| mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, |
| HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); |
| } |
| } |
| // Update insets of the adjacent tasks, as we might switch to them. |
| int runningTaskIndex = mRecentsView == null ? -1 : mRecentsView.getRunningTaskIndex(); |
| if (mInteractionType == INTERACTION_NORMAL && runningTaskIndex >= 0) { |
| TaskView nextTaskView = mRecentsView.getTaskViewAt(runningTaskIndex + 1); |
| TaskView prevTaskView = mRecentsView.getTaskViewAt(runningTaskIndex - 1); |
| if (nextTaskView != null) { |
| nextTaskView.setFullscreenProgress(1 - mCurrentShift.value); |
| } |
| if (prevTaskView != null) { |
| prevTaskView.setFullscreenProgress(1 - mCurrentShift.value); |
| } |
| } |
| |
| if (mLauncherTransitionController == null || mLauncherTransitionController |
| .getAnimationPlayer().isStarted()) { |
| return; |
| } |
| updateLauncherTransitionProgress(); |
| } |
| |
| private void updateLauncherTransitionProgress() { |
| float progress = mCurrentShift.value; |
| mLauncherTransitionController.setPlayFraction( |
| progress <= mShiftAtGestureStart || mShiftAtGestureStart >= 1 |
| ? 0 : (progress - mShiftAtGestureStart) / (1 - mShiftAtGestureStart)); |
| } |
| |
| public void onRecentsAnimationStart(RecentsAnimationControllerCompat controller, |
| RemoteAnimationTargetSet targets, Rect homeContentInsets, Rect minimizedHomeBounds) { |
| DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext); |
| final Rect overviewStackBounds; |
| RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mRunningTaskId); |
| |
| if (minimizedHomeBounds != null && runningTaskTarget != null) { |
| overviewStackBounds = mActivityControlHelper |
| .getOverviewWindowBounds(minimizedHomeBounds, runningTaskTarget); |
| dp = dp.getMultiWindowProfile(mContext, |
| new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height())); |
| dp.updateInsets(homeContentInsets); |
| } else { |
| if (mActivity != null) { |
| int loc[] = new int[2]; |
| View rootView = mActivity.getRootView(); |
| rootView.getLocationOnScreen(loc); |
| overviewStackBounds = new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(), |
| loc[1] + rootView.getHeight()); |
| } else { |
| overviewStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx); |
| } |
| // If we are not in multi-window mode, home insets should be same as system insets. |
| dp = dp.copy(mContext); |
| dp.updateInsets(homeContentInsets); |
| } |
| dp.updateIsSeascape(mContext.getSystemService(WindowManager.class)); |
| |
| if (runningTaskTarget != null) { |
| mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget); |
| } |
| mClipAnimationHelper.prepareAnimation(false /* isOpening */); |
| initTransitionEndpoints(dp); |
| |
| mRecentsAnimationWrapper.setController(controller, targets); |
| mTouchInteractionLog.startRecentsAnimationCallback(targets.apps.length); |
| setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED); |
| |
| mPassedOverviewThreshold = false; |
| } |
| |
| public void onRecentsAnimationCanceled() { |
| mRecentsAnimationWrapper.setController(null, null); |
| mActivityInitListener.unregister(); |
| setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED); |
| mTouchInteractionLog.cancelRecentsAnimation(); |
| } |
| |
| public void onGestureStarted() { |
| notifyGestureStartedAsync(); |
| mShiftAtGestureStart = mCurrentShift.value; |
| setStateOnUiThread(mInteractionType == INTERACTION_NORMAL |
| ? STATE_GESTURE_STARTED_QUICKSTEP : STATE_GESTURE_STARTED_QUICKSCRUB); |
| mGestureStarted = true; |
| mRecentsAnimationWrapper.hideCurrentInputMethod(); |
| mRecentsAnimationWrapper.enableInputConsumer(); |
| } |
| |
| /** |
| * Notifies the launcher that the swipe gesture has started. This can be called multiple times |
| * on both background and UI threads |
| */ |
| @AnyThread |
| private void notifyGestureStartedAsync() { |
| final T curActivity = mActivity; |
| if (curActivity != null) { |
| // Once the gesture starts, we can no longer transition home through the button, so |
| // reset the force override of the activity visibility |
| mActivity.clearForceInvisibleFlag(STATE_HANDLER_INVISIBILITY_FLAGS); |
| } |
| } |
| |
| @WorkerThread |
| public void onGestureEnded(float endVelocity, float velocityX) { |
| float flingThreshold = mContext.getResources() |
| .getDimension(R.dimen.quickstep_fling_threshold_velocity); |
| boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold; |
| setStateOnUiThread(STATE_GESTURE_COMPLETED); |
| |
| mLogAction = isFling ? Touch.FLING : Touch.SWIPE; |
| |
| if (mBgLongSwipeMode) { |
| executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling, velocityX)); |
| } else { |
| handleNormalGestureEnd(endVelocity, isFling, velocityX); |
| } |
| } |
| |
| @UiThread |
| private TouchConsumer createNewTouchProxyHandler() { |
| mCurrentShift.finishAnimation(); |
| if (mLauncherTransitionController != null) { |
| mLauncherTransitionController.getAnimationPlayer().end(); |
| } |
| if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { |
| // Hide the task view, if not already hidden |
| setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha); |
| } |
| |
| return OverviewTouchConsumer.newInstance(mActivityControlHelper, true, |
| mTouchInteractionLog); |
| } |
| |
| private void handleNormalGestureEnd(float endVelocity, boolean isFling, float velocityX) { |
| float velocityPxPerMs = endVelocity / 1000; |
| float velocityXPxPerMs = velocityX / 1000; |
| long duration = MAX_SWIPE_DURATION; |
| float currentShift = mCurrentShift.value; |
| final boolean goingToRecents; |
| float endShift; |
| final float startShift; |
| Interpolator interpolator = DEACCEL; |
| final int nextPage = mRecentsView != null ? mRecentsView.getNextPage() : -1; |
| final int runningTaskIndex = mRecentsView != null ? mRecentsView.getRunningTaskIndex() : -1; |
| boolean goingToNewTask = mRecentsView != null && nextPage != runningTaskIndex; |
| final boolean reachedOverviewThreshold = currentShift >= MIN_PROGRESS_FOR_OVERVIEW; |
| if (!isFling) { |
| goingToRecents = reachedOverviewThreshold && mGestureStarted; |
| endShift = goingToRecents ? 1 : 0; |
| long expectedDuration = Math.abs(Math.round((endShift - currentShift) |
| * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); |
| duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); |
| startShift = currentShift; |
| interpolator = goingToRecents ? OVERSHOOT_1_2 : DEACCEL; |
| } else { |
| // If user scrolled to a new task, only go to recents if they already passed |
| // the overview threshold. Otherwise, we'll snap to the new task and launch it. |
| goingToRecents = endVelocity < 0 && (!goingToNewTask || reachedOverviewThreshold); |
| endShift = goingToRecents ? 1 : 0; |
| startShift = Utilities.boundToRange(currentShift - velocityPxPerMs |
| * SINGLE_FRAME_MS / mTransitionDragLength, 0, 1); |
| float minFlingVelocity = mContext.getResources() |
| .getDimension(R.dimen.quickstep_fling_min_velocity); |
| if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) { |
| if (goingToRecents) { |
| Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams( |
| startShift, endShift, endShift, velocityPxPerMs, mTransitionDragLength); |
| endShift = overshoot.end; |
| interpolator = overshoot.interpolator; |
| duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION, |
| MAX_SWIPE_DURATION); |
| } else { |
| float distanceToTravel = (endShift - currentShift) * 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. 2. |
| long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs)); |
| duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); |
| } |
| } |
| } |
| if (goingToRecents) { |
| mRecentsAnimationWrapper.enableTouchProxy(); |
| } else if (goingToNewTask) { |
| // We aren't goingToRecents, and user scrolled/flung to a new task; snap to the closest |
| // task in that direction and launch it (in startNewTask()). |
| int taskToLaunch = runningTaskIndex + (nextPage > runningTaskIndex ? 1 : - 1); |
| if (taskToLaunch >= mRecentsView.getTaskViewCount()) { |
| // Scrolled to Clear all button, snap back to current task and resume it. |
| mRecentsView.snapToPage(runningTaskIndex, Math.toIntExact(duration)); |
| goingToNewTask = false; |
| } else { |
| float distance = Math.abs(mRecentsView.getScrollForPage(taskToLaunch) |
| - mRecentsView.getScrollX()); |
| int durationX = (int) Math.abs(distance / velocityXPxPerMs); |
| if (durationX > MAX_SWIPE_DURATION) { |
| durationX = Math.toIntExact(MAX_SWIPE_DURATION); |
| } |
| interpolator = Interpolators.scrollInterpolatorForVelocity(velocityXPxPerMs); |
| mRecentsView.snapToPage(taskToLaunch, durationX, interpolator); |
| duration = Math.max(duration, durationX); |
| } |
| } |
| |
| animateToProgress(startShift, endShift, duration, interpolator, goingToRecents, |
| goingToNewTask, velocityPxPerMs); |
| } |
| |
| private void doLogGesture(boolean toLauncher) { |
| DeviceProfile dp = mDp; |
| if (dp == null) { |
| // We probably never received an animation controller, skip logging. |
| return; |
| } |
| final int direction; |
| if (dp.isVerticalBarLayout()) { |
| direction = (dp.isSeascape() ^ toLauncher) ? Direction.LEFT : Direction.RIGHT; |
| } else { |
| direction = toLauncher ? Direction.UP : Direction.DOWN; |
| } |
| |
| int dstContainerType = toLauncher ? ContainerType.TASKSWITCHER : ContainerType.APP; |
| UserEventDispatcher.newInstance(mContext).logStateChangeAction( |
| mLogAction, direction, |
| ContainerType.NAVBAR, ContainerType.APP, |
| dstContainerType, |
| 0); |
| } |
| |
| /** Animates to the given progress, where 0 is the current app and 1 is overview. */ |
| private void animateToProgress(float start, float end, long duration, Interpolator interpolator, |
| boolean goingToRecents, boolean goingToNewTask, float velocityPxPerMs) { |
| mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration, |
| interpolator, goingToRecents, goingToNewTask, velocityPxPerMs)); |
| } |
| |
| private void animateToProgressInternal(float start, float end, long duration, |
| Interpolator interpolator, boolean goingToRecents, boolean goingToNewTask, |
| float velocityPxPerMs) { |
| mIsGoingToRecents = goingToRecents; |
| ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration); |
| anim.setInterpolator(interpolator); |
| anim.addListener(new AnimationSuccessListener() { |
| @Override |
| public void onAnimationSuccess(Animator animator) { |
| int recentsState = STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT |
| | STATE_SCREENSHOT_VIEW_SHOWN; |
| setStateOnUiThread(mIsGoingToRecents |
| ? recentsState |
| : goingToNewTask |
| ? STATE_START_NEW_TASK |
| : STATE_SCALED_CONTROLLER_LAST_TASK); |
| } |
| }); |
| anim.start(); |
| long startMillis = SystemClock.uptimeMillis(); |
| executeOnUiThread(() -> { |
| // Animate the launcher components at the same time as the window, always on UI thread. |
| if (mLauncherTransitionController == null) { |
| return; |
| } |
| if (start == end || duration <= 0) { |
| mLauncherTransitionController.dispatchSetInterpolator(t -> end); |
| mLauncherTransitionController.getAnimationPlayer().end(); |
| } else { |
| // Adjust start progress and duration in case we are on a different thread. |
| long elapsedMillis = SystemClock.uptimeMillis() - startMillis; |
| elapsedMillis = Utilities.boundToRange(elapsedMillis, 0, duration); |
| float elapsedProgress = (float) elapsedMillis / duration; |
| float adjustedStart = Utilities.mapRange(elapsedProgress, start, end); |
| long adjustedDuration = duration - elapsedMillis; |
| // We want to use the same interpolator as the window, but need to adjust it to |
| // interpolate over the remaining progress (end - start). |
| mLauncherTransitionController.dispatchSetInterpolator(Interpolators.mapToProgress( |
| interpolator, adjustedStart, end)); |
| mLauncherTransitionController.getAnimationPlayer().setDuration(adjustedDuration); |
| |
| if (QUICKSTEP_SPRINGS.get()) { |
| mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs); |
| } else { |
| mLauncherTransitionController.getAnimationPlayer().start(); |
| } |
| } |
| }); |
| } |
| |
| @UiThread |
| private void resumeLastTaskForQuickstep() { |
| setStateOnUiThread(STATE_RESUME_LAST_TASK); |
| doLogGesture(false /* toLauncher */); |
| reset(); |
| } |
| |
| @UiThread |
| private void resumeLastTask() { |
| mRecentsAnimationWrapper.finish(false /* toRecents */, null); |
| mTouchInteractionLog.finishRecentsAnimation(false); |
| } |
| |
| @UiThread |
| private void startNewTask() { |
| // Launch the task user scrolled to (mRecentsView.getNextPage()). |
| mRecentsAnimationWrapper.finish(true /* toRecents */, () -> { |
| mRecentsView.getTaskViewAt(mRecentsView.getNextPage()).launchTask(false, |
| result -> setStateOnUiThread(STATE_HANDLER_INVALIDATED), |
| mMainThreadHandler); |
| }); |
| mTouchInteractionLog.finishRecentsAnimation(false); |
| doLogGesture(false /* toLauncher */); |
| } |
| |
| 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.finishAnimation(); |
| |
| if (mGestureEndCallback != null) { |
| mGestureEndCallback.run(); |
| } |
| |
| mActivityInitListener.unregister(); |
| mTaskSnapshot = null; |
| |
| if (mRecentsView != null) { |
| mRecentsView.setOnScrollChangeListener(null); |
| } |
| } |
| |
| private void invalidateHandlerWithLauncher() { |
| mLauncherTransitionController = null; |
| mLayoutListener.finish(); |
| mActivityControlHelper.getAlphaProperty(mActivity).setValue(1); |
| |
| mRecentsView.setRunningTaskIconScaledDown(false); |
| mQuickScrubController.cancelActiveQuickscrub(); |
| } |
| |
| private void notifyTransitionCancelled() { |
| mAnimationFactory.onTransitionCancelled(); |
| } |
| |
| private void resetStateForAnimationCancel() { |
| boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted; |
| mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible); |
| |
| // Leave the pending invisible flag, as it may be used by wallpaper open animation. |
| mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); |
| } |
| |
| public void layoutListenerClosed() { |
| mRecentsView.setRunningTaskHidden(false); |
| if (mWasLauncherAlreadyVisible && mLauncherTransitionController != null) { |
| mLauncherTransitionController.setPlayFraction(1); |
| } |
| mRecentsView.setEnableDrawingLiveTile(true); |
| } |
| |
| private void switchToScreenshot() { |
| if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { |
| setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); |
| } else { |
| boolean finishTransitionPosted = false; |
| RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); |
| if (controller != null) { |
| // Update the screenshot of the task |
| if (mTaskSnapshot == null) { |
| mTaskSnapshot = controller.screenshotTask(mRunningTaskId); |
| } |
| TaskView taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot); |
| if (taskView != null) { |
| // Defer finishing the animation until the next launcher frame with the |
| // new thumbnail |
| finishTransitionPosted = new WindowCallbacksCompat(taskView) { |
| |
| // The number of frames to defer until we actually finish the animation |
| private int mDeferFrameCount = 2; |
| |
| @Override |
| public void onPostDraw(Canvas canvas) { |
| if (mDeferFrameCount > 0) { |
| mDeferFrameCount--; |
| // Workaround, detach and reattach to invalidate the root node for |
| // another draw |
| detach(); |
| attach(); |
| taskView.invalidate(); |
| return; |
| } |
| |
| setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); |
| detach(); |
| } |
| }.attach(); |
| } |
| } |
| if (!finishTransitionPosted) { |
| // If we haven't posted a draw callback, set the state immediately. |
| RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, ENTER); |
| setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); |
| RaceConditionTracker.onEvent(SCREENSHOT_CAPTURED_EVT, EXIT); |
| } |
| } |
| } |
| |
| private void finishCurrentTransitionToRecents() { |
| if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { |
| setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); |
| } else { |
| synchronized (mRecentsAnimationWrapper) { |
| mRecentsAnimationWrapper.finish(true /* toRecents */, |
| () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); |
| } |
| } |
| mTouchInteractionLog.finishRecentsAnimation(true); |
| } |
| |
| private void setupLauncherUiAfterSwipeUpAnimation() { |
| if (mLauncherTransitionController != null) { |
| mLauncherTransitionController.getAnimationPlayer().end(); |
| mLauncherTransitionController = null; |
| } |
| mActivityControlHelper.onSwipeUpComplete(mActivity); |
| |
| // Animate the first icon. |
| mRecentsView.animateUpRunningTaskIconScale(); |
| mRecentsView.setSwipeDownShouldLaunchApp(true); |
| |
| RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG); |
| |
| doLogGesture(true /* toLauncher */); |
| reset(); |
| } |
| |
| public void onQuickScrubStart() { |
| if (mInteractionType != INTERACTION_NORMAL) { |
| throw new IllegalArgumentException( |
| "Can't change interaction type from " + mInteractionType); |
| } |
| mInteractionType = INTERACTION_QUICK_SCRUB; |
| mRecentsAnimationWrapper.runOnInit(this::shiftAnimationDestinationForQuickscrub); |
| |
| setStateOnUiThread(STATE_QUICK_SCRUB_START | STATE_GESTURE_COMPLETED); |
| |
| // Start the window animation without waiting for launcher. |
| long duration = FeatureFlags.QUICK_SWITCH.get() |
| ? QUICK_SWITCH_FROM_APP_START_DURATION |
| : QUICK_SCRUB_FROM_APP_START_DURATION; |
| animateToProgress(mCurrentShift.value, 1f, duration, LINEAR, true /* goingToRecents */, |
| false /* goingToNewTask */, 1f); |
| } |
| |
| private void onQuickScrubStartUi() { |
| if (!mQuickScrubController.prepareQuickScrub(TAG, FeatureFlags.QUICK_SWITCH.get())) { |
| mQuickScrubBlocked = true; |
| setStateOnUiThread(STATE_RESUME_LAST_TASK | STATE_HANDLER_INVALIDATED); |
| return; |
| } |
| if (mLauncherTransitionController != null) { |
| mLauncherTransitionController.getAnimationPlayer().end(); |
| mLauncherTransitionController = null; |
| } |
| |
| mActivityControlHelper.onQuickInteractionStart(mActivity, mRunningTaskInfo, false, |
| mTouchInteractionLog); |
| |
| // Inform the last progress in case we skipped before. |
| mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress); |
| } |
| |
| private void onFinishedTransitionToQuickScrub() { |
| if (mQuickScrubBlocked) { |
| return; |
| } |
| mLayoutListener.finish(); |
| mQuickScrubController.onFinishedTransitionToQuickScrub(); |
| |
| mRecentsView.animateUpRunningTaskIconScale(); |
| if (mQuickScrubController.isQuickSwitch()) { |
| // Adjust the running task so that it is centered and fills the screen. |
| TaskView runningTask = mRecentsView.getRunningTaskView(); |
| if (runningTask != null) { |
| float insetHeight = mDp.heightPx - mDp.getInsets().top - mDp.getInsets().bottom; |
| // Usually insetDiff will be 0, unless we allow apps to draw under the insets. In |
| // that case (insetDiff != 0), we need to center in the system-specified available |
| // height rather than launcher's inset height by adding half the insetDiff. |
| float insetDiff = mDp.availableHeightPx - insetHeight; |
| float topMargin = mActivity.getResources().getDimension( |
| R.dimen.task_thumbnail_half_top_margin); |
| runningTask.setTranslationY((insetDiff / 2 - topMargin) / mRecentsView.getScaleX()); |
| } |
| } |
| RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG); |
| } |
| |
| public void onQuickScrubProgress(float progress) { |
| mCurrentQuickScrubProgress = progress; |
| if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null |
| || mQuickScrubBlocked || !mStateCallback.hasStates(QUICK_SCRUB_START_UI_STATE)) { |
| return; |
| } |
| mQuickScrubController.onQuickScrubProgress(progress); |
| } |
| |
| public void onQuickScrubEnd() { |
| setStateOnUiThread(STATE_QUICK_SCRUB_END); |
| } |
| |
| private void switchToFinalAppAfterQuickScrub() { |
| if (mQuickScrubBlocked) { |
| return; |
| } |
| 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); |
| } |
| |
| 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); |
| } |
| |
| public void setGestureEndCallback(Runnable gestureEndCallback) { |
| mGestureEndCallback = gestureEndCallback; |
| } |
| |
| // Handling long swipe |
| private void onLongSwipeEnabledUi() { |
| mUiLongSwipeMode = true; |
| checkLongSwipeCanEnter(); |
| checkLongSwipeCanStart(); |
| } |
| |
| private void onLongSwipeDisabledUi() { |
| mUiLongSwipeMode = false; |
| mStateCallback.clearState(STATE_SCREENSHOT_VIEW_SHOWN); |
| |
| if (mLongSwipeController != null) { |
| mLongSwipeController.destroy(); |
| setTargetAlphaProvider((t, a1) -> a1); |
| |
| // Rebuild animations |
| buildAnimationController(); |
| } |
| } |
| |
| private void onLongSwipeDisplacementUpdated() { |
| if (!mUiLongSwipeMode || mLongSwipeController == null) { |
| return; |
| } |
| |
| mLongSwipeController.onMove(mLongSwipeDisplacement); |
| } |
| |
| private void checkLongSwipeCanEnter() { |
| if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_ENTER_STATE) |
| || !mActivityControlHelper.supportsLongSwipe(mActivity)) { |
| return; |
| } |
| |
| // We are entering long swipe mode, make sure the screen shot is captured. |
| mStateCallback.setState(STATE_CAPTURE_SCREENSHOT | STATE_SCREENSHOT_VIEW_SHOWN); |
| |
| } |
| |
| private void checkLongSwipeCanStart() { |
| if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_START_STATE) |
| || !mActivityControlHelper.supportsLongSwipe(mActivity)) { |
| return; |
| } |
| |
| RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; |
| if (targetSet == null) { |
| // This can happen when cancelAnimation comes on the background thread, while we are |
| // processing the long swipe on the UI thread. |
| return; |
| } |
| |
| mLongSwipeController = mActivityControlHelper.getLongSwipeController( |
| mActivity, mRunningTaskId); |
| onLongSwipeDisplacementUpdated(); |
| if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { |
| setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha); |
| } |
| } |
| |
| private void onLongSwipeGestureFinishUi(float velocity, boolean isFling, float velocityX) { |
| if (!mUiLongSwipeMode || mLongSwipeController == null) { |
| mUiLongSwipeMode = false; |
| handleNormalGestureEnd(velocity, isFling, velocityX); |
| return; |
| } |
| mUiLongSwipeMode = false; |
| finishCurrentTransitionToRecents(); |
| mLongSwipeController.end(velocity, isFling, |
| () -> setStateOnUiThread(STATE_HANDLER_INVALIDATED)); |
| |
| } |
| |
| private void setTargetAlphaProvider( |
| BiFunction<RemoteAnimationTargetCompat, Float, Float> provider) { |
| mClipAnimationHelper.setTaskAlphaCallback(provider); |
| updateFinalShift(); |
| } |
| |
| public void onAssistDataReceived(Bundle assistData) { |
| mAssistData = assistData; |
| setStateOnUiThread(STATE_ASSIST_DATA_RECEIVED); |
| } |
| |
| private void preloadAssistData() { |
| RecentsModel.INSTANCE.get(mContext).preloadAssistData(mRunningTaskId, mAssistData); |
| } |
| |
| public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha) { |
| if (!(app.isNotInRecents |
| || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME)) { |
| return 0; |
| } |
| return expectedAlpha; |
| } |
| } |