/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.quickstep; import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.anim.Interpolators.ACCEL_1_5; import static com.android.launcher3.anim.Interpolators.DEACCEL; import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION; import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG; import android.animation.Animator; import android.annotation.TargetApi; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.Intent; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.MotionEvent; import android.view.View; import android.view.animation.Interpolator; import androidx.annotation.UiThread; 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.graphics.RotationMode; import com.android.launcher3.util.VibratorWrapper; import com.android.launcher3.views.FloatingIconView; import com.android.quickstep.ActivityControlHelper.ActivityInitListener; import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory; import com.android.quickstep.SysUINavigationMode.Mode; import com.android.quickstep.inputconsumers.InputConsumer; import com.android.quickstep.util.ClipAnimationHelper; import com.android.quickstep.util.ClipAnimationHelper.TransformParams; import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.quickstep.util.SwipeAnimationTargetSet; import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; import java.util.function.Consumer; /** * Base class for swipe up handler with some utility methods */ @TargetApi(Build.VERSION_CODES.Q) public abstract class BaseSwipeUpHandler implements SwipeAnimationListener { private static final String TAG = "BaseSwipeUpHandler"; protected static final Rect TEMP_RECT = new Rect(); // Start resisting when swiping past this factor of mTransitionDragLength. private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f; // This is how far down we can scale down, where 0f is full screen and 1f is recents. private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f; private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL; // The distance needed to drag to reach the task size in recents. protected int mTransitionDragLength; // How much further we can drag past recents, as a factor of mTransitionDragLength. protected float mDragLengthFactor = 1; protected final Context mContext; protected final OverviewComponentObserver mOverviewComponentObserver; protected final ActivityControlHelper mActivityControlHelper; protected final RecentsModel mRecentsModel; protected final int mRunningTaskId; protected final ClipAnimationHelper mClipAnimationHelper; protected final TransformParams mTransformParams = new TransformParams(); protected final Mode mMode; // 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. protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); protected final ActivityInitListener mActivityInitListener; protected final RecentsAnimationWrapper mRecentsAnimationWrapper; protected T mActivity; protected Q mRecentsView; protected DeviceProfile mDp; private final int mPageSpacing; protected Runnable mGestureEndCallback; protected final Handler mMainThreadHandler = MAIN_EXECUTOR.getHandler(); protected MultiStateCallback mStateCallback; protected boolean mCanceled; protected int mFinishingRecentsAnimationForNewTaskId = -1; protected BaseSwipeUpHandler(Context context, OverviewComponentObserver overviewComponentObserver, RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId) { mContext = context; mOverviewComponentObserver = overviewComponentObserver; mActivityControlHelper = overviewComponentObserver.getActivityControlHelper(); mRecentsModel = recentsModel; mActivityInitListener = mActivityControlHelper.createActivityInitListener(this::onActivityInit); mRunningTaskId = runningTaskId; mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer, this::createNewInputProxyHandler); mMode = SysUINavigationMode.getMode(context); mClipAnimationHelper = new ClipAnimationHelper(context); mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing); initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext) .getDeviceProfile(mContext)); } protected void setStateOnUiThread(int stateFlag) { if (Looper.myLooper() == mMainThreadHandler.getLooper()) { mStateCallback.setState(stateFlag); } else { postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag)); } } protected void performHapticFeedback() { VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC); } public Consumer getRecentsViewDispatcher(RotationMode rotationMode) { return mRecentsView != null ? mRecentsView.getEventDispatcher(rotationMode) : null; } @UiThread public void updateDisplacement(float displacement) { // We are moving in the negative x/y direction displacement = -displacement; float shift; if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) { shift = mDragLengthFactor; } else { float translation = Math.max(displacement, 0); shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) { float pullbackProgress = Utilities.getProgress(shift, DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor); pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress); shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK); } } mCurrentShift.updateValue(shift); } public void setGestureEndCallback(Runnable gestureEndCallback) { mGestureEndCallback = gestureEndCallback; } public abstract Intent getLaunchIntent(); protected void linkRecentsViewScroll() { SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, applier -> { mTransformParams.setSyncTransactionApplier(applier); mRecentsAnimationWrapper.runOnInit(() -> mRecentsAnimationWrapper.targetSet.addDependentTransactionApplier(applier)); }); mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { if (moveWindowWithRecentsScroll()) { updateFinalShift(); } }); mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper); mRecentsView.setClipAnimationHelper(mClipAnimationHelper); } protected void startNewTask(int successStateFlag, Consumer resultCallback) { // Launch the task user scrolled to (mRecentsView.getNextPage()). if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { // We finish recents animation inside launchTask() when live tile is enabled. mRecentsView.getNextPageTaskView().launchTask(false /* animate */, true /* freezeTaskList */); } else { int taskId = mRecentsView.getNextPageTaskView().getTask().key.id; mFinishingRecentsAnimationForNewTaskId = taskId; mRecentsAnimationWrapper.finish(true /* toRecents */, () -> { if (!mCanceled) { TaskView nextTask = mRecentsView.getTaskView(taskId); if (nextTask != null) { nextTask.launchTask(false /* animate */, true /* freezeTaskList */, success -> { resultCallback.accept(success); if (!success) { mActivityControlHelper.onLaunchTaskFailed(mActivity); nextTask.notifyTaskLaunchFailed(TAG); } else { mActivityControlHelper.onLaunchTaskSuccess(mActivity); } }, mMainThreadHandler); } setStateOnUiThread(successStateFlag); } mCanceled = false; mFinishingRecentsAnimationForNewTaskId = -1; }); } TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true); } @Override public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) { DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext); final Rect overviewStackBounds; RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId); if (targetSet.minimizedHomeBounds != null && runningTaskTarget != null) { overviewStackBounds = mActivityControlHelper .getOverviewWindowBounds(targetSet.minimizedHomeBounds, runningTaskTarget); dp = dp.getMultiWindowProfile(mContext, new Point( overviewStackBounds.width(), overviewStackBounds.height())); } else { // If we are not in multi-window mode, home insets should be same as system insets. dp = dp.copy(mContext); overviewStackBounds = getStackBounds(dp); } dp.updateInsets(targetSet.homeContentInsets); dp.updateIsSeascape(mContext); if (runningTaskTarget != null) { mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget); } mClipAnimationHelper.prepareAnimation(dp, false /* isOpening */); initTransitionEndpoints(dp); mRecentsAnimationWrapper.setController(targetSet); } private Rect getStackBounds(DeviceProfile dp) { if (mActivity != null) { int loc[] = new int[2]; View rootView = mActivity.getRootView(); rootView.getLocationOnScreen(loc); return new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight()); } else { return new Rect(0, 0, dp.widthPx, dp.heightPx); } } protected void initTransitionEndpoints(DeviceProfile dp) { mDp = dp; mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength( dp, mContext, TEMP_RECT); if (!dp.isMultiWindowMode) { // When updating the target rect, also update the home bounds since the location on // screen of the launcher window may be stale (position is not updated until first // traversal after the window is resized). We only do this for non-multiwindow because // we otherwise use the minimized home bounds provided by the system. mClipAnimationHelper.updateHomeBounds(getStackBounds(dp)); } mClipAnimationHelper.updateTargetRect(TEMP_RECT); if (mMode == Mode.NO_BUTTON) { // We can drag all the way to the top of the screen. mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength; } } /** * Return true if the window should be translated horizontally if the recents view scrolls */ protected abstract boolean moveWindowWithRecentsScroll(); protected abstract boolean onActivityInit(final T activity, Boolean alreadyOnHome); /** * Called to create a input proxy for the running task */ @UiThread protected abstract InputConsumer createNewInputProxyHandler(); /** * Called when the value of {@link #mCurrentShift} changes */ @UiThread public abstract void updateFinalShift(); /** * Called when motion pause is detected */ public abstract void onMotionPauseChanged(boolean isPaused); @UiThread public void onGestureStarted() { } @UiThread public abstract void onGestureCancelled(); @UiThread public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos); public abstract void onConsumerAboutToBeSwitched(SwipeSharedState sharedState); public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { } public void initWhenReady() { // Preload the plan mRecentsModel.getTasks(null); mActivityInitListener.register(); } /** * Applies the transform on the recents animation without any additional null checks */ protected void applyTransformUnchecked() { float shift = mCurrentShift.value; float offsetX = mRecentsView == null ? 0 : mRecentsView.getScrollOffset(); float offsetScale = getTaskCurveScaleForOffsetX(offsetX, mClipAnimationHelper.getTargetRect().width()); mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale); mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, mTransformParams); } private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) { float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 + mPageSpacing; float interpolation = Math.min(1, offsetX / distanceToReachEdge); return TaskView.getCurveScaleForInterpolation(interpolation); } /** * Creates an animation that transforms the current app window into the home app. * @param startProgress The progress of {@link #mCurrentShift} to start the window from. * @param homeAnimationFactory The home animation factory. */ protected RectFSpringAnim createWindowAnimationToHome(float startProgress, HomeAnimationFactory homeAnimationFactory) { final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet, mTransformParams.setProgress(startProgress), false /* launcherOnTop */)); final RectF targetRect = homeAnimationFactory.getWindowTargetRect(); final View floatingView = homeAnimationFactory.getFloatingView(); final boolean isFloatingIconView = floatingView instanceof FloatingIconView; RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext.getResources()); if (isFloatingIconView) { FloatingIconView fiv = (FloatingIconView) floatingView; anim.addAnimatorListener(fiv); fiv.setOnTargetChangeListener(anim::onTargetPositionChanged); } AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome(); // End on a "round-enough" radius so that the shape reveal doesn't have to do too much // rounding at the end of the animation. float startRadius = mClipAnimationHelper.getCurrentCornerRadius(); float endRadius = startRect.width() / 6f; float startTransformProgress = mTransformParams.getProgress(); float endTransformProgress = 1; // We want the window alpha to be 0 once this threshold is met, so that the // FolderIconView can be seen morphing into the icon shape. final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f; anim.addOnUpdateListener(new RectFSpringAnim.OnUpdateListener() { // Alpha interpolates between [1, 0] between progress values [start, end] final float start = 0f; final float end = 0.85f; private float getWindowAlpha(float progress) { if (progress <= start) { return 1f; } if (progress >= end) { return 0f; } return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5); } @Override public void onUpdate(RectF currentRect, float progress) { homeAnim.setPlayFraction(progress); mTransformParams.setProgress( Utilities.mapRange(progress, startTransformProgress, endTransformProgress)) .setCurrentRectAndTargetAlpha(currentRect, getWindowAlpha(progress)); if (isFloatingIconView) { mTransformParams.setCornerRadius(endRadius * progress + startRadius * (1f - progress)); } mClipAnimationHelper.applyTransform(targetSet, mTransformParams, false /* launcherOnTop */); if (isFloatingIconView) { ((FloatingIconView) floatingView).update(currentRect, 1f, progress, windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(), false); } } @Override public void onCancel() { if (isFloatingIconView) { ((FloatingIconView) floatingView).fastFinish(); } } }); anim.addAnimatorListener(new AnimationSuccessListener() { @Override public void onAnimationStart(Animator animation) { homeAnim.dispatchOnStart(); } @Override public void onAnimationSuccess(Animator animator) { homeAnim.getAnimationPlayer().end(); } }); return anim; } public interface Factory { BaseSwipeUpHandler newHandler(RunningTaskInfo runningTask, long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask); } protected interface RunningWindowAnim { void end(); void cancel(); static RunningWindowAnim wrap(Animator animator) { return new RunningWindowAnim() { @Override public void end() { animator.end(); } @Override public void cancel() { animator.cancel(); } }; } static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) { return new RunningWindowAnim() { @Override public void end() { rectFSpringAnim.end(); } @Override public void cancel() { rectFSpringAnim.cancel(); } }; } } }