1 package com.android.launcher3.allapps;
2 
3 import android.animation.Animator;
4 import android.animation.AnimatorInflater;
5 import android.animation.AnimatorListenerAdapter;
6 import android.animation.AnimatorSet;
7 import android.animation.ArgbEvaluator;
8 import android.animation.ObjectAnimator;
9 import android.graphics.Color;
10 import android.support.v4.graphics.ColorUtils;
11 import android.support.v4.view.animation.FastOutSlowInInterpolator;
12 import android.view.MotionEvent;
13 import android.view.View;
14 import android.view.animation.AccelerateInterpolator;
15 import android.view.animation.DecelerateInterpolator;
16 import android.view.animation.Interpolator;
17 
18 import com.android.launcher3.AbstractFloatingView;
19 import com.android.launcher3.Hotseat;
20 import com.android.launcher3.Launcher;
21 import com.android.launcher3.LauncherAnimUtils;
22 import com.android.launcher3.R;
23 import com.android.launcher3.Utilities;
24 import com.android.launcher3.Workspace;
25 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
26 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
27 import com.android.launcher3.util.Themes;
28 import com.android.launcher3.util.TouchController;
29 
30 /**
31  * Handles AllApps view transition.
32  * 1) Slides all apps view using direct manipulation
33  * 2) When finger is released, animate to either top or bottom accordingly.
34  * <p/>
35  * Algorithm:
36  * If release velocity > THRES1, snap according to the direction of movement.
37  * If release velocity < THRES1, snap according to either top or bottom depending on whether it's
38  * closer to top or closer to the page indicator.
39  */
40 public class AllAppsTransitionController implements TouchController, VerticalPullDetector.Listener,
41         View.OnLayoutChangeListener {
42 
43     private static final String TAG = "AllAppsTrans";
44     private static final boolean DBG = false;
45 
46     private final Interpolator mAccelInterpolator = new AccelerateInterpolator(2f);
47     private final Interpolator mDecelInterpolator = new DecelerateInterpolator(3f);
48     private final Interpolator mFastOutSlowInInterpolator = new FastOutSlowInInterpolator();
49     private final VerticalPullDetector.ScrollInterpolator mScrollInterpolator
50             = new VerticalPullDetector.ScrollInterpolator();
51 
52     private static final float PARALLAX_COEFFICIENT = .125f;
53     private static final int SINGLE_FRAME_MS = 16;
54 
55     private AllAppsContainerView mAppsView;
56     private int mAllAppsBackgroundColor;
57     private Workspace mWorkspace;
58     private Hotseat mHotseat;
59     private int mHotseatBackgroundColor;
60 
61     private AllAppsCaretController mCaretController;
62 
63     private float mStatusBarHeight;
64 
65     private final Launcher mLauncher;
66     private final VerticalPullDetector mDetector;
67     private final ArgbEvaluator mEvaluator;
68 
69     // Animation in this class is controlled by a single variable {@link mProgress}.
70     // Visually, it represents top y coordinate of the all apps container if multiplied with
71     // {@link mShiftRange}.
72 
73     // When {@link mProgress} is 0, all apps container is pulled up.
74     // When {@link mProgress} is 1, all apps container is pulled down.
75     private float mShiftStart;      // [0, mShiftRange]
76     private float mShiftRange;      // changes depending on the orientation
77     private float mProgress;        // [0, 1], mShiftRange * mProgress = shiftCurrent
78 
79     // Velocity of the container. Unit is in px/ms.
80     private float mContainerVelocity;
81 
82     private static final float DEFAULT_SHIFT_RANGE = 10;
83 
84     private static final float RECATCH_REJECTION_FRACTION = .0875f;
85 
86     private long mAnimationDuration;
87 
88     private AnimatorSet mCurrentAnimation;
89     private boolean mNoIntercept;
90 
91     // Used in discovery bounce animation to provide the transition without workspace changing.
92     private boolean mIsTranslateWithoutWorkspace = false;
93     private AnimatorSet mDiscoBounceAnimation;
94 
AllAppsTransitionController(Launcher l)95     public AllAppsTransitionController(Launcher l) {
96         mLauncher = l;
97         mDetector = new VerticalPullDetector(l);
98         mDetector.setListener(this);
99         mShiftRange = DEFAULT_SHIFT_RANGE;
100         mProgress = 1f;
101 
102         mEvaluator = new ArgbEvaluator();
103         mAllAppsBackgroundColor = Themes.getAttrColor(l, android.R.attr.colorPrimary);
104     }
105 
106     @Override
onControllerInterceptTouchEvent(MotionEvent ev)107     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
108         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
109             mNoIntercept = false;
110             if (!mLauncher.isAllAppsVisible() && mLauncher.getWorkspace().workspaceInModalState()) {
111                 mNoIntercept = true;
112             } else if (mLauncher.isAllAppsVisible() &&
113                     !mAppsView.shouldContainerScroll(ev)) {
114                 mNoIntercept = true;
115             } else if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
116                 mNoIntercept = true;
117             } else {
118                 // Now figure out which direction scroll events the controller will start
119                 // calling the callbacks.
120                 int directionsToDetectScroll = 0;
121                 boolean ignoreSlopWhenSettling = false;
122 
123                 if (mDetector.isIdleState()) {
124                     if (mLauncher.isAllAppsVisible()) {
125                         directionsToDetectScroll |= VerticalPullDetector.DIRECTION_DOWN;
126                     } else {
127                         directionsToDetectScroll |= VerticalPullDetector.DIRECTION_UP;
128                     }
129                 } else {
130                     if (isInDisallowRecatchBottomZone()) {
131                         directionsToDetectScroll |= VerticalPullDetector.DIRECTION_UP;
132                     } else if (isInDisallowRecatchTopZone()) {
133                         directionsToDetectScroll |= VerticalPullDetector.DIRECTION_DOWN;
134                     } else {
135                         directionsToDetectScroll |= VerticalPullDetector.DIRECTION_BOTH;
136                         ignoreSlopWhenSettling = true;
137                     }
138                 }
139                 mDetector.setDetectableScrollConditions(directionsToDetectScroll,
140                         ignoreSlopWhenSettling);
141             }
142         }
143 
144         if (mNoIntercept) {
145             return false;
146         }
147         mDetector.onTouchEvent(ev);
148         if (mDetector.isSettlingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) {
149             return false;
150         }
151         return mDetector.isDraggingOrSettling();
152     }
153 
154     @Override
onControllerTouchEvent(MotionEvent ev)155     public boolean onControllerTouchEvent(MotionEvent ev) {
156         return mDetector.onTouchEvent(ev);
157     }
158 
isInDisallowRecatchTopZone()159     private boolean isInDisallowRecatchTopZone() {
160         return mProgress < RECATCH_REJECTION_FRACTION;
161     }
162 
isInDisallowRecatchBottomZone()163     private boolean isInDisallowRecatchBottomZone() {
164         return mProgress > 1 - RECATCH_REJECTION_FRACTION;
165     }
166 
167     @Override
onDragStart(boolean start)168     public void onDragStart(boolean start) {
169         mCaretController.onDragStart();
170         cancelAnimation();
171         mCurrentAnimation = LauncherAnimUtils.createAnimatorSet();
172         mShiftStart = mAppsView.getTranslationY();
173         preparePull(start);
174     }
175 
176     @Override
onDrag(float displacement, float velocity)177     public boolean onDrag(float displacement, float velocity) {
178         if (mAppsView == null) {
179             return false;   // early termination.
180         }
181 
182         mContainerVelocity = velocity;
183 
184         float shift = Math.min(Math.max(0, mShiftStart + displacement), mShiftRange);
185         setProgress(shift / mShiftRange);
186 
187         return true;
188     }
189 
190     @Override
onDragEnd(float velocity, boolean fling)191     public void onDragEnd(float velocity, boolean fling) {
192         if (mAppsView == null) {
193             return; // early termination.
194         }
195 
196         if (fling) {
197             if (velocity < 0) {
198                 calculateDuration(velocity, mAppsView.getTranslationY());
199 
200                 if (!mLauncher.isAllAppsVisible()) {
201                     mLauncher.getUserEventDispatcher().logActionOnContainer(
202                             Action.Touch.FLING,
203                             Action.Direction.UP,
204                             ContainerType.HOTSEAT);
205                 }
206                 mLauncher.showAppsView(true /* animated */,
207                         false /* updatePredictedApps */,
208                         false /* focusSearchBar */);
209             } else {
210                 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
211                 mLauncher.showWorkspace(true);
212             }
213             // snap to top or bottom using the release velocity
214         } else {
215             if (mAppsView.getTranslationY() > mShiftRange / 2) {
216                 calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
217                 mLauncher.showWorkspace(true);
218             } else {
219                 calculateDuration(velocity, Math.abs(mAppsView.getTranslationY()));
220                 if (!mLauncher.isAllAppsVisible()) {
221                     mLauncher.getUserEventDispatcher().logActionOnContainer(
222                             Action.Touch.SWIPE,
223                             Action.Direction.UP,
224                             ContainerType.HOTSEAT);
225                 }
226                 mLauncher.showAppsView(true, /* animated */
227                         false /* updatePredictedApps */,
228                         false /* focusSearchBar */);
229             }
230         }
231     }
232 
isTransitioning()233     public boolean isTransitioning() {
234         return mDetector.isDraggingOrSettling();
235     }
236 
237     /**
238      * @param start {@code true} if start of new drag.
239      */
preparePull(boolean start)240     public void preparePull(boolean start) {
241         if (start) {
242             // Initialize values that should not change until #onDragEnd
243             mStatusBarHeight = mLauncher.getDragLayer().getInsets().top;
244             mHotseat.setVisibility(View.VISIBLE);
245             mHotseatBackgroundColor = mHotseat.getBackgroundDrawableColor();
246             mHotseat.setBackgroundTransparent(true /* transparent */);
247             if (!mLauncher.isAllAppsVisible()) {
248                 mLauncher.tryAndUpdatePredictedApps();
249                 mAppsView.setVisibility(View.VISIBLE);
250                 mAppsView.setRevealDrawableColor(mHotseatBackgroundColor);
251             }
252         }
253     }
254 
updateLightStatusBar(float shift)255     private void updateLightStatusBar(float shift) {
256         // Do not modify status bar on landscape as all apps is not full bleed.
257         if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
258             return;
259         }
260         // Use a light status bar (dark icons) if all apps is behind at least half of the status
261         // bar. If the status bar is already light due to wallpaper extraction, keep it that way.
262         boolean forceLight = shift <= mStatusBarHeight / 2;
263         mLauncher.activateLightSystemBars(forceLight, true /* statusBar */, true /* navBar */);
264     }
265 
266     /**
267      * @param progress       value between 0 and 1, 0 shows all apps and 1 shows workspace
268      */
setProgress(float progress)269     public void setProgress(float progress) {
270         float shiftPrevious = mProgress * mShiftRange;
271         mProgress = progress;
272         float shiftCurrent = progress * mShiftRange;
273 
274         float workspaceHotseatAlpha = Utilities.boundToRange(progress, 0f, 1f);
275         float alpha = 1 - workspaceHotseatAlpha;
276         float interpolation = mAccelInterpolator.getInterpolation(workspaceHotseatAlpha);
277 
278         int color = (Integer) mEvaluator.evaluate(mDecelInterpolator.getInterpolation(alpha),
279                 mHotseatBackgroundColor, mAllAppsBackgroundColor);
280         int bgAlpha = Color.alpha((int) mEvaluator.evaluate(alpha,
281                 mHotseatBackgroundColor, mAllAppsBackgroundColor));
282 
283         mAppsView.setRevealDrawableColor(ColorUtils.setAlphaComponent(color, bgAlpha));
284         mAppsView.getContentView().setAlpha(alpha);
285         mAppsView.setTranslationY(shiftCurrent);
286 
287         if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
288             mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y, -mShiftRange + shiftCurrent,
289                     interpolation);
290         } else {
291             mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y,
292                     PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent),
293                     interpolation);
294         }
295 
296         if (mIsTranslateWithoutWorkspace) {
297             return;
298         }
299         mWorkspace.setWorkspaceYTranslationAndAlpha(
300                 PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent), interpolation);
301 
302         if (!mDetector.isDraggingState()) {
303             mContainerVelocity = mDetector.computeVelocity(shiftCurrent - shiftPrevious,
304                     System.currentTimeMillis());
305         }
306 
307         mCaretController.updateCaret(progress, mContainerVelocity, mDetector.isDraggingState());
308         updateLightStatusBar(shiftCurrent);
309     }
310 
getProgress()311     public float getProgress() {
312         return mProgress;
313     }
314 
calculateDuration(float velocity, float disp)315     private void calculateDuration(float velocity, float disp) {
316         mAnimationDuration = mDetector.calculateDuration(velocity, disp / mShiftRange);
317     }
318 
animateToAllApps(AnimatorSet animationOut, long duration)319     public boolean animateToAllApps(AnimatorSet animationOut, long duration) {
320         boolean shouldPost = true;
321         if (animationOut == null) {
322             return shouldPost;
323         }
324         Interpolator interpolator;
325         if (mDetector.isIdleState()) {
326             preparePull(true);
327             mAnimationDuration = duration;
328             mShiftStart = mAppsView.getTranslationY();
329             interpolator = mFastOutSlowInInterpolator;
330         } else {
331             mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity));
332             interpolator = mScrollInterpolator;
333             float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange;
334             if (nextFrameProgress >= 0f) {
335                 mProgress = nextFrameProgress;
336             }
337             shouldPost = false;
338         }
339 
340         ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
341                 mProgress, 0f);
342         driftAndAlpha.setDuration(mAnimationDuration);
343         driftAndAlpha.setInterpolator(interpolator);
344         animationOut.play(driftAndAlpha);
345 
346         animationOut.addListener(new AnimatorListenerAdapter() {
347             boolean canceled = false;
348 
349             @Override
350             public void onAnimationCancel(Animator animation) {
351                 canceled = true;
352             }
353 
354             @Override
355             public void onAnimationEnd(Animator animation) {
356                 if (canceled) {
357                     return;
358                 } else {
359                     finishPullUp();
360                     cleanUpAnimation();
361                     mDetector.finishedScrolling();
362                 }
363             }
364         });
365         mCurrentAnimation = animationOut;
366         return shouldPost;
367     }
368 
showDiscoveryBounce()369     public void showDiscoveryBounce() {
370         // cancel existing animation in case user locked and unlocked at a super human speed.
371         cancelDiscoveryAnimation();
372 
373         // assumption is that this variable is always null
374         mDiscoBounceAnimation = (AnimatorSet) AnimatorInflater.loadAnimator(mLauncher,
375                 R.anim.discovery_bounce);
376         mDiscoBounceAnimation.addListener(new AnimatorListenerAdapter() {
377             @Override
378             public void onAnimationStart(Animator animator) {
379                 mIsTranslateWithoutWorkspace = true;
380                 preparePull(true);
381             }
382 
383             @Override
384             public void onAnimationEnd(Animator animator) {
385                 finishPullDown();
386                 mDiscoBounceAnimation = null;
387                 mIsTranslateWithoutWorkspace = false;
388             }
389         });
390         mDiscoBounceAnimation.setTarget(this);
391         mAppsView.post(new Runnable() {
392             @Override
393             public void run() {
394                 if (mDiscoBounceAnimation == null) {
395                     return;
396                 }
397                 mDiscoBounceAnimation.start();
398             }
399         });
400     }
401 
animateToWorkspace(AnimatorSet animationOut, long duration)402     public boolean animateToWorkspace(AnimatorSet animationOut, long duration) {
403         boolean shouldPost = true;
404         if (animationOut == null) {
405             return shouldPost;
406         }
407         Interpolator interpolator;
408         if (mDetector.isIdleState()) {
409             preparePull(true);
410             mAnimationDuration = duration;
411             mShiftStart = mAppsView.getTranslationY();
412             interpolator = mFastOutSlowInInterpolator;
413         } else {
414             mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity));
415             interpolator = mScrollInterpolator;
416             float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange;
417             if (nextFrameProgress <= 1f) {
418                 mProgress = nextFrameProgress;
419             }
420             shouldPost = false;
421         }
422 
423         ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
424                 mProgress, 1f);
425         driftAndAlpha.setDuration(mAnimationDuration);
426         driftAndAlpha.setInterpolator(interpolator);
427         animationOut.play(driftAndAlpha);
428 
429         animationOut.addListener(new AnimatorListenerAdapter() {
430             boolean canceled = false;
431 
432             @Override
433             public void onAnimationCancel(Animator animation) {
434                 canceled = true;
435             }
436 
437             @Override
438             public void onAnimationEnd(Animator animation) {
439                 if (canceled) {
440                     return;
441                 } else {
442                     finishPullDown();
443                     cleanUpAnimation();
444                     mDetector.finishedScrolling();
445                 }
446             }
447         });
448         mCurrentAnimation = animationOut;
449         return shouldPost;
450     }
451 
finishPullUp()452     public void finishPullUp() {
453         mHotseat.setVisibility(View.INVISIBLE);
454         setProgress(0f);
455     }
456 
finishPullDown()457     public void finishPullDown() {
458         mAppsView.setVisibility(View.INVISIBLE);
459         mHotseat.setBackgroundTransparent(false /* transparent */);
460         mHotseat.setVisibility(View.VISIBLE);
461         mAppsView.reset();
462         setProgress(1f);
463     }
464 
cancelAnimation()465     private void cancelAnimation() {
466         if (mCurrentAnimation != null) {
467             mCurrentAnimation.cancel();
468             mCurrentAnimation = null;
469         }
470         cancelDiscoveryAnimation();
471     }
472 
cancelDiscoveryAnimation()473     public void cancelDiscoveryAnimation() {
474         if (mDiscoBounceAnimation == null) {
475             return;
476         }
477         mDiscoBounceAnimation.cancel();
478         mDiscoBounceAnimation = null;
479     }
480 
cleanUpAnimation()481     private void cleanUpAnimation() {
482         mCurrentAnimation = null;
483     }
484 
setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace)485     public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) {
486         mAppsView = appsView;
487         mHotseat = hotseat;
488         mWorkspace = workspace;
489         mHotseat.addOnLayoutChangeListener(this);
490         mHotseat.bringToFront();
491         mCaretController = new AllAppsCaretController(
492                 mWorkspace.getPageIndicator().getCaretDrawable(), mLauncher);
493     }
494 
495     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)496     public void onLayoutChange(View v, int left, int top, int right, int bottom,
497             int oldLeft, int oldTop, int oldRight, int oldBottom) {
498         if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
499             mShiftRange = top;
500         } else {
501             mShiftRange = bottom;
502         }
503         setProgress(mProgress);
504     }
505 
506 }
507