AllAppsTransitionController.java revision 4325a56f5359a83164692ae6109d6463f792bf13
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                // This controller is now going to handle all the touch events.
101                // First cancel any animation that is in progress.
102                cancelAnimation();
103                // Now figure out which direction scroll events the controller will start
104                // calling the callbacks.
105                int conditionsToReportScroll = 0;
106
107                if (mDetector.isRestingState()) {
108                    if (mLauncher.isAllAppsVisible()) {
109                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_DOWN;
110                    } else {
111                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_UP;
112                    }
113                } else {
114                    if (isInDisallowRecatchBottomZone()) {
115                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_UP;
116                    } else if (isInDisallowRecatchTopZone()) {
117                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_DOWN;
118                    } else {
119                        conditionsToReportScroll |= VerticalPullDetector.THRESHOLD_ONLY;
120                    }
121                }
122                mDetector.setDetectableScrollConditions(conditionsToReportScroll);
123            }
124        }
125        if (mNoIntercept) {
126            return false;
127        }
128        mDetector.onTouchEvent(ev);
129        if (mDetector.isScrollingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) {
130            return false;
131        }
132        return mDetector.shouldIntercept();
133    }
134
135    private boolean shouldPossiblyIntercept(MotionEvent ev) {
136        DeviceProfile grid = mLauncher.getDeviceProfile();
137        if (mDetector.isRestingState()) {
138            if (grid.isVerticalBarLayout()) {
139                if (ev.getY() > mLauncher.getDeviceProfile().heightPx - mBezelSwipeUpHeight) {
140                    return true;
141                }
142            } else {
143                if (mLauncher.getDragLayer().isEventOverHotseat(ev) && !grid.isVerticalBarLayout()) {
144                    return true;
145                }
146            }
147            return false;
148        } else {
149            return true;
150        }
151    }
152
153    @Override
154    public boolean onTouchEvent(MotionEvent ev) {
155        return mDetector.onTouchEvent(ev);
156    }
157
158    private boolean isInDisallowRecatchTopZone() {
159        return mShiftCurrent / mShiftRange < RECATCH_REJECTION_FRACTION;
160    }
161
162    private boolean isInDisallowRecatchBottomZone() {
163        return mShiftCurrent / mShiftRange > 1 - RECATCH_REJECTION_FRACTION;
164    }
165
166    @Override
167    public void onScrollStart(boolean start) {
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        if (0 <= mShiftStart + displacement && mShiftStart + displacement < mShiftRange) {
179            setProgress(mShiftStart + displacement);
180        }
181        return true;
182    }
183
184    @Override
185    public void onScrollEnd(float velocity, boolean fling) {
186        if (mAppsView == null) {
187            return; // early termination.
188        }
189
190        if (fling) {
191            if (velocity < 0) {
192                calculateDuration(velocity, mAppsView.getTranslationY());
193
194                if (!mLauncher.isAllAppsVisible()) {
195                    mLauncher.showAppsView(true, true, false, false);
196                } else {
197                    animateToAllApps(mCurrentAnimation, mAnimationDuration, true);
198                }
199            } else {
200                calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
201                if (mLauncher.isAllAppsVisible()) {
202                    mLauncher.showWorkspace(true);
203                } else {
204                    animateToWorkspace(mCurrentAnimation, mAnimationDuration, true);
205                }
206            }
207            // snap to top or bottom using the release velocity
208        } else {
209            if (mAppsView.getTranslationY() > mShiftRange / 2) {
210                calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
211                if (mLauncher.isAllAppsVisible()) {
212                    mLauncher.showWorkspace(true);
213                } else {
214                    animateToWorkspace(mCurrentAnimation, mAnimationDuration, true);
215                }
216            } else {
217                calculateDuration(velocity, Math.abs(mAppsView.getTranslationY()));
218                if (!mLauncher.isAllAppsVisible()) {
219                    mLauncher.showAppsView(true, true, false, false);
220                } else {
221                    animateToAllApps(mCurrentAnimation, mAnimationDuration, true);
222                }
223
224            }
225        }
226    }
227    /**
228     * @param start {@code true} if start of new drag.
229     */
230    public void preparePull(boolean start) {
231        if (start) {
232            // Initialize values that should not change until #onScrollEnd
233            mStatusBarHeight = mLauncher.getDragLayer().getInsets().top;
234            mHotseat.setVisibility(View.VISIBLE);
235            mHotseat.bringToFront();
236            if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
237                mShiftRange = mHotseat.getTop();
238            } else {
239                mShiftRange = mHotseat.getBottom();
240            }
241            if (!mLauncher.isAllAppsVisible()) {
242                mLauncher.tryAndUpdatePredictedApps();
243
244                mHotseatBackgroundAlpha = mHotseat.getBackground().getAlpha() / 255f;
245                mHotseat.setBackgroundTransparent(true /* transparent */);
246                mAppsView.setVisibility(View.VISIBLE);
247                mAppsView.getContentView().setVisibility(View.VISIBLE);
248                mAppsView.getContentView().setBackground(null);
249                mAppsView.getRevealView().setVisibility(View.VISIBLE);
250                mAppsView.getRevealView().setAlpha(mHotseatBackgroundAlpha);
251
252                DeviceProfile grid= mLauncher.getDeviceProfile();
253                if (!grid.isVerticalBarLayout()) {
254                    mShiftRange = mHotseat.getTop();
255                } else {
256                    mShiftRange = mHotseat.getBottom();
257                }
258                mAppsView.getRevealView().setAlpha(mHotseatBackgroundAlpha);
259                setProgress(mShiftRange);
260            } else {
261                // TODO: get rid of this workaround to override state change by workspace transition
262                mWorkspace.onLauncherTransitionPrepare(mLauncher, false, false);
263                View child = ((CellLayout) mWorkspace.getChildAt(mWorkspace.getNextPage()))
264                        .getShortcutsAndWidgets();
265                child.setVisibility(View.VISIBLE);
266                child.setAlpha(1f);
267            }
268        } else {
269            setProgress(mShiftCurrent);
270        }
271    }
272
273    private void updateLightStatusBar(float progress) {
274        boolean enable = (progress < mStatusBarHeight / 2);
275        // Already set correctly
276        if (mLightStatusBar == enable) {
277            return;
278        }
279        int systemUiFlags = mLauncher.getWindow().getDecorView().getSystemUiVisibility();
280        if (enable) {
281            mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags
282                    | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
283
284        } else {
285            mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags
286                    & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
287
288        }
289        mLightStatusBar = enable;
290    }
291
292    /**
293     * @param progress y value of the border between hotseat and all apps
294     */
295    public void setProgress(float progress) {
296        updateLightStatusBar(progress);
297        mShiftCurrent = progress;
298        float alpha = calcAlphaAllApps(progress);
299        float workspaceHotseatAlpha = 1 - alpha;
300
301        mAppsView.getRevealView().setAlpha(Math.min(ALL_APPS_FINAL_ALPHA, Math.max(mHotseatBackgroundAlpha,
302                mDecelInterpolator.getInterpolation(alpha))));
303        mAppsView.getContentView().setAlpha(alpha);
304        mAppsView.setTranslationY(progress);
305        mWorkspace.setWorkspaceTranslation(Direction.Y,
306                PARALLAX_COEFFICIENT * (-mShiftRange + progress),
307                mAccelInterpolator.getInterpolation(workspaceHotseatAlpha));
308        if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
309            mWorkspace.setHotseatTranslation(Direction.Y, -mShiftRange + progress,
310                    mAccelInterpolator.getInterpolation(workspaceHotseatAlpha));
311        } else {
312            mWorkspace.setHotseatTranslation(Direction.Y,
313                    PARALLAX_COEFFICIENT * (-mShiftRange + progress),
314                    mAccelInterpolator.getInterpolation(workspaceHotseatAlpha));
315        }
316    }
317
318    public float getProgress() {
319        return mShiftCurrent;
320    }
321
322    private float calcAlphaAllApps(float progress) {
323        return ((mShiftRange - progress)/ mShiftRange);
324    }
325
326    private void calculateDuration(float velocity, float disp) {
327        // TODO: make these values constants after tuning.
328        float velocityDivisor = Math.max(1.5f, Math.abs(0.5f * velocity));
329        float travelDistance = Math.max(0.2f, disp / mShiftRange);
330        mAnimationDuration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
331        if (DBG) {
332            Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", mAnimationDuration, velocity, disp));
333        }
334    }
335
336    public void animateToAllApps(AnimatorSet animationOut, long duration, boolean start) {
337        if (animationOut == null){
338            return;
339        }
340        if (mDetector.isRestingState()) {
341            preparePull(true);
342            mAnimationDuration = duration;
343            mShiftStart = mAppsView.getTranslationY();
344        }
345        final float fromAllAppsTop = mAppsView.getTranslationY();
346        final float toAllAppsTop = 0;
347
348        ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
349                fromAllAppsTop, toAllAppsTop);
350        driftAndAlpha.setDuration(mAnimationDuration);
351        driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator());
352        animationOut.play(driftAndAlpha);
353
354        animationOut.addListener(new AnimatorListenerAdapter() {
355            boolean canceled = false;
356            @Override
357            public void onAnimationCancel(Animator animation) {
358                canceled = true;
359            }
360            @Override
361            public void onAnimationEnd(Animator animation) {
362                if (canceled) {
363                    return;
364                } else {
365                    finishPullUp();
366                    cleanUpAnimation();
367                    mDetector.finishedScrolling();
368                }
369            }});
370        mCurrentAnimation = animationOut;
371        if (start) {
372            mCurrentAnimation.start();
373        }
374    }
375
376    public void animateToWorkspace(AnimatorSet animationOut, long duration, boolean start) {
377        if (animationOut == null){
378            return;
379        }
380        if(mDetector.isRestingState()) {
381            preparePull(true);
382            mAnimationDuration = duration;
383            mShiftStart = mAppsView.getTranslationY();
384        }
385        final float fromAllAppsTop = mAppsView.getTranslationY();
386        final float toAllAppsTop = mShiftRange;
387
388        ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
389                fromAllAppsTop, toAllAppsTop);
390        driftAndAlpha.setDuration(mAnimationDuration);
391        driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator());
392        animationOut.play(driftAndAlpha);
393
394        animationOut.addListener(new AnimatorListenerAdapter() {
395             boolean canceled = false;
396             @Override
397             public void onAnimationCancel(Animator animation) {
398                 canceled = true;
399                 setProgress(mShiftCurrent);
400             }
401
402             @Override
403             public void onAnimationEnd(Animator animation) {
404                 if (canceled) {
405                     return;
406                 } else {
407                     finishPullDown();
408                     cleanUpAnimation();
409                     mDetector.finishedScrolling();
410                 }
411             }});
412        mCurrentAnimation = animationOut;
413        if (start) {
414            mCurrentAnimation.start();
415        }
416    }
417
418    private void finishPullUp() {
419        mHotseat.setVisibility(View.INVISIBLE);
420        setProgress(0f);
421    }
422
423    public void finishPullDown() {
424        if (mHotseat.getBackground() != null) {
425            return;
426        }
427        mAppsView.setVisibility(View.INVISIBLE);
428        mHotseat.setBackgroundTransparent(false /* transparent */);
429        mHotseat.setVisibility(View.VISIBLE);
430        setProgress(mShiftRange);
431    }
432
433    private void cancelAnimation() {
434        if (mCurrentAnimation != null) {
435            mCurrentAnimation.setDuration(0);
436            mCurrentAnimation.cancel();
437            mCurrentAnimation = null;
438        }
439    }
440
441    private void cleanUpAnimation() {
442        mCurrentAnimation = null;
443    }
444
445    public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) {
446        mAppsView = appsView;
447        mHotseat = hotseat;
448        mWorkspace = workspace;
449    }
450}
451