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