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