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