| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.quickstep; |
| |
| import static android.os.VibrationEffect.EFFECT_CLICK; |
| import static android.os.VibrationEffect.createPredefined; |
| |
| 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.Executors.UI_HELPER_EXECUTOR; |
| 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.os.VibrationEffect; |
| import android.os.Vibrator; |
| import android.provider.Settings; |
| 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.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<T extends BaseDraggingActivity, Q extends RecentsView> |
| 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<T> mActivityControlHelper; |
| protected final RecentsModel mRecentsModel; |
| protected final int mRunningTaskId; |
| |
| protected final ClipAnimationHelper mClipAnimationHelper; |
| protected final TransformParams mTransformParams = new TransformParams(); |
| |
| private final Vibrator mVibrator; |
| 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); |
| mVibrator = context.getSystemService(Vibrator.class); |
| 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() { |
| if (!mVibrator.hasVibrator()) { |
| return; |
| } |
| if (Settings.System.getInt( |
| mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 0) { |
| return; |
| } |
| |
| VibrationEffect effect = createPredefined(EFFECT_CLICK); |
| if (effect == null) { |
| return; |
| } |
| UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(effect)); |
| } |
| |
| public Consumer<MotionEvent> 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<Boolean> 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; |
| // 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(progress) |
| .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(); |
| } |
| }; |
| } |
| } |
| } |