1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.quickstep; 17 18 import static com.android.launcher3.Utilities.postAsyncCallback; 19 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5; 20 import static com.android.launcher3.anim.Interpolators.DEACCEL; 21 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 22 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 23 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; 24 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION; 25 import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG; 26 27 import android.animation.Animator; 28 import android.annotation.TargetApi; 29 import android.app.ActivityManager.RunningTaskInfo; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.graphics.Point; 33 import android.graphics.PointF; 34 import android.graphics.Rect; 35 import android.graphics.RectF; 36 import android.os.Build; 37 import android.os.Handler; 38 import android.os.Looper; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.animation.Interpolator; 42 43 import androidx.annotation.UiThread; 44 45 import com.android.launcher3.BaseDraggingActivity; 46 import com.android.launcher3.DeviceProfile; 47 import com.android.launcher3.InvariantDeviceProfile; 48 import com.android.launcher3.R; 49 import com.android.launcher3.Utilities; 50 import com.android.launcher3.anim.AnimationSuccessListener; 51 import com.android.launcher3.anim.AnimatorPlaybackController; 52 import com.android.launcher3.graphics.RotationMode; 53 import com.android.launcher3.util.VibratorWrapper; 54 import com.android.launcher3.views.FloatingIconView; 55 import com.android.quickstep.ActivityControlHelper.ActivityInitListener; 56 import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory; 57 import com.android.quickstep.SysUINavigationMode.Mode; 58 import com.android.quickstep.inputconsumers.InputConsumer; 59 import com.android.quickstep.util.ClipAnimationHelper; 60 import com.android.quickstep.util.ClipAnimationHelper.TransformParams; 61 import com.android.quickstep.util.RectFSpringAnim; 62 import com.android.quickstep.util.RemoteAnimationTargetSet; 63 import com.android.quickstep.util.SwipeAnimationTargetSet; 64 import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener; 65 import com.android.quickstep.views.RecentsView; 66 import com.android.quickstep.views.TaskView; 67 import com.android.systemui.shared.system.InputConsumerController; 68 import com.android.systemui.shared.system.RemoteAnimationTargetCompat; 69 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat; 70 71 import java.util.function.Consumer; 72 73 /** 74 * Base class for swipe up handler with some utility methods 75 */ 76 @TargetApi(Build.VERSION_CODES.Q) 77 public abstract class BaseSwipeUpHandler<T extends BaseDraggingActivity, Q extends RecentsView> 78 implements SwipeAnimationListener { 79 80 private static final String TAG = "BaseSwipeUpHandler"; 81 protected static final Rect TEMP_RECT = new Rect(); 82 83 // Start resisting when swiping past this factor of mTransitionDragLength. 84 private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f; 85 // This is how far down we can scale down, where 0f is full screen and 1f is recents. 86 private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f; 87 private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL; 88 89 // The distance needed to drag to reach the task size in recents. 90 protected int mTransitionDragLength; 91 // How much further we can drag past recents, as a factor of mTransitionDragLength. 92 protected float mDragLengthFactor = 1; 93 94 protected final Context mContext; 95 protected final OverviewComponentObserver mOverviewComponentObserver; 96 protected final ActivityControlHelper<T> mActivityControlHelper; 97 protected final RecentsModel mRecentsModel; 98 protected final int mRunningTaskId; 99 100 protected final ClipAnimationHelper mClipAnimationHelper; 101 protected final TransformParams mTransformParams = new TransformParams(); 102 103 protected final Mode mMode; 104 105 // Shift in the range of [0, 1]. 106 // 0 => preview snapShot is completely visible, and hotseat is completely translated down 107 // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely 108 // visible. 109 protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); 110 111 protected final ActivityInitListener mActivityInitListener; 112 protected final RecentsAnimationWrapper mRecentsAnimationWrapper; 113 114 protected T mActivity; 115 protected Q mRecentsView; 116 protected DeviceProfile mDp; 117 private final int mPageSpacing; 118 119 protected Runnable mGestureEndCallback; 120 121 protected final Handler mMainThreadHandler = MAIN_EXECUTOR.getHandler(); 122 protected MultiStateCallback mStateCallback; 123 124 protected boolean mCanceled; 125 protected int mFinishingRecentsAnimationForNewTaskId = -1; 126 BaseSwipeUpHandler(Context context, OverviewComponentObserver overviewComponentObserver, RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId)127 protected BaseSwipeUpHandler(Context context, 128 OverviewComponentObserver overviewComponentObserver, 129 RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId) { 130 mContext = context; 131 mOverviewComponentObserver = overviewComponentObserver; 132 mActivityControlHelper = overviewComponentObserver.getActivityControlHelper(); 133 mRecentsModel = recentsModel; 134 mActivityInitListener = 135 mActivityControlHelper.createActivityInitListener(this::onActivityInit); 136 mRunningTaskId = runningTaskId; 137 mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer, 138 this::createNewInputProxyHandler); 139 mMode = SysUINavigationMode.getMode(context); 140 141 mClipAnimationHelper = new ClipAnimationHelper(context); 142 mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing); 143 initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext) 144 .getDeviceProfile(mContext)); 145 } 146 setStateOnUiThread(int stateFlag)147 protected void setStateOnUiThread(int stateFlag) { 148 if (Looper.myLooper() == mMainThreadHandler.getLooper()) { 149 mStateCallback.setState(stateFlag); 150 } else { 151 postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag)); 152 } 153 } 154 performHapticFeedback()155 protected void performHapticFeedback() { 156 VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC); 157 } 158 getRecentsViewDispatcher(RotationMode rotationMode)159 public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) { 160 return mRecentsView != null ? mRecentsView.getEventDispatcher(rotationMode) : null; 161 } 162 163 @UiThread updateDisplacement(float displacement)164 public void updateDisplacement(float displacement) { 165 // We are moving in the negative x/y direction 166 displacement = -displacement; 167 float shift; 168 if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) { 169 shift = mDragLengthFactor; 170 } else { 171 float translation = Math.max(displacement, 0); 172 shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; 173 if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) { 174 float pullbackProgress = Utilities.getProgress(shift, 175 DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor); 176 pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress); 177 shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress 178 * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK); 179 } 180 } 181 182 mCurrentShift.updateValue(shift); 183 } 184 setGestureEndCallback(Runnable gestureEndCallback)185 public void setGestureEndCallback(Runnable gestureEndCallback) { 186 mGestureEndCallback = gestureEndCallback; 187 } 188 getLaunchIntent()189 public abstract Intent getLaunchIntent(); 190 linkRecentsViewScroll()191 protected void linkRecentsViewScroll() { 192 SyncRtSurfaceTransactionApplierCompat.create(mRecentsView, applier -> { 193 mTransformParams.setSyncTransactionApplier(applier); 194 mRecentsAnimationWrapper.runOnInit(() -> 195 mRecentsAnimationWrapper.targetSet.addDependentTransactionApplier(applier)); 196 }); 197 198 mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { 199 if (moveWindowWithRecentsScroll()) { 200 updateFinalShift(); 201 } 202 }); 203 mRecentsView.setRecentsAnimationWrapper(mRecentsAnimationWrapper); 204 mRecentsView.setClipAnimationHelper(mClipAnimationHelper); 205 } 206 startNewTask(int successStateFlag, Consumer<Boolean> resultCallback)207 protected void startNewTask(int successStateFlag, Consumer<Boolean> resultCallback) { 208 // Launch the task user scrolled to (mRecentsView.getNextPage()). 209 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 210 // We finish recents animation inside launchTask() when live tile is enabled. 211 mRecentsView.getNextPageTaskView().launchTask(false /* animate */, 212 true /* freezeTaskList */); 213 } else { 214 int taskId = mRecentsView.getNextPageTaskView().getTask().key.id; 215 mFinishingRecentsAnimationForNewTaskId = taskId; 216 mRecentsAnimationWrapper.finish(true /* toRecents */, () -> { 217 if (!mCanceled) { 218 TaskView nextTask = mRecentsView.getTaskView(taskId); 219 if (nextTask != null) { 220 nextTask.launchTask(false /* animate */, true /* freezeTaskList */, 221 success -> { 222 resultCallback.accept(success); 223 if (!success) { 224 mActivityControlHelper.onLaunchTaskFailed(mActivity); 225 nextTask.notifyTaskLaunchFailed(TAG); 226 } else { 227 mActivityControlHelper.onLaunchTaskSuccess(mActivity); 228 } 229 }, mMainThreadHandler); 230 } 231 setStateOnUiThread(successStateFlag); 232 } 233 mCanceled = false; 234 mFinishingRecentsAnimationForNewTaskId = -1; 235 }); 236 } 237 TOUCH_INTERACTION_LOG.addLog("finishRecentsAnimation", true); 238 } 239 240 @Override onRecentsAnimationStart(SwipeAnimationTargetSet targetSet)241 public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) { 242 DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext); 243 final Rect overviewStackBounds; 244 RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId); 245 246 if (targetSet.minimizedHomeBounds != null && runningTaskTarget != null) { 247 overviewStackBounds = mActivityControlHelper 248 .getOverviewWindowBounds(targetSet.minimizedHomeBounds, runningTaskTarget); 249 dp = dp.getMultiWindowProfile(mContext, new Point( 250 overviewStackBounds.width(), overviewStackBounds.height())); 251 } else { 252 // If we are not in multi-window mode, home insets should be same as system insets. 253 dp = dp.copy(mContext); 254 overviewStackBounds = getStackBounds(dp); 255 } 256 dp.updateInsets(targetSet.homeContentInsets); 257 dp.updateIsSeascape(mContext); 258 if (runningTaskTarget != null) { 259 mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget); 260 } 261 262 mClipAnimationHelper.prepareAnimation(dp, false /* isOpening */); 263 initTransitionEndpoints(dp); 264 265 mRecentsAnimationWrapper.setController(targetSet); 266 } 267 getStackBounds(DeviceProfile dp)268 private Rect getStackBounds(DeviceProfile dp) { 269 if (mActivity != null) { 270 int loc[] = new int[2]; 271 View rootView = mActivity.getRootView(); 272 rootView.getLocationOnScreen(loc); 273 return new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(), 274 loc[1] + rootView.getHeight()); 275 } else { 276 return new Rect(0, 0, dp.widthPx, dp.heightPx); 277 } 278 } 279 initTransitionEndpoints(DeviceProfile dp)280 protected void initTransitionEndpoints(DeviceProfile dp) { 281 mDp = dp; 282 283 mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength( 284 dp, mContext, TEMP_RECT); 285 if (!dp.isMultiWindowMode) { 286 // When updating the target rect, also update the home bounds since the location on 287 // screen of the launcher window may be stale (position is not updated until first 288 // traversal after the window is resized). We only do this for non-multiwindow because 289 // we otherwise use the minimized home bounds provided by the system. 290 mClipAnimationHelper.updateHomeBounds(getStackBounds(dp)); 291 } 292 mClipAnimationHelper.updateTargetRect(TEMP_RECT); 293 if (mMode == Mode.NO_BUTTON) { 294 // We can drag all the way to the top of the screen. 295 mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength; 296 } 297 } 298 299 /** 300 * Return true if the window should be translated horizontally if the recents view scrolls 301 */ moveWindowWithRecentsScroll()302 protected abstract boolean moveWindowWithRecentsScroll(); 303 onActivityInit(final T activity, Boolean alreadyOnHome)304 protected abstract boolean onActivityInit(final T activity, Boolean alreadyOnHome); 305 306 /** 307 * Called to create a input proxy for the running task 308 */ 309 @UiThread createNewInputProxyHandler()310 protected abstract InputConsumer createNewInputProxyHandler(); 311 312 /** 313 * Called when the value of {@link #mCurrentShift} changes 314 */ 315 @UiThread updateFinalShift()316 public abstract void updateFinalShift(); 317 318 /** 319 * Called when motion pause is detected 320 */ onMotionPauseChanged(boolean isPaused)321 public abstract void onMotionPauseChanged(boolean isPaused); 322 323 @UiThread onGestureStarted()324 public void onGestureStarted() { } 325 326 @UiThread onGestureCancelled()327 public abstract void onGestureCancelled(); 328 329 @UiThread onGestureEnded(float endVelocity, PointF velocity, PointF downPos)330 public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos); 331 onConsumerAboutToBeSwitched(SwipeSharedState sharedState)332 public abstract void onConsumerAboutToBeSwitched(SwipeSharedState sharedState); 333 setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask)334 public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { } 335 initWhenReady()336 public void initWhenReady() { 337 // Preload the plan 338 mRecentsModel.getTasks(null); 339 340 mActivityInitListener.register(); 341 } 342 343 /** 344 * Applies the transform on the recents animation without any additional null checks 345 */ applyTransformUnchecked()346 protected void applyTransformUnchecked() { 347 float shift = mCurrentShift.value; 348 float offsetX = mRecentsView == null ? 0 : mRecentsView.getScrollOffset(); 349 float offsetScale = getTaskCurveScaleForOffsetX(offsetX, 350 mClipAnimationHelper.getTargetRect().width()); 351 mTransformParams.setProgress(shift).setOffsetX(offsetX).setOffsetScale(offsetScale); 352 mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, 353 mTransformParams); 354 } 355 getTaskCurveScaleForOffsetX(float offsetX, float taskWidth)356 private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) { 357 float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 + mPageSpacing; 358 float interpolation = Math.min(1, offsetX / distanceToReachEdge); 359 return TaskView.getCurveScaleForInterpolation(interpolation); 360 } 361 362 /** 363 * Creates an animation that transforms the current app window into the home app. 364 * @param startProgress The progress of {@link #mCurrentShift} to start the window from. 365 * @param homeAnimationFactory The home animation factory. 366 */ createWindowAnimationToHome(float startProgress, HomeAnimationFactory homeAnimationFactory)367 protected RectFSpringAnim createWindowAnimationToHome(float startProgress, 368 HomeAnimationFactory homeAnimationFactory) { 369 final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; 370 final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet, 371 mTransformParams.setProgress(startProgress), false /* launcherOnTop */)); 372 final RectF targetRect = homeAnimationFactory.getWindowTargetRect(); 373 374 final View floatingView = homeAnimationFactory.getFloatingView(); 375 final boolean isFloatingIconView = floatingView instanceof FloatingIconView; 376 RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext.getResources()); 377 if (isFloatingIconView) { 378 FloatingIconView fiv = (FloatingIconView) floatingView; 379 anim.addAnimatorListener(fiv); 380 fiv.setOnTargetChangeListener(anim::onTargetPositionChanged); 381 } 382 383 AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome(); 384 385 // End on a "round-enough" radius so that the shape reveal doesn't have to do too much 386 // rounding at the end of the animation. 387 float startRadius = mClipAnimationHelper.getCurrentCornerRadius(); 388 float endRadius = startRect.width() / 6f; 389 390 float startTransformProgress = mTransformParams.getProgress(); 391 float endTransformProgress = 1; 392 393 // We want the window alpha to be 0 once this threshold is met, so that the 394 // FolderIconView can be seen morphing into the icon shape. 395 final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f; 396 anim.addOnUpdateListener(new RectFSpringAnim.OnUpdateListener() { 397 398 // Alpha interpolates between [1, 0] between progress values [start, end] 399 final float start = 0f; 400 final float end = 0.85f; 401 402 private float getWindowAlpha(float progress) { 403 if (progress <= start) { 404 return 1f; 405 } 406 if (progress >= end) { 407 return 0f; 408 } 409 return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5); 410 } 411 412 @Override 413 public void onUpdate(RectF currentRect, float progress) { 414 homeAnim.setPlayFraction(progress); 415 416 mTransformParams.setProgress( 417 Utilities.mapRange(progress, startTransformProgress, endTransformProgress)) 418 .setCurrentRectAndTargetAlpha(currentRect, getWindowAlpha(progress)); 419 if (isFloatingIconView) { 420 mTransformParams.setCornerRadius(endRadius * progress + startRadius 421 * (1f - progress)); 422 } 423 mClipAnimationHelper.applyTransform(targetSet, mTransformParams, 424 false /* launcherOnTop */); 425 426 if (isFloatingIconView) { 427 ((FloatingIconView) floatingView).update(currentRect, 1f, progress, 428 windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(), 429 false); 430 } 431 } 432 433 @Override 434 public void onCancel() { 435 if (isFloatingIconView) { 436 ((FloatingIconView) floatingView).fastFinish(); 437 } 438 } 439 }); 440 anim.addAnimatorListener(new AnimationSuccessListener() { 441 @Override 442 public void onAnimationStart(Animator animation) { 443 homeAnim.dispatchOnStart(); 444 } 445 446 @Override 447 public void onAnimationSuccess(Animator animator) { 448 homeAnim.getAnimationPlayer().end(); 449 } 450 }); 451 return anim; 452 } 453 454 public interface Factory { 455 newHandler(RunningTaskInfo runningTask, long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask)456 BaseSwipeUpHandler newHandler(RunningTaskInfo runningTask, 457 long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask); 458 } 459 460 protected interface RunningWindowAnim { end()461 void end(); 462 cancel()463 void cancel(); 464 wrap(Animator animator)465 static RunningWindowAnim wrap(Animator animator) { 466 return new RunningWindowAnim() { 467 @Override 468 public void end() { 469 animator.end(); 470 } 471 472 @Override 473 public void cancel() { 474 animator.cancel(); 475 } 476 }; 477 } 478 wrap(RectFSpringAnim rectFSpringAnim)479 static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) { 480 return new RunningWindowAnim() { 481 @Override 482 public void end() { 483 rectFSpringAnim.end(); 484 } 485 486 @Override 487 public void cancel() { 488 rectFSpringAnim.cancel(); 489 } 490 }; 491 } 492 } 493 } 494