LauncherStateTransitionAnimation.java revision fc3a2711b3f192981663554e615af20b48e2089b
1/*
2 * Copyright (C) 2015 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
17package com.android.launcher3;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.animation.PropertyValuesHolder;
24import android.animation.TimeInterpolator;
25import android.annotation.SuppressLint;
26import android.content.res.Resources;
27import android.util.Log;
28import android.view.View;
29import android.view.animation.AccelerateInterpolator;
30import android.view.animation.DecelerateInterpolator;
31
32import com.android.launcher3.allapps.AllAppsContainerView;
33import com.android.launcher3.util.Thunk;
34import com.android.launcher3.util.UiThreadCircularReveal;
35import com.android.launcher3.widget.WidgetsContainerView;
36
37import java.util.HashMap;
38
39/**
40 * TODO: figure out what kind of tests we can write for this
41 *
42 * Things to test when changing the following class.
43 *   - Home from workspace
44 *          - from center screen
45 *          - from other screens
46 *   - Home from all apps
47 *          - from center screen
48 *          - from other screens
49 *   - Back from all apps
50 *          - from center screen
51 *          - from other screens
52 *   - Launch app from workspace and quit
53 *          - with back
54 *          - with home
55 *   - Launch app from all apps and quit
56 *          - with back
57 *          - with home
58 *   - Go to a screen that's not the default, then all
59 *     apps, and launch and app, and go back
60 *          - with back
61 *          -with home
62 *   - On workspace, long press power and go back
63 *          - with back
64 *          - with home
65 *   - On all apps, long press power and go back
66 *          - with back
67 *          - with home
68 *   - On workspace, power off
69 *   - On all apps, power off
70 *   - Launch an app and turn off the screen while in that app
71 *          - Go back with home key
72 *          - Go back with back key  TODO: make this not go to workspace
73 *          - From all apps
74 *          - From workspace
75 *   - Enter and exit car mode (becuase it causes an extra configuration changed)
76 *          - From all apps
77 *          - From the center workspace
78 *          - From another workspace
79 */
80public class LauncherStateTransitionAnimation {
81
82    /**
83     * Private callbacks made during transition setup.
84     */
85    static abstract class PrivateTransitionCallbacks {
86        float getMaterialRevealViewFinalAlpha(View revealView) {
87            return 0;
88        }
89        float getMaterialRevealViewStartFinalRadius() {
90            return 0;
91        }
92        AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(View revealView,
93                View buttonView) {
94            return null;
95        }
96        void onTransitionComplete() {}
97    }
98
99    public static final String TAG = "LauncherStateTransitionAnimation";
100
101    // Flags to determine how to set the layers on views before the transition animation
102    public static final int BUILD_LAYER = 0;
103    public static final int BUILD_AND_SET_LAYER = 1;
104    public static final int SINGLE_FRAME_DELAY = 16;
105
106    @Thunk Launcher mLauncher;
107    @Thunk AnimatorSet mCurrentAnimation;
108
109    public LauncherStateTransitionAnimation(Launcher l) {
110        mLauncher = l;
111    }
112
113    /**
114     * Starts an animation to the apps view.
115     *
116     * @param startSearchAfterTransition Immediately starts app search after the transition to
117     *                                   All Apps is completed.
118     */
119    public void startAnimationToAllApps(final Workspace.State fromWorkspaceState,
120            final boolean animated, final boolean startSearchAfterTransition) {
121        final AllAppsContainerView toView = mLauncher.getAppsView();
122        final View buttonView = mLauncher.getAllAppsButton();
123        PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() {
124            @Override
125            public float getMaterialRevealViewFinalAlpha(View revealView) {
126                return 1f;
127            }
128            @Override
129            public float getMaterialRevealViewStartFinalRadius() {
130                int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize;
131                return allAppsButtonSize / 2;
132            }
133            @Override
134            public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(
135                    final View revealView, final View allAppsButtonView) {
136                return new AnimatorListenerAdapter() {
137                    public void onAnimationStart(Animator animation) {
138                        allAppsButtonView.setVisibility(View.INVISIBLE);
139                    }
140                    public void onAnimationEnd(Animator animation) {
141                        allAppsButtonView.setVisibility(View.VISIBLE);
142                    }
143                };
144            }
145            @Override
146            void onTransitionComplete() {
147                if (startSearchAfterTransition) {
148                    toView.startAppsSearch();
149                }
150            }
151        };
152        // Only animate the search bar if animating from spring loaded mode back to all apps
153        mCurrentAnimation = startAnimationToOverlay(fromWorkspaceState,
154                Workspace.State.NORMAL_HIDDEN, buttonView, toView, toView.getContentView(),
155                toView.getRevealView(), toView.getSearchBarView(), animated, cb);
156    }
157
158    /**
159     * Starts an animation to the widgets view.
160     */
161    public void startAnimationToWidgets(final Workspace.State fromWorkspaceState,
162            final boolean animated) {
163        final WidgetsContainerView toView = mLauncher.getWidgetsView();
164        final View buttonView = mLauncher.getWidgetsButton();
165        PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() {
166            @Override
167            public float getMaterialRevealViewFinalAlpha(View revealView) {
168                return 0.3f;
169            }
170        };
171        mCurrentAnimation = startAnimationToOverlay(fromWorkspaceState,
172                Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, toView.getContentView(),
173                toView.getRevealView(), null, animated, cb);
174    }
175
176    /**
177     * Starts and animation to the workspace from the current overlay view.
178     */
179    public void startAnimationToWorkspace(final Launcher.State fromState,
180            final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState,
181            final boolean animated, final Runnable onCompleteRunnable) {
182        if (toWorkspaceState != Workspace.State.NORMAL &&
183                toWorkspaceState != Workspace.State.SPRING_LOADED &&
184                toWorkspaceState != Workspace.State.OVERVIEW) {
185            Log.e(TAG, "Unexpected call to startAnimationToWorkspace");
186        }
187
188        if (fromState == Launcher.State.APPS || fromState == Launcher.State.APPS_SPRING_LOADED) {
189            startAnimationToWorkspaceFromAllApps(fromWorkspaceState, toWorkspaceState,
190                    animated, onCompleteRunnable);
191        } else {
192            startAnimationToWorkspaceFromWidgets(fromWorkspaceState, toWorkspaceState,
193                    animated, onCompleteRunnable);
194        }
195    }
196
197    /**
198     * Creates and starts a new animation to a particular overlay view.
199     */
200    @SuppressLint("NewApi")
201    private AnimatorSet startAnimationToOverlay(final Workspace.State fromWorkspaceState,
202            final Workspace.State toWorkspaceState, final View buttonView, final View toView,
203            final View contentView, final View revealView, final View overlaySearchBarView,
204            final boolean animated, final PrivateTransitionCallbacks pCb) {
205        final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
206        final Resources res = mLauncher.getResources();
207        final boolean material = Utilities.isLmpOrAbove();
208        final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime);
209        final int itemsAlphaStagger =
210                res.getInteger(R.integer.config_overlayItemsAlphaStagger);
211
212        final View fromView = mLauncher.getWorkspace();
213
214        final HashMap<View, Integer> layerViews = new HashMap<>();
215
216        // If for some reason our views aren't initialized, don't animate
217        boolean initialized = buttonView != null;
218
219        // Cancel the current animation
220        cancelAnimation();
221
222        // Create the workspace animation.
223        // NOTE: this call apparently also sets the state for the workspace if !animated
224        Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState,
225                animated, layerViews);
226
227        // Animate the search bar
228        startWorkspaceSearchBarAnimation(animation, fromWorkspaceState, toWorkspaceState,
229                animated ? revealDuration : 0, overlaySearchBarView);
230
231        if (animated && initialized) {
232            // Setup the reveal view animation
233            int width = revealView.getMeasuredWidth();
234            int height = revealView.getMeasuredHeight();
235            float revealRadius = (float) Math.hypot(width / 2, height / 2);
236            revealView.setVisibility(View.VISIBLE);
237            revealView.setAlpha(0f);
238            revealView.setTranslationY(0f);
239            revealView.setTranslationX(0f);
240
241            // Calculate the final animation values
242            final float revealViewToAlpha;
243            final float revealViewToXDrift;
244            final float revealViewToYDrift;
245            if (material) {
246                int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView,
247                        buttonView, null);
248                revealViewToAlpha = pCb.getMaterialRevealViewFinalAlpha(revealView);
249                revealViewToYDrift = buttonViewToPanelDelta[1];
250                revealViewToXDrift = buttonViewToPanelDelta[0];
251            } else {
252                revealViewToAlpha = 0f;
253                revealViewToYDrift = 2 * height / 3;
254                revealViewToXDrift = 0;
255            }
256
257            // Create the animators
258            PropertyValuesHolder panelAlpha =
259                    PropertyValuesHolder.ofFloat(View.ALPHA, revealViewToAlpha, 1f);
260            PropertyValuesHolder panelDriftY =
261                    PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, revealViewToYDrift, 0);
262            PropertyValuesHolder panelDriftX =
263                    PropertyValuesHolder.ofFloat(View.TRANSLATION_X, revealViewToXDrift, 0);
264            ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView,
265                    panelAlpha, panelDriftY, panelDriftX);
266            panelAlphaAndDrift.setDuration(revealDuration);
267            panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));
268
269            // Play the animation
270            layerViews.put(revealView, BUILD_AND_SET_LAYER);
271            animation.play(panelAlphaAndDrift);
272
273            if (overlaySearchBarView != null) {
274                overlaySearchBarView.setAlpha(0f);
275                ObjectAnimator searchBarAlpha = ObjectAnimator.ofFloat(overlaySearchBarView, "alpha", 0f, 1f);
276                searchBarAlpha.setDuration(100);
277                searchBarAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
278                layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER);
279                animation.play(searchBarAlpha);
280            }
281
282            // Setup the animation for the content view
283            contentView.setVisibility(View.VISIBLE);
284            contentView.setAlpha(0f);
285            contentView.setTranslationY(revealViewToYDrift);
286            layerViews.put(contentView, BUILD_AND_SET_LAYER);
287
288            // Create the individual animators
289            ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY",
290                    revealViewToYDrift, 0);
291            pageDrift.setDuration(revealDuration);
292            pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));
293            pageDrift.setStartDelay(itemsAlphaStagger);
294            animation.play(pageDrift);
295
296            ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f);
297            itemsAlpha.setDuration(revealDuration);
298            itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
299            itemsAlpha.setStartDelay(itemsAlphaStagger);
300            animation.play(itemsAlpha);
301
302            if (material) {
303                float startRadius = pCb.getMaterialRevealViewStartFinalRadius();
304                AnimatorListenerAdapter listener = pCb.getMaterialRevealViewAnimatorListener(
305                        revealView, buttonView);
306                Animator reveal = UiThreadCircularReveal.createCircularReveal(revealView, width / 2,
307                        height / 2, startRadius, revealRadius);
308                reveal.setDuration(revealDuration);
309                reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
310                if (listener != null) {
311                    reveal.addListener(listener);
312                }
313                animation.play(reveal);
314            }
315
316            animation.addListener(new AnimatorListenerAdapter() {
317                @Override
318                public void onAnimationEnd(Animator animation) {
319                    dispatchOnLauncherTransitionEnd(fromView, animated, false);
320                    dispatchOnLauncherTransitionEnd(toView, animated, false);
321
322                    // Hide the reveal view
323                    revealView.setVisibility(View.INVISIBLE);
324
325                    // Disable all necessary layers
326                    for (View v : layerViews.keySet()) {
327                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
328                            v.setLayerType(View.LAYER_TYPE_NONE, null);
329                        }
330                    }
331
332                    // This can hold unnecessary references to views.
333                    cleanupAnimation();
334                    pCb.onTransitionComplete();
335                }
336
337            });
338
339            // Play the workspace animation
340            if (workspaceAnim != null) {
341                animation.play(workspaceAnim);
342            }
343
344            // Dispatch the prepare transition signal
345            dispatchOnLauncherTransitionPrepare(fromView, animated, false);
346            dispatchOnLauncherTransitionPrepare(toView, animated, false);
347
348
349            final AnimatorSet stateAnimation = animation;
350            final Runnable startAnimRunnable = new Runnable() {
351                public void run() {
352                    // Check that mCurrentAnimation hasn't changed while
353                    // we waited for a layout/draw pass
354                    if (mCurrentAnimation != stateAnimation)
355                        return;
356                    dispatchOnLauncherTransitionStart(fromView, animated, false);
357                    dispatchOnLauncherTransitionStart(toView, animated, false);
358
359                    // Enable all necessary layers
360                    boolean isLmpOrAbove = Utilities.isLmpOrAbove();
361                    for (View v : layerViews.keySet()) {
362                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
363                            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
364                        }
365                        if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) {
366                            v.buildLayer();
367                        }
368                    }
369
370                    // Focus the new view
371                    toView.requestFocus();
372
373                    stateAnimation.start();
374                }
375            };
376            toView.bringToFront();
377            toView.setVisibility(View.VISIBLE);
378            toView.post(startAnimRunnable);
379
380            return animation;
381        } else {
382            toView.setTranslationX(0.0f);
383            toView.setTranslationY(0.0f);
384            toView.setScaleX(1.0f);
385            toView.setScaleY(1.0f);
386            toView.setVisibility(View.VISIBLE);
387            toView.bringToFront();
388
389            // Show the content view
390            contentView.setVisibility(View.VISIBLE);
391
392            dispatchOnLauncherTransitionPrepare(fromView, animated, false);
393            dispatchOnLauncherTransitionStart(fromView, animated, false);
394            dispatchOnLauncherTransitionEnd(fromView, animated, false);
395            dispatchOnLauncherTransitionPrepare(toView, animated, false);
396            dispatchOnLauncherTransitionStart(toView, animated, false);
397            dispatchOnLauncherTransitionEnd(toView, animated, false);
398            pCb.onTransitionComplete();
399
400            return null;
401        }
402    }
403
404    /**
405     * Starts and animation to the workspace from the apps view.
406     */
407    private void startAnimationToWorkspaceFromAllApps(final Workspace.State fromWorkspaceState,
408            final Workspace.State toWorkspaceState, final boolean animated,
409            final Runnable onCompleteRunnable) {
410        AllAppsContainerView appsView = mLauncher.getAppsView();
411        PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() {
412            @Override
413            float getMaterialRevealViewFinalAlpha(View revealView) {
414                // No alpha anim from all apps
415                return 1f;
416            }
417            @Override
418            float getMaterialRevealViewStartFinalRadius() {
419                int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize;
420                return allAppsButtonSize / 2;
421            }
422            @Override
423            public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(
424                    final View revealView, final View allAppsButtonView) {
425                return new AnimatorListenerAdapter() {
426                    public void onAnimationStart(Animator animation) {
427                        // We set the alpha instead of visibility to ensure that the focus does not
428                        // get taken from the all apps view
429                        allAppsButtonView.setVisibility(View.VISIBLE);
430                        allAppsButtonView.setAlpha(0f);
431                    }
432                    public void onAnimationEnd(Animator animation) {
433                        // Hide the reveal view
434                        revealView.setVisibility(View.INVISIBLE);
435
436                        // Show the all apps button, and focus it
437                        allAppsButtonView.setAlpha(1f);
438                    }
439                };
440            }
441        };
442        // Only animate the search bar if animating to spring loaded mode from all apps
443        mCurrentAnimation = startAnimationToWorkspaceFromOverlay(fromWorkspaceState, toWorkspaceState,
444                mLauncher.getAllAppsButton(), appsView, appsView.getContentView(),
445                appsView.getRevealView(), appsView.getSearchBarView(), animated,
446                onCompleteRunnable, cb);
447    }
448
449    /**
450     * Starts and animation to the workspace from the widgets view.
451     */
452    private void startAnimationToWorkspaceFromWidgets(final Workspace.State fromWorkspaceState,
453            final Workspace.State toWorkspaceState, final boolean animated,
454            final Runnable onCompleteRunnable) {
455        final WidgetsContainerView widgetsView = mLauncher.getWidgetsView();
456        PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() {
457            @Override
458            float getMaterialRevealViewFinalAlpha(View revealView) {
459                return 0.3f;
460            }
461            @Override
462            public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(
463                    final View revealView, final View widgetsButtonView) {
464                return new AnimatorListenerAdapter() {
465                    public void onAnimationEnd(Animator animation) {
466                        // Hide the reveal view
467                        revealView.setVisibility(View.INVISIBLE);
468                    }
469                };
470            }
471        };
472        mCurrentAnimation = startAnimationToWorkspaceFromOverlay(fromWorkspaceState,
473                toWorkspaceState, mLauncher.getWidgetsButton(), widgetsView,
474                widgetsView.getContentView(), widgetsView.getRevealView(), null, animated,
475                onCompleteRunnable, cb);
476    }
477
478    /**
479     * Creates and starts a new animation to the workspace.
480     */
481    private AnimatorSet startAnimationToWorkspaceFromOverlay(final Workspace.State fromWorkspaceState,
482            final Workspace.State toWorkspaceState, final View buttonView,
483            final View fromView, final View contentView, final View revealView,
484            final View overlaySearchBarView, final boolean animated, final Runnable onCompleteRunnable,
485            final PrivateTransitionCallbacks pCb) {
486        final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
487        final Resources res = mLauncher.getResources();
488        final boolean material = Utilities.isLmpOrAbove();
489        final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime);
490        final int itemsAlphaStagger =
491                res.getInteger(R.integer.config_overlayItemsAlphaStagger);
492
493        final View toView = mLauncher.getWorkspace();
494
495        final HashMap<View, Integer> layerViews = new HashMap<>();
496
497        // If for some reason our views aren't initialized, don't animate
498        boolean initialized = buttonView != null;
499
500        // Cancel the current animation
501        cancelAnimation();
502
503        // Create the workspace animation.
504        // NOTE: this call apparently also sets the state for the workspace if !animated
505        Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState,
506                animated, layerViews);
507
508        // Animate the search bar
509        startWorkspaceSearchBarAnimation(animation, fromWorkspaceState, toWorkspaceState,
510                animated ? revealDuration : 0, overlaySearchBarView);
511
512        if (animated && initialized) {
513            // Play the workspace animation
514            if (workspaceAnim != null) {
515                animation.play(workspaceAnim);
516            }
517
518            // hideAppsCustomizeHelper is called in some cases when it is already hidden
519            // don't perform all these no-op animations. In particularly, this was causing
520            // the all-apps button to pop in and out.
521            if (fromView.getVisibility() == View.VISIBLE) {
522                int width = revealView.getMeasuredWidth();
523                int height = revealView.getMeasuredHeight();
524                float revealRadius = (float) Math.hypot(width / 2, height / 2);
525                revealView.setVisibility(View.VISIBLE);
526                revealView.setAlpha(1f);
527                revealView.setTranslationY(0);
528                layerViews.put(revealView, BUILD_AND_SET_LAYER);
529
530                // Calculate the final animation values
531                final float revealViewToXDrift;
532                final float revealViewToYDrift;
533                if (material) {
534                    int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView,
535                            buttonView, null);
536                    revealViewToYDrift = buttonViewToPanelDelta[1];
537                    revealViewToXDrift = buttonViewToPanelDelta[0];
538                } else {
539                    revealViewToYDrift = 2 * height / 3;
540                    revealViewToXDrift = 0;
541                }
542
543                // The vertical motion of the apps panel should be delayed by one frame
544                // from the conceal animation in order to give the right feel. We correspondingly
545                // shorten the duration so that the slide and conceal end at the same time.
546                TimeInterpolator decelerateInterpolator = material ?
547                        new LogDecelerateInterpolator(100, 0) :
548                        new DecelerateInterpolator(1f);
549                ObjectAnimator panelDriftY = ObjectAnimator.ofFloat(revealView, "translationY",
550                        0, revealViewToYDrift);
551                panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY);
552                panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
553                panelDriftY.setInterpolator(decelerateInterpolator);
554                animation.play(panelDriftY);
555
556                ObjectAnimator panelDriftX = ObjectAnimator.ofFloat(revealView, "translationX",
557                        0, revealViewToXDrift);
558                panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY);
559                panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
560                panelDriftX.setInterpolator(decelerateInterpolator);
561                animation.play(panelDriftX);
562
563                // Setup animation for the reveal panel alpha
564                final float revealViewToAlpha = !material ? 0f :
565                        pCb.getMaterialRevealViewFinalAlpha(revealView);
566                if (revealViewToAlpha != 1f) {
567                    ObjectAnimator panelAlpha = ObjectAnimator.ofFloat(revealView, "alpha",
568                            1f, revealViewToAlpha);
569                    panelAlpha.setDuration(material ? revealDuration : 150);
570                    panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY);
571                    panelAlpha.setInterpolator(decelerateInterpolator);
572                    animation.play(panelAlpha);
573                }
574
575                // Setup the animation for the content view
576                layerViews.put(contentView, BUILD_AND_SET_LAYER);
577
578                // Create the individual animators
579                ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY",
580                        0, revealViewToYDrift);
581                contentView.setTranslationY(0);
582                pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY);
583                pageDrift.setInterpolator(decelerateInterpolator);
584                pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
585                animation.play(pageDrift);
586
587                contentView.setAlpha(1f);
588                ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 1f, 0f);
589                itemsAlpha.setDuration(100);
590                itemsAlpha.setInterpolator(decelerateInterpolator);
591                animation.play(itemsAlpha);
592
593                if (overlaySearchBarView != null) {
594                    overlaySearchBarView.setAlpha(1f);
595                    ObjectAnimator searchAlpha = ObjectAnimator.ofFloat(overlaySearchBarView, "alpha", 1f, 0f);
596                    searchAlpha.setDuration(material ? 100 : 150);
597                    searchAlpha.setInterpolator(decelerateInterpolator);
598                    searchAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY);
599                    layerViews.put(overlaySearchBarView, BUILD_AND_SET_LAYER);
600                    animation.play(searchAlpha);
601                }
602
603                if (material) {
604                    // Animate the all apps button
605                    float finalRadius = pCb.getMaterialRevealViewStartFinalRadius();
606                    AnimatorListenerAdapter listener =
607                            pCb.getMaterialRevealViewAnimatorListener(revealView, buttonView);
608                    Animator reveal = UiThreadCircularReveal.createCircularReveal(revealView, width / 2,
609                            height / 2, revealRadius, finalRadius);
610                    reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
611                    reveal.setDuration(revealDuration);
612                    reveal.setStartDelay(itemsAlphaStagger);
613                    if (listener != null) {
614                        reveal.addListener(listener);
615                    }
616                    animation.play(reveal);
617                }
618
619                dispatchOnLauncherTransitionPrepare(fromView, animated, true);
620                dispatchOnLauncherTransitionPrepare(toView, animated, true);
621            }
622
623            animation.addListener(new AnimatorListenerAdapter() {
624                @Override
625                public void onAnimationEnd(Animator animation) {
626                    fromView.setVisibility(View.GONE);
627                    dispatchOnLauncherTransitionEnd(fromView, animated, true);
628                    dispatchOnLauncherTransitionEnd(toView, animated, true);
629
630                    // Run any queued runnables
631                    if (onCompleteRunnable != null) {
632                        onCompleteRunnable.run();
633                    }
634
635                    // Disable all necessary layers
636                    for (View v : layerViews.keySet()) {
637                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
638                            v.setLayerType(View.LAYER_TYPE_NONE, null);
639                        }
640                    }
641
642                    // Reset page transforms
643                    if (contentView != null) {
644                        contentView.setTranslationX(0);
645                        contentView.setTranslationY(0);
646                        contentView.setAlpha(1);
647                    }
648                    if (overlaySearchBarView != null) {
649                        overlaySearchBarView.setAlpha(1f);
650                    }
651
652                    // This can hold unnecessary references to views.
653                    cleanupAnimation();
654                    pCb.onTransitionComplete();
655                }
656            });
657
658            final AnimatorSet stateAnimation = animation;
659            final Runnable startAnimRunnable = new Runnable() {
660                public void run() {
661                    // Check that mCurrentAnimation hasn't changed while
662                    // we waited for a layout/draw pass
663                    if (mCurrentAnimation != stateAnimation)
664                        return;
665
666                    dispatchOnLauncherTransitionStart(fromView, animated, false);
667                    dispatchOnLauncherTransitionStart(toView, animated, false);
668
669                    // Enable all necessary layers
670                    boolean isLmpOrAbove = Utilities.isLmpOrAbove();
671                    for (View v : layerViews.keySet()) {
672                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
673                            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
674                        }
675                        if (isLmpOrAbove && Utilities.isViewAttachedToWindow(v)) {
676                            v.buildLayer();
677                        }
678                    }
679                    stateAnimation.start();
680                }
681            };
682            fromView.post(startAnimRunnable);
683
684            return animation;
685        } else {
686            fromView.setVisibility(View.GONE);
687            dispatchOnLauncherTransitionPrepare(fromView, animated, true);
688            dispatchOnLauncherTransitionStart(fromView, animated, true);
689            dispatchOnLauncherTransitionEnd(fromView, animated, true);
690            dispatchOnLauncherTransitionPrepare(toView, animated, true);
691            dispatchOnLauncherTransitionStart(toView, animated, true);
692            dispatchOnLauncherTransitionEnd(toView, animated, true);
693            pCb.onTransitionComplete();
694
695            // Run any queued runnables
696            if (onCompleteRunnable != null) {
697                onCompleteRunnable.run();
698            }
699
700            return null;
701        }
702    }
703
704    /**
705     * Coordinates the workspace search bar animation along with the launcher state animation.
706     */
707    private void startWorkspaceSearchBarAnimation(AnimatorSet animation,
708            final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState, int duration,
709            View overlaySearchBar) {
710        final SearchDropTargetBar.State toSearchBarState =
711                toWorkspaceState.getSearchDropTargetBarState();
712
713        if (overlaySearchBar != null) {
714            if ((toWorkspaceState == Workspace.State.NORMAL) &&
715                    (fromWorkspaceState == Workspace.State.NORMAL_HIDDEN)) {
716                // If we are transitioning from the overlay to the workspace, then show the
717                // workspace search bar immediately and let the overlay search bar fade out on top
718                mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0);
719            } else if (fromWorkspaceState == Workspace.State.NORMAL) {
720                // If we are transitioning from the workspace to the overlay, then keep the
721                // workspace search bar visible until the overlay search bar fades in on top
722                animation.addListener(new AnimatorListenerAdapter() {
723                    @Override
724                    public void onAnimationEnd(Animator animation) {
725                        mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0);
726                    }
727                });
728            } else {
729                // Otherwise, then just animate the workspace search bar normally
730                mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration);
731            }
732        } else {
733            // If there is no overlay search bar, then just animate the workspace search bar
734            mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration);
735        }
736    }
737
738    /**
739     * Dispatches the prepare-transition event to suitable views.
740     */
741    void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) {
742        if (v instanceof LauncherTransitionable) {
743            ((LauncherTransitionable) v).onLauncherTransitionPrepare(mLauncher, animated,
744                    toWorkspace);
745        }
746    }
747
748    /**
749     * Dispatches the start-transition event to suitable views.
750     */
751    void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) {
752        if (v instanceof LauncherTransitionable) {
753            ((LauncherTransitionable) v).onLauncherTransitionStart(mLauncher, animated,
754                    toWorkspace);
755        }
756
757        // Update the workspace transition step as well
758        dispatchOnLauncherTransitionStep(v, 0f);
759    }
760
761    /**
762     * Dispatches the step-transition event to suitable views.
763     */
764    void dispatchOnLauncherTransitionStep(View v, float t) {
765        if (v instanceof LauncherTransitionable) {
766            ((LauncherTransitionable) v).onLauncherTransitionStep(mLauncher, t);
767        }
768    }
769
770    /**
771     * Dispatches the end-transition event to suitable views.
772     */
773    void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) {
774        if (v instanceof LauncherTransitionable) {
775            ((LauncherTransitionable) v).onLauncherTransitionEnd(mLauncher, animated,
776                    toWorkspace);
777        }
778
779        // Update the workspace transition step as well
780        dispatchOnLauncherTransitionStep(v, 1f);
781    }
782
783    /**
784     * Cancels the current animation.
785     */
786    private void cancelAnimation() {
787        if (mCurrentAnimation != null) {
788            mCurrentAnimation.setDuration(0);
789            mCurrentAnimation.cancel();
790            mCurrentAnimation = null;
791        }
792    }
793
794    @Thunk void cleanupAnimation() {
795        mCurrentAnimation = null;
796    }
797}
798