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