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