AllAppsTransitionController.java revision 83fb07bb6cb8817f5c35ff63206a9fe2a1d3ebce
1package com.android.launcher3.allapps;
2
3import android.animation.Animator;
4import android.animation.AnimatorListenerAdapter;
5import android.animation.AnimatorSet;
6import android.animation.ObjectAnimator;
7import android.util.Log;
8import android.view.MotionEvent;
9import android.view.View;
10import android.view.animation.AccelerateInterpolator;
11import android.view.animation.DecelerateInterpolator;
12import android.view.animation.Interpolator;
13
14import com.android.launcher3.CellLayout;
15import com.android.launcher3.DeviceProfile;
16import com.android.launcher3.Hotseat;
17import com.android.launcher3.Launcher;
18import com.android.launcher3.LauncherAnimUtils;
19import com.android.launcher3.PagedView;
20import com.android.launcher3.R;
21import com.android.launcher3.Workspace;
22import com.android.launcher3.Workspace.Direction;
23import com.android.launcher3.util.TouchController;
24
25/**
26 * Handles AllApps view transition.
27 * 1) Slides all apps view using direct manipulation
28 * 2) When finger is released, animate to either top or bottom accordingly.
29 *
30 * Algorithm:
31 * If release velocity > THRES1, snap according to the direction of movement.
32 * If release velocity < THRES1, snap according to either top or bottom depending on whether it's
33 *     closer to top or closer to the page indicator.
34 */
35public class AllAppsTransitionController implements TouchController, VerticalPullDetector.Listener {
36
37    private static final String TAG = "AllAppsTrans";
38    private static final boolean DBG = false;
39
40    private final Interpolator mAccelInterpolator = new AccelerateInterpolator(2f);
41    private final Interpolator mDecelInterpolator = new DecelerateInterpolator(1f);
42
43    private static final float ANIMATION_DURATION = 1200;
44    public static final float ALL_APPS_FINAL_ALPHA = .9f;
45
46    private static final float PARALLAX_COEFFICIENT = .125f;
47
48    private AllAppsContainerView mAppsView;
49    private Workspace mWorkspace;
50    private Hotseat mHotseat;
51    private float mHotseatBackgroundAlpha;
52
53    private float mStatusBarHeight;
54
55    private final Launcher mLauncher;
56    private final VerticalPullDetector mDetector;
57
58    // Animation in this class is controlled by a single variable {@link mShiftCurrent}.
59    // Visually, it represents top y coordinate of the all apps container. Using the
60    // {@link mShiftRange} as the denominator, this fraction value ranges in [0, 1].
61    //
62    // When {@link mShiftCurrent} is 0, all apps container is pulled up.
63    // When {@link mShiftCurrent} is {@link mShirtRange}, all apps container is pulled down.
64    private float mShiftStart;      // [0, mShiftRange]
65    private float mShiftCurrent;    // [0, mShiftRange]
66    private float mShiftRange;      // changes depending on the orientation
67
68
69    private static final float RECATCH_REJECTION_FRACTION = .0875f;
70
71    private int mBezelSwipeUpHeight;
72    private long mAnimationDuration;
73
74
75    private AnimatorSet mCurrentAnimation;
76    private boolean mNoIntercept;
77
78    private boolean mLightStatusBar;
79
80    public AllAppsTransitionController(Launcher launcher) {
81        mLauncher = launcher;
82        mDetector = new VerticalPullDetector(launcher);
83        mDetector.setListener(this);
84        mBezelSwipeUpHeight = launcher.getResources().getDimensionPixelSize(
85                R.dimen.all_apps_bezel_swipe_height);
86    }
87
88    @Override
89    public boolean onInterceptTouchEvent(MotionEvent ev) {
90        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
91            mNoIntercept = false;
92            if (mLauncher.getWorkspace().isInOverviewMode() || mLauncher.isWidgetsViewVisible()) {
93                mNoIntercept = true;
94            } else if (mLauncher.isAllAppsVisible() &&
95                    !mAppsView.shouldContainerScroll(ev.getX(), ev.getY())) {
96                mNoIntercept = true;
97            } else if (!mLauncher.isAllAppsVisible() && !shouldPossiblyIntercept(ev)) {
98                mNoIntercept = true;
99            } else {
100                // Now figure out which direction scroll events the controller will start
101                // calling the callbacks.
102                int conditionsToReportScroll = 0;
103
104                if (mDetector.isRestingState()) {
105                    if (mLauncher.isAllAppsVisible()) {
106                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_DOWN;
107                    } else {
108                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_UP;
109                    }
110                } else {
111                    if (isInDisallowRecatchBottomZone()) {
112                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_UP;
113                    } else if (isInDisallowRecatchTopZone()) {
114                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_DOWN;
115                    } else {
116                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_ONLY;
117                    }
118                }
119                mDetector.setDetectableScrollConditions(conditionsToReportScroll);
120            }
121        }
122        if (mNoIntercept) {
123            return false;
124        }
125        mDetector.onTouchEvent(ev);
126        if (mDetector.isScrollingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) {
127            return false;
128        }
129        return mDetector.shouldIntercept();
130    }
131
132    private boolean shouldPossiblyIntercept(MotionEvent ev) {
133        DeviceProfile grid = mLauncher.getDeviceProfile();
134        if (mDetector.isRestingState()) {
135            if (grid.isVerticalBarLayout()) {
136                if (ev.getY() > mLauncher.getDeviceProfile().heightPx - mBezelSwipeUpHeight) {
137                    return true;
138                }
139            } else {
140                if ((mLauncher.getDragLayer().isEventOverHotseat(ev)
141                        || mLauncher.getDragLayer().isEventOverPageIndicator(ev))
142                        && !grid.isVerticalBarLayout()) {
143                    return true;
144                }
145            }
146            return false;
147        } else {
148            return true;
149        }
150    }
151
152    @Override
153    public boolean onTouchEvent(MotionEvent ev) {
154        return mDetector.onTouchEvent(ev);
155    }
156
157    private boolean isInDisallowRecatchTopZone() {
158        return mShiftCurrent / mShiftRange < RECATCH_REJECTION_FRACTION;
159    }
160
161    private boolean isInDisallowRecatchBottomZone() {
162        return mShiftCurrent / mShiftRange > 1 - RECATCH_REJECTION_FRACTION;
163    }
164
165    @Override
166    public void onScrollStart(boolean start) {
167        cancelAnimation();
168        mCurrentAnimation = LauncherAnimUtils.createAnimatorSet();
169        mShiftStart = mAppsView.getTranslationY();
170        preparePull(start);
171    }
172
173    @Override
174    public boolean onScroll(float displacement, float velocity) {
175        if (mAppsView == null) {
176            return false;   // early termination.
177        }
178        float progress = Math.min(Math.max(0, mShiftStart + displacement), mShiftRange);
179        setProgress(progress);
180        return true;
181    }
182
183    @Override
184    public void onScrollEnd(float velocity, boolean fling) {
185        if (mAppsView == null) {
186            return; // early termination.
187        }
188
189        if (fling) {
190            if (velocity < 0) {
191                calculateDuration(velocity, mAppsView.getTranslationY());
192
193                if (!mLauncher.isAllAppsVisible()) {
194                    mLauncher.showAppsView(true, true, false, false);
195                } else {
196                    animateToAllApps(mCurrentAnimation, mAnimationDuration, true);
197                }
198            } else {
199                calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
200                if (mLauncher.isAllAppsVisible()) {
201                    mLauncher.showWorkspace(true);
202                } else {
203                    animateToWorkspace(mCurrentAnimation, mAnimationDuration, true);
204                }
205            }
206            // snap to top or bottom using the release velocity
207        } else {
208            if (mAppsView.getTranslationY() > mShiftRange / 2) {
209                calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
210                if (mLauncher.isAllAppsVisible()) {
211                    mLauncher.showWorkspace(true);
212                } else {
213                    animateToWorkspace(mCurrentAnimation, mAnimationDuration, true);
214                }
215            } else {
216                calculateDuration(velocity, Math.abs(mAppsView.getTranslationY()));
217                if (!mLauncher.isAllAppsVisible()) {
218                    mLauncher.showAppsView(true, true, false, false);
219                } else {
220                    animateToAllApps(mCurrentAnimation, mAnimationDuration, true);
221                }
222
223            }
224        }
225    }
226    /**
227     * @param start {@code true} if start of new drag.
228     */
229    public void preparePull(boolean start) {
230        if (start) {
231            // Initialize values that should not change until #onScrollEnd
232            mStatusBarHeight = mLauncher.getDragLayer().getInsets().top;
233            mHotseat.setVisibility(View.VISIBLE);
234            mHotseat.bringToFront();
235
236            if (!mLauncher.isAllAppsVisible()) {
237                mLauncher.tryAndUpdatePredictedApps();
238
239                mHotseatBackgroundAlpha = mHotseat.getBackgroundDrawableAlpha() / 255f;
240                mHotseat.setBackgroundTransparent(true /* transparent */);
241                mAppsView.setVisibility(View.VISIBLE);
242                mAppsView.getContentView().setVisibility(View.VISIBLE);
243                mAppsView.getContentView().setBackground(null);
244                mAppsView.getRevealView().setVisibility(View.VISIBLE);
245                mAppsView.getRevealView().setAlpha(mHotseatBackgroundAlpha);
246            }
247        } else {
248            setProgress(mShiftCurrent);
249        }
250    }
251
252    private void updateLightStatusBar(float progress) {
253        boolean enable = (progress < mStatusBarHeight / 2);
254        // Do not modify status bar on landscape as all apps is not full bleed.
255        if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
256            return;
257        }
258        // Already set correctly
259        if (mLightStatusBar == enable) {
260            return;
261        }
262        int systemUiFlags = mLauncher.getWindow().getDecorView().getSystemUiVisibility();
263        // SYSTEM_UI_FLAG_LIGHT_NAV_BAR == SYSTEM_UI_FLAG_LIGHT_STATUS_BAR << 1
264        // Use proper constant once API is submitted.
265        if (enable) {
266            mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags
267                    | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
268                    | (View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR << 1));
269
270        } else {
271            mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags
272                    & ~(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
273                            |(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR << 1)));
274
275        }
276        mLightStatusBar = enable;
277    }
278
279    /**
280     * @param progress y value of the border between hotseat and all apps
281     */
282    public void setProgress(float progress) {
283        updateLightStatusBar(progress);
284        mShiftCurrent = progress;
285        float alpha = calcAlphaAllApps(progress);
286        float workspaceHotseatAlpha = 1 - alpha;
287
288        mAppsView.getRevealView().setAlpha(Math.min(ALL_APPS_FINAL_ALPHA, Math.max(mHotseatBackgroundAlpha,
289                mDecelInterpolator.getInterpolation(alpha))));
290        mAppsView.getContentView().setAlpha(alpha);
291        mAppsView.setTranslationY(progress);
292        mWorkspace.setWorkspaceTranslation(Direction.Y,
293                PARALLAX_COEFFICIENT * (-mShiftRange + progress),
294                mAccelInterpolator.getInterpolation(workspaceHotseatAlpha));
295        if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
296            mWorkspace.setHotseatTranslation(Direction.Y, -mShiftRange + progress,
297                    mAccelInterpolator.getInterpolation(workspaceHotseatAlpha));
298        } else {
299            mWorkspace.setHotseatTranslation(Direction.Y,
300                    PARALLAX_COEFFICIENT * (-mShiftRange + progress),
301                    mAccelInterpolator.getInterpolation(workspaceHotseatAlpha));
302        }
303    }
304
305    public float getProgress() {
306        return mShiftCurrent;
307    }
308
309    private float calcAlphaAllApps(float progress) {
310        return ((mShiftRange - progress)/ mShiftRange);
311    }
312
313    private void calculateDuration(float velocity, float disp) {
314        // TODO: make these values constants after tuning.
315        float velocityDivisor = Math.max(1.5f, Math.abs(0.5f * velocity));
316        float travelDistance = Math.max(0.2f, disp / mShiftRange);
317        mAnimationDuration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
318        if (DBG) {
319            Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", mAnimationDuration, velocity, disp));
320        }
321    }
322
323    public void animateToAllApps(AnimatorSet animationOut, long duration, boolean start) {
324        if (animationOut == null){
325            return;
326        }
327        if (mDetector.isRestingState()) {
328            preparePull(true);
329            mAnimationDuration = duration;
330            mShiftStart = mAppsView.getTranslationY();
331        }
332        final float fromAllAppsTop = mAppsView.getTranslationY();
333        final float toAllAppsTop = 0;
334
335        ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
336                fromAllAppsTop, toAllAppsTop);
337        driftAndAlpha.setDuration(mAnimationDuration);
338        driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator());
339        animationOut.play(driftAndAlpha);
340
341        animationOut.addListener(new AnimatorListenerAdapter() {
342            boolean canceled = false;
343            @Override
344            public void onAnimationCancel(Animator animation) {
345                canceled = true;
346            }
347            @Override
348            public void onAnimationEnd(Animator animation) {
349                if (canceled) {
350                    return;
351                } else {
352                    finishPullUp();
353                    cleanUpAnimation();
354                    mDetector.finishedScrolling();
355                }
356            }});
357        mCurrentAnimation = animationOut;
358        if (start) {
359            mCurrentAnimation.start();
360        }
361    }
362
363    public void animateToWorkspace(AnimatorSet animationOut, long duration, boolean start) {
364        if (animationOut == null){
365            return;
366        }
367        if(mDetector.isRestingState()) {
368            preparePull(true);
369            mAnimationDuration = duration;
370            mShiftStart = mAppsView.getTranslationY();
371        }
372        final float fromAllAppsTop = mAppsView.getTranslationY();
373        final float toAllAppsTop = mShiftRange;
374
375        ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
376                fromAllAppsTop, toAllAppsTop);
377        driftAndAlpha.setDuration(mAnimationDuration);
378        driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator());
379        animationOut.play(driftAndAlpha);
380
381        animationOut.addListener(new AnimatorListenerAdapter() {
382             boolean canceled = false;
383             @Override
384             public void onAnimationCancel(Animator animation) {
385                 canceled = true;
386                 setProgress(mShiftCurrent);
387             }
388
389             @Override
390             public void onAnimationEnd(Animator animation) {
391                 if (canceled) {
392                     return;
393                 } else {
394                     finishPullDown();
395                     cleanUpAnimation();
396                     mDetector.finishedScrolling();
397                 }
398             }});
399        mCurrentAnimation = animationOut;
400        if (start) {
401            mCurrentAnimation.start();
402        }
403    }
404
405    private void finishPullUp() {
406        mHotseat.setVisibility(View.INVISIBLE);
407        setProgress(0f);
408    }
409
410    public void finishPullDown() {
411        mAppsView.setVisibility(View.INVISIBLE);
412        mHotseat.setBackgroundTransparent(false /* transparent */);
413        mHotseat.setVisibility(View.VISIBLE);
414        setProgress(mShiftRange);
415    }
416
417    private void cancelAnimation() {
418        if (mCurrentAnimation != null) {
419            mCurrentAnimation.setDuration(0);
420            mCurrentAnimation.cancel();
421            mCurrentAnimation = null;
422        }
423    }
424
425    private void cleanUpAnimation() {
426        mCurrentAnimation = null;
427    }
428
429    public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) {
430        mAppsView = appsView;
431        mHotseat = hotseat;
432        mWorkspace = workspace;
433        mHotseat.addOnLayoutChangeListener(new View.OnLayoutChangeListener(){
434            public void onLayoutChange(View v, int left, int top, int right, int bottom,
435                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
436                if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
437                    mShiftRange = top;
438                } else {
439                    mShiftRange = bottom;
440                }
441                if (!mLauncher.isAllAppsVisible()) {
442                    setProgress(mShiftRange);
443                }
444            }
445        });
446    }
447}
448