1 /* 2 * Copyright (C) 2020 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.interaction; 17 18 import static com.android.launcher3.anim.Interpolators.ACCEL; 19 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; 20 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION; 21 import static com.android.quickstep.AbsSwipeUpHandler.MAX_SWIPE_DURATION; 22 import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE; 23 import static com.android.quickstep.interaction.TutorialController.TutorialType.OVERVIEW_NAVIGATION_COMPLETE; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.AnimatorSet; 28 import android.animation.ValueAnimator; 29 import android.annotation.TargetApi; 30 import android.content.Context; 31 import android.graphics.Outline; 32 import android.graphics.PointF; 33 import android.graphics.Rect; 34 import android.graphics.RectF; 35 import android.os.Build; 36 import android.view.View; 37 import android.view.ViewOutlineProvider; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 42 import com.android.launcher3.DeviceProfile; 43 import com.android.launcher3.InvariantDeviceProfile; 44 import com.android.launcher3.Utilities; 45 import com.android.launcher3.anim.AnimatedFloat; 46 import com.android.launcher3.anim.AnimatorListeners; 47 import com.android.launcher3.anim.AnimatorPlaybackController; 48 import com.android.launcher3.anim.PendingAnimation; 49 import com.android.quickstep.GestureState; 50 import com.android.quickstep.OverviewComponentObserver; 51 import com.android.quickstep.RecentsAnimationDeviceState; 52 import com.android.quickstep.RemoteTargetGluer; 53 import com.android.quickstep.SwipeUpAnimationLogic; 54 import com.android.quickstep.SwipeUpAnimationLogic.RunningWindowAnim; 55 import com.android.quickstep.util.RecordingSurfaceTransaction; 56 import com.android.quickstep.util.RectFSpringAnim; 57 import com.android.quickstep.util.SurfaceTransaction; 58 import com.android.quickstep.util.SurfaceTransaction.MockProperties; 59 import com.android.quickstep.util.TransformParams; 60 61 @TargetApi(Build.VERSION_CODES.R) 62 abstract class SwipeUpGestureTutorialController extends TutorialController { 63 64 private static final int FAKE_PREVIOUS_TASK_MARGIN = Utilities.dpToPx(12); 65 66 protected static final long TASK_VIEW_END_ANIMATION_DURATION_MILLIS = 300; 67 private static final long HOME_SWIPE_ANIMATION_DURATION_MILLIS = 625; 68 private static final long OVERVIEW_SWIPE_ANIMATION_DURATION_MILLIS = 1000; 69 70 final ViewSwipeUpAnimation mTaskViewSwipeUpAnimation; 71 private float mFakeTaskViewRadius; 72 private final Rect mFakeTaskViewRect = new Rect(); 73 RunningWindowAnim mRunningWindowAnim; 74 private boolean mShowTasks = false; 75 private boolean mShowPreviousTasks = false; 76 77 private final AnimatorListenerAdapter mResetTaskView = new AnimatorListenerAdapter() { 78 @Override 79 public void onAnimationEnd(Animator animation) { 80 mFakeHotseatView.setVisibility(View.INVISIBLE); 81 mFakeIconView.setVisibility(View.INVISIBLE); 82 if (mTutorialFragment.getActivity() != null) { 83 int height = mTutorialFragment.getRootView().getFullscreenHeight(); 84 int width = mTutorialFragment.getRootView().getWidth(); 85 mFakeTaskViewRect.set(0, 0, width, height); 86 } 87 mFakeTaskViewRadius = 0; 88 mFakeTaskView.invalidateOutline(); 89 mFakeTaskView.setVisibility(View.VISIBLE); 90 mFakeTaskView.setAlpha(1); 91 mFakePreviousTaskView.setVisibility(View.INVISIBLE); 92 mFakePreviousTaskView.setAlpha(1); 93 mFakePreviousTaskView.setToSingleRowLayout(false); 94 mShowTasks = false; 95 mShowPreviousTasks = false; 96 mRunningWindowAnim = null; 97 } 98 }; 99 SwipeUpGestureTutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType)100 SwipeUpGestureTutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) { 101 super(tutorialFragment, tutorialType); 102 RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(mContext); 103 OverviewComponentObserver observer = new OverviewComponentObserver(mContext, deviceState); 104 mTaskViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState, 105 new GestureState(observer, -1)); 106 observer.onDestroy(); 107 deviceState.destroy(); 108 109 DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext) 110 .getDeviceProfile(mContext) 111 .copy(mContext); 112 mTaskViewSwipeUpAnimation.initDp(dp); 113 114 int height = mTutorialFragment.getRootView().getFullscreenHeight(); 115 int width = mTutorialFragment.getRootView().getWidth(); 116 mFakeTaskViewRect.set(0, 0, width, height); 117 mFakeTaskViewRadius = 0; 118 119 ViewOutlineProvider outlineProvider = new ViewOutlineProvider() { 120 @Override 121 public void getOutline(View view, Outline outline) { 122 outline.setRoundRect(mFakeTaskViewRect, mFakeTaskViewRadius); 123 } 124 }; 125 126 mFakeTaskView.setClipToOutline(true); 127 mFakeTaskView.setOutlineProvider(outlineProvider); 128 129 mFakePreviousTaskView.setClipToOutline(true); 130 mFakePreviousTaskView.setOutlineProvider(outlineProvider); 131 } 132 cancelRunningAnimation()133 private void cancelRunningAnimation() { 134 if (mRunningWindowAnim != null) { 135 mRunningWindowAnim.cancel(); 136 } 137 mRunningWindowAnim = null; 138 } 139 140 /** Fades the task view, optionally after animating to a fake Overview. */ fadeOutFakeTaskView(boolean toOverviewFirst, boolean reset, @Nullable Runnable onEndRunnable)141 void fadeOutFakeTaskView(boolean toOverviewFirst, boolean reset, 142 @Nullable Runnable onEndRunnable) { 143 cancelRunningAnimation(); 144 PendingAnimation anim = new PendingAnimation(300); 145 if (toOverviewFirst) { 146 anim.setFloat(mTaskViewSwipeUpAnimation 147 .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL); 148 anim.addListener(new AnimatorListenerAdapter() { 149 @Override 150 public void onAnimationEnd(Animator animation, boolean isReverse) { 151 PendingAnimation fadeAnim = 152 new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS); 153 if (reset) { 154 fadeAnim.setFloat(mTaskViewSwipeUpAnimation 155 .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL); 156 fadeAnim.addListener(mResetTaskView); 157 } else { 158 fadeAnim.setViewAlpha(mFakeTaskView, 0, ACCEL); 159 fadeAnim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL); 160 } 161 if (onEndRunnable != null) { 162 fadeAnim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable)); 163 } 164 AnimatorSet animset = fadeAnim.buildAnim(); 165 166 if (reset && mTutorialFragment.isLargeScreen()) { 167 animset.addListener(new AnimatorListenerAdapter() { 168 @Override 169 public void onAnimationStart(Animator animation) { 170 super.onAnimationStart(animation); 171 Animator multiRowAnimation = 172 mFakePreviousTaskView.createAnimationToMultiRowLayout(); 173 174 if (multiRowAnimation != null) { 175 multiRowAnimation.setDuration( 176 TASK_VIEW_END_ANIMATION_DURATION_MILLIS).start(); 177 } 178 } 179 }); 180 } 181 182 animset.setStartDelay(100); 183 animset.start(); 184 mRunningWindowAnim = RunningWindowAnim.wrap(animset); 185 } 186 }); 187 } else { 188 if (reset) { 189 anim.setFloat(mTaskViewSwipeUpAnimation 190 .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL); 191 anim.addListener(mResetTaskView); 192 } else { 193 anim.setViewAlpha(mFakeTaskView, 0, ACCEL); 194 anim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL); 195 } 196 if (onEndRunnable != null) { 197 anim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable)); 198 } 199 } 200 AnimatorSet animset = anim.buildAnim(); 201 hideFakeTaskbar(/* animateToHotseat= */ false); 202 animset.start(); 203 mRunningWindowAnim = RunningWindowAnim.wrap(animset); 204 } 205 resetFakeTaskViewFromOverview()206 void resetFakeTaskViewFromOverview() { 207 resetFakeTaskView(false, false); 208 } 209 resetFakeTaskView(boolean animateFromHome)210 void resetFakeTaskView(boolean animateFromHome) { 211 resetFakeTaskView(animateFromHome, true); 212 } 213 resetFakeTaskView(boolean animateFromHome, boolean animateTaskbar)214 void resetFakeTaskView(boolean animateFromHome, boolean animateTaskbar) { 215 mFakeTaskView.setVisibility(View.VISIBLE); 216 PendingAnimation anim = new PendingAnimation(300); 217 anim.setFloat(mTaskViewSwipeUpAnimation 218 .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL); 219 anim.setViewAlpha(mFakeTaskView, 1, ACCEL); 220 anim.addListener(mResetTaskView); 221 AnimatorSet animset = anim.buildAnim(); 222 if (animateTaskbar) { 223 showFakeTaskbar(animateFromHome); 224 } 225 animset.start(); 226 mRunningWindowAnim = RunningWindowAnim.wrap(animset); 227 } 228 animateFakeTaskViewHome(PointF finalVelocity, @Nullable Runnable onEndRunnable)229 void animateFakeTaskViewHome(PointF finalVelocity, @Nullable Runnable onEndRunnable) { 230 cancelRunningAnimation(); 231 hideFakeTaskbar(/* animateToHotseat= */ true); 232 mFakePreviousTaskView.setVisibility(View.INVISIBLE); 233 mFakeHotseatView.setVisibility(View.VISIBLE); 234 mShowPreviousTasks = false; 235 RectFSpringAnim rectAnim = 236 mTaskViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity); 237 // After home animation finishes, fade out and run onEndRunnable. 238 PendingAnimation fadeAnim = new PendingAnimation(300); 239 fadeAnim.setViewAlpha(mFakeIconView, 0, ACCEL); 240 if (onEndRunnable != null) { 241 fadeAnim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable)); 242 } 243 AnimatorSet animset = fadeAnim.buildAnim(); 244 rectAnim.addAnimatorListener(AnimatorListeners.forSuccessCallback(animset::start)); 245 mRunningWindowAnim = RunningWindowAnim.wrap(rectAnim); 246 } 247 248 @Override setNavBarGestureProgress(@ullable Float displacement)249 public void setNavBarGestureProgress(@Nullable Float displacement) { 250 if (isGestureCompleted()) { 251 return; 252 } 253 if (mTutorialType == HOME_NAVIGATION_COMPLETE 254 || mTutorialType == OVERVIEW_NAVIGATION_COMPLETE) { 255 mFakeTaskView.setVisibility(View.INVISIBLE); 256 mFakePreviousTaskView.setVisibility(View.INVISIBLE); 257 } else { 258 mShowTasks = true; 259 mFakeTaskView.setVisibility(View.VISIBLE); 260 if (mShowPreviousTasks) { 261 mFakePreviousTaskView.setVisibility(View.VISIBLE); 262 } 263 if (mRunningWindowAnim == null && displacement != null) { 264 mTaskViewSwipeUpAnimation.updateDisplacement(displacement); 265 } 266 } 267 } 268 269 @Override onMotionPaused(boolean unused)270 public void onMotionPaused(boolean unused) { 271 if (isGestureCompleted()) { 272 return; 273 } 274 if (mShowTasks) { 275 if (!mShowPreviousTasks) { 276 mFakePreviousTaskView.setTranslationX( 277 -(2 * mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN)); 278 mFakePreviousTaskView.animate() 279 .setDuration(300) 280 .translationX(-(mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN)) 281 .start(); 282 } 283 mShowPreviousTasks = true; 284 } 285 } 286 287 class ViewSwipeUpAnimation extends SwipeUpAnimationLogic { 288 ViewSwipeUpAnimation(Context context, RecentsAnimationDeviceState deviceState, GestureState gestureState)289 ViewSwipeUpAnimation(Context context, RecentsAnimationDeviceState deviceState, 290 GestureState gestureState) { 291 super(context, deviceState, gestureState); 292 mRemoteTargetHandles[0] = new RemoteTargetGluer.RemoteTargetHandle( 293 mRemoteTargetHandles[0].getTaskViewSimulator(), new FakeTransformParams()); 294 295 for (RemoteTargetGluer.RemoteTargetHandle handle 296 : mTargetGluer.getRemoteTargetHandles()) { 297 // Override home screen rotation preference so that home and overview animations 298 // work properly 299 handle.getTaskViewSimulator() 300 .getOrientationState() 301 .ignoreAllowHomeRotationPreference(); 302 } 303 } 304 initDp(DeviceProfile dp)305 void initDp(DeviceProfile dp) { 306 initTransitionEndpoints(dp); 307 mRemoteTargetHandles[0].getTaskViewSimulator().setPreviewBounds( 308 new Rect(0, 0, dp.widthPx, dp.heightPx), dp.getInsets()); 309 } 310 311 @Override updateFinalShift()312 public void updateFinalShift() { 313 mRemoteTargetHandles[0].getPlaybackController() 314 .setProgress(mCurrentShift.value, mDragLengthFactor); 315 mRemoteTargetHandles[0].getTaskViewSimulator().apply( 316 mRemoteTargetHandles[0].getTransformParams()); 317 } 318 getCurrentShift()319 AnimatedFloat getCurrentShift() { 320 return mCurrentShift; 321 } 322 handleSwipeUpToHome(PointF velocity)323 RectFSpringAnim handleSwipeUpToHome(PointF velocity) { 324 PointF velocityPxPerMs = new PointF(velocity.x, velocity.y); 325 float currentShift = mCurrentShift.value; 326 final float startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y 327 * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor); 328 float distanceToTravel = (1 - currentShift) * mTransitionDragLength; 329 330 // we want the page's snap velocity to approximately match the velocity at 331 // which the user flings, so we scale the duration by a value near to the 332 // derivative of the scroll interpolator at zero, ie. 2. 333 long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y)); 334 long duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); 335 HomeAnimationFactory homeAnimFactory = new HomeAnimationFactory() { 336 @Override 337 public AnimatorPlaybackController createActivityAnimationToHome() { 338 return AnimatorPlaybackController.wrap(new AnimatorSet(), duration); 339 } 340 341 @NonNull 342 @Override 343 public RectF getWindowTargetRect() { 344 int fakeHomeIconSizePx = Utilities.dpToPx(60); 345 int fakeHomeIconLeft = getHotseatIconLeft(); 346 int fakeHomeIconTop = getHotseatIconTop(); 347 return new RectF(fakeHomeIconLeft, fakeHomeIconTop, 348 fakeHomeIconLeft + fakeHomeIconSizePx, 349 fakeHomeIconTop + fakeHomeIconSizePx); 350 } 351 352 @Override 353 public void update(RectF rect, float progress, float radius) { 354 mFakeIconView.setVisibility(View.VISIBLE); 355 mFakeIconView.update(rect, progress, 356 1f - SHAPE_PROGRESS_DURATION /* shapeProgressStart */, 357 radius, 358 false, /* isOpening */ 359 mFakeIconView, mDp); 360 mFakeIconView.setAlpha(1); 361 mFakeTaskView.setAlpha(getWindowAlpha(progress)); 362 mFakePreviousTaskView.setAlpha(getWindowAlpha(progress)); 363 } 364 365 @Override 366 public void onCancel() { 367 mFakeIconView.setVisibility(View.INVISIBLE); 368 } 369 }; 370 RectFSpringAnim windowAnim = createWindowAnimationToHome(startShift, 371 homeAnimFactory)[0]; 372 windowAnim.start(mContext, mDp, velocityPxPerMs); 373 return windowAnim; 374 } 375 } 376 createFingerDotHomeSwipeAnimator(float fingerDotStartTranslationY)377 protected Animator createFingerDotHomeSwipeAnimator(float fingerDotStartTranslationY) { 378 Animator homeSwipeAnimator = createFingerDotSwipeUpAnimator(fingerDotStartTranslationY) 379 .setDuration(HOME_SWIPE_ANIMATION_DURATION_MILLIS); 380 381 homeSwipeAnimator.addListener(new AnimatorListenerAdapter() { 382 @Override 383 public void onAnimationEnd(Animator animation) { 384 super.onAnimationEnd(animation); 385 animateFakeTaskViewHome( 386 new PointF( 387 0f, 388 fingerDotStartTranslationY / HOME_SWIPE_ANIMATION_DURATION_MILLIS), 389 null); 390 } 391 }); 392 393 return homeSwipeAnimator; 394 } 395 createFingerDotOverviewSwipeAnimator(float fingerDotStartTranslationY)396 protected Animator createFingerDotOverviewSwipeAnimator(float fingerDotStartTranslationY) { 397 Animator overviewSwipeAnimator = createFingerDotSwipeUpAnimator(fingerDotStartTranslationY) 398 .setDuration(OVERVIEW_SWIPE_ANIMATION_DURATION_MILLIS); 399 400 overviewSwipeAnimator.addListener(new AnimatorListenerAdapter() { 401 @Override 402 public void onAnimationEnd(Animator animation) { 403 super.onAnimationEnd(animation); 404 mFakePreviousTaskView.setVisibility(View.VISIBLE); 405 onMotionPaused(true /*arbitrary value*/); 406 } 407 }); 408 409 return overviewSwipeAnimator; 410 } 411 412 createFingerDotSwipeUpAnimator(float fingerDotStartTranslationY)413 private Animator createFingerDotSwipeUpAnimator(float fingerDotStartTranslationY) { 414 ValueAnimator swipeAnimator = ValueAnimator.ofFloat(0f, 1f); 415 416 swipeAnimator.addUpdateListener(valueAnimator -> { 417 float gestureProgress = 418 -fingerDotStartTranslationY * valueAnimator.getAnimatedFraction(); 419 setNavBarGestureProgress(gestureProgress); 420 mFingerDotView.setTranslationY(fingerDotStartTranslationY + gestureProgress); 421 }); 422 423 return swipeAnimator; 424 } 425 426 private class FakeTransformParams extends TransformParams { 427 428 @Override createSurfaceParams(BuilderProxy proxy)429 public SurfaceTransaction createSurfaceParams(BuilderProxy proxy) { 430 RecordingSurfaceTransaction transaction = new RecordingSurfaceTransaction(); 431 proxy.onBuildTargetParams(transaction.mockProperties, null, this); 432 return transaction; 433 } 434 435 @Override applySurfaceParams(SurfaceTransaction params)436 public void applySurfaceParams(SurfaceTransaction params) { 437 if (params instanceof RecordingSurfaceTransaction) { 438 MockProperties p = ((RecordingSurfaceTransaction) params).mockProperties; 439 mFakeTaskView.setAnimationMatrix(p.matrix); 440 mFakePreviousTaskView.setAnimationMatrix(p.matrix); 441 mFakeTaskViewRect.set(p.windowCrop); 442 mFakeTaskViewRadius = p.cornerRadius; 443 mFakeTaskView.invalidateOutline(); 444 mFakePreviousTaskView.invalidateOutline(); 445 } 446 } 447 } 448 } 449