Home | History | Annotate | Download | only in launcher3
      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 
     17 package com.android.launcher3;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.AnimatorSet;
     22 import android.animation.ObjectAnimator;
     23 import android.animation.TimeInterpolator;
     24 import android.animation.ValueAnimator;
     25 import android.content.Context;
     26 import android.content.res.Resources;
     27 import android.view.View;
     28 import android.view.ViewGroup;
     29 import android.view.accessibility.AccessibilityManager;
     30 import android.view.accessibility.AccessibilityNodeInfo;
     31 import android.view.animation.DecelerateInterpolator;
     32 
     33 import com.android.launcher3.util.Thunk;
     34 
     35 import java.util.HashMap;
     36 
     37 /**
     38  * A convenience class to update a view's visibility state after an alpha animation.
     39  */
     40 class AlphaUpdateListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener {
     41     private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f;
     42 
     43     private View mView;
     44     private boolean mAccessibilityEnabled;
     45 
     46     public AlphaUpdateListener(View v, boolean accessibilityEnabled) {
     47         mView = v;
     48         mAccessibilityEnabled = accessibilityEnabled;
     49     }
     50 
     51     @Override
     52     public void onAnimationUpdate(ValueAnimator arg0) {
     53         updateVisibility(mView, mAccessibilityEnabled);
     54     }
     55 
     56     public static void updateVisibility(View view, boolean accessibilityEnabled) {
     57         // We want to avoid the extra layout pass by setting the views to GONE unless
     58         // accessibility is on, in which case not setting them to GONE causes a glitch.
     59         int invisibleState = accessibilityEnabled ? View.GONE : View.INVISIBLE;
     60         if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) {
     61             view.setVisibility(invisibleState);
     62         } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD
     63                 && view.getVisibility() != View.VISIBLE) {
     64             view.setVisibility(View.VISIBLE);
     65         }
     66     }
     67 
     68     @Override
     69     public void onAnimationEnd(Animator arg0) {
     70         updateVisibility(mView, mAccessibilityEnabled);
     71     }
     72 
     73     @Override
     74     public void onAnimationStart(Animator arg0) {
     75         // We want the views to be visible for animation, so fade-in/out is visible
     76         mView.setVisibility(View.VISIBLE);
     77     }
     78 }
     79 
     80 /**
     81  * This interpolator emulates the rate at which the perceived scale of an object changes
     82  * as its distance from a camera increases. When this interpolator is applied to a scale
     83  * animation on a view, it evokes the sense that the object is shrinking due to moving away
     84  * from the camera.
     85  */
     86 class ZInterpolator implements TimeInterpolator {
     87     private float focalLength;
     88 
     89     public ZInterpolator(float foc) {
     90         focalLength = foc;
     91     }
     92 
     93     public float getInterpolation(float input) {
     94         return (1.0f - focalLength / (focalLength + input)) /
     95                 (1.0f - focalLength / (focalLength + 1.0f));
     96     }
     97 }
     98 
     99 /**
    100  * The exact reverse of ZInterpolator.
    101  */
    102 class InverseZInterpolator implements TimeInterpolator {
    103     private ZInterpolator zInterpolator;
    104     public InverseZInterpolator(float foc) {
    105         zInterpolator = new ZInterpolator(foc);
    106     }
    107     public float getInterpolation(float input) {
    108         return 1 - zInterpolator.getInterpolation(1 - input);
    109     }
    110 }
    111 
    112 /**
    113  * InverseZInterpolator compounded with an ease-out.
    114  */
    115 class ZoomInInterpolator implements TimeInterpolator {
    116     private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f);
    117     private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f);
    118 
    119     public float getInterpolation(float input) {
    120         return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input));
    121     }
    122 }
    123 
    124 /**
    125  * Stores the transition states for convenience.
    126  */
    127 class TransitionStates {
    128 
    129     // Raw states
    130     final boolean oldStateIsNormal;
    131     final boolean oldStateIsSpringLoaded;
    132     final boolean oldStateIsNormalHidden;
    133     final boolean oldStateIsOverviewHidden;
    134     final boolean oldStateIsOverview;
    135 
    136     final boolean stateIsNormal;
    137     final boolean stateIsSpringLoaded;
    138     final boolean stateIsNormalHidden;
    139     final boolean stateIsOverviewHidden;
    140     final boolean stateIsOverview;
    141 
    142     // Convenience members
    143     final boolean workspaceToAllApps;
    144     final boolean overviewToAllApps;
    145     final boolean allAppsToWorkspace;
    146     final boolean workspaceToOverview;
    147     final boolean overviewToWorkspace;
    148 
    149     public TransitionStates(final Workspace.State fromState, final Workspace.State toState) {
    150         oldStateIsNormal = (fromState == Workspace.State.NORMAL);
    151         oldStateIsSpringLoaded = (fromState == Workspace.State.SPRING_LOADED);
    152         oldStateIsNormalHidden = (fromState == Workspace.State.NORMAL_HIDDEN);
    153         oldStateIsOverviewHidden = (fromState == Workspace.State.OVERVIEW_HIDDEN);
    154         oldStateIsOverview = (fromState == Workspace.State.OVERVIEW);
    155 
    156         stateIsNormal = (toState == Workspace.State.NORMAL);
    157         stateIsSpringLoaded = (toState == Workspace.State.SPRING_LOADED);
    158         stateIsNormalHidden = (toState == Workspace.State.NORMAL_HIDDEN);
    159         stateIsOverviewHidden = (toState == Workspace.State.OVERVIEW_HIDDEN);
    160         stateIsOverview = (toState == Workspace.State.OVERVIEW);
    161 
    162         workspaceToOverview = (oldStateIsNormal && stateIsOverview);
    163         workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden);
    164         overviewToWorkspace = (oldStateIsOverview && stateIsNormal);
    165         overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden);
    166         allAppsToWorkspace = (stateIsNormalHidden && stateIsNormal);
    167     }
    168 }
    169 
    170 /**
    171  * Manages the animations between each of the workspace states.
    172  */
    173 public class WorkspaceStateTransitionAnimation {
    174 
    175     public static final String TAG = "WorkspaceStateTransitionAnimation";
    176 
    177     public static final int SCROLL_TO_CURRENT_PAGE = -1;
    178     @Thunk static final int BACKGROUND_FADE_OUT_DURATION = 350;
    179 
    180     final @Thunk Launcher mLauncher;
    181     final @Thunk Workspace mWorkspace;
    182 
    183     @Thunk AnimatorSet mStateAnimator;
    184     @Thunk float[] mOldBackgroundAlphas;
    185     @Thunk float[] mOldAlphas;
    186     @Thunk float[] mNewBackgroundAlphas;
    187     @Thunk float[] mNewAlphas;
    188     @Thunk int mLastChildCount = -1;
    189 
    190     @Thunk float mCurrentScale;
    191     @Thunk float mNewScale;
    192 
    193     @Thunk final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator();
    194 
    195     @Thunk float mSpringLoadedShrinkFactor;
    196     @Thunk float mOverviewModeShrinkFactor;
    197     @Thunk float mWorkspaceScrimAlpha;
    198     @Thunk int mAllAppsTransitionTime;
    199     @Thunk int mOverviewTransitionTime;
    200     @Thunk int mOverlayTransitionTime;
    201     @Thunk boolean mWorkspaceFadeInAdjacentScreens;
    202 
    203     public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) {
    204         mLauncher = launcher;
    205         mWorkspace = workspace;
    206 
    207         DeviceProfile grid = mLauncher.getDeviceProfile();
    208         Resources res = launcher.getResources();
    209         mAllAppsTransitionTime = res.getInteger(R.integer.config_allAppsTransitionTime);
    210         mOverviewTransitionTime = res.getInteger(R.integer.config_overviewTransitionTime);
    211         mOverlayTransitionTime = res.getInteger(R.integer.config_overlayTransitionTime);
    212         mSpringLoadedShrinkFactor =
    213                 res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100f;
    214         mOverviewModeShrinkFactor =
    215                 res.getInteger(R.integer.config_workspaceOverviewShrinkPercentage) / 100f;
    216         mWorkspaceScrimAlpha = res.getInteger(R.integer.config_workspaceScrimAlpha) / 100f;
    217         mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
    218     }
    219 
    220     public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState,
    221             int toPage, boolean animated, HashMap<View, Integer> layerViews) {
    222         AccessibilityManager am = (AccessibilityManager)
    223                 mLauncher.getSystemService(Context.ACCESSIBILITY_SERVICE);
    224         final boolean accessibilityEnabled = am.isEnabled();
    225         TransitionStates states = new TransitionStates(fromState, toState);
    226         int workspaceDuration = getAnimationDuration(states);
    227         animateWorkspace(states, toPage, animated, workspaceDuration, layerViews,
    228                 accessibilityEnabled);
    229         animateBackgroundGradient(states, animated, BACKGROUND_FADE_OUT_DURATION);
    230         return mStateAnimator;
    231     }
    232 
    233     public float getFinalScale() {
    234         return mNewScale;
    235     }
    236 
    237     /**
    238      * Reinitializes the arrays that we need for the animations on each page.
    239      */
    240     private void reinitializeAnimationArrays() {
    241         final int childCount = mWorkspace.getChildCount();
    242         if (mLastChildCount == childCount) return;
    243 
    244         mOldBackgroundAlphas = new float[childCount];
    245         mOldAlphas = new float[childCount];
    246         mNewBackgroundAlphas = new float[childCount];
    247         mNewAlphas = new float[childCount];
    248     }
    249 
    250     /**
    251      * Returns the proper animation duration for a transition.
    252      */
    253     private int getAnimationDuration(TransitionStates states) {
    254         if (states.workspaceToAllApps || states.overviewToAllApps) {
    255             return mAllAppsTransitionTime;
    256         } else if (states.workspaceToOverview || states.overviewToWorkspace) {
    257             return mOverviewTransitionTime;
    258         } else {
    259             return mOverlayTransitionTime;
    260         }
    261     }
    262 
    263     /**
    264      * Starts a transition animation for the workspace.
    265      */
    266     private void animateWorkspace(final TransitionStates states, int toPage, final boolean animated,
    267                                   final int duration, final HashMap<View, Integer> layerViews,
    268                                   final boolean accessibilityEnabled) {
    269         // Reinitialize animation arrays for the current workspace state
    270         reinitializeAnimationArrays();
    271 
    272         // Cancel existing workspace animations and create a new animator set if requested
    273         cancelAnimation();
    274         if (animated) {
    275             mStateAnimator = LauncherAnimUtils.createAnimatorSet();
    276         }
    277 
    278         // Update the workspace state
    279         float finalBackgroundAlpha = (states.stateIsSpringLoaded || states.stateIsOverview) ?
    280                 1.0f : 0f;
    281         float finalHotseatAndPageIndicatorAlpha = (states.stateIsNormal || states.stateIsSpringLoaded) ?
    282                 1f : 0f;
    283         float finalOverviewPanelAlpha = states.stateIsOverview ? 1f : 0f;
    284         float finalWorkspaceTranslationY = states.stateIsOverview || states.stateIsOverviewHidden ?
    285                 mWorkspace.getOverviewModeTranslationY() : 0;
    286 
    287         final int childCount = mWorkspace.getChildCount();
    288         final int customPageCount = mWorkspace.numCustomPages();
    289 
    290         mNewScale = 1.0f;
    291 
    292         if (states.oldStateIsOverview) {
    293             mWorkspace.disableFreeScroll();
    294         } else if (states.stateIsOverview) {
    295             mWorkspace.enableFreeScroll();
    296         }
    297 
    298         if (!states.stateIsNormal) {
    299             if (states.stateIsSpringLoaded) {
    300                 mNewScale = mSpringLoadedShrinkFactor;
    301             } else if (states.stateIsOverview || states.stateIsOverviewHidden) {
    302                 mNewScale = mOverviewModeShrinkFactor;
    303             }
    304         }
    305 
    306         if (toPage == SCROLL_TO_CURRENT_PAGE) {
    307             toPage = mWorkspace.getPageNearestToCenterOfScreen();
    308         }
    309         mWorkspace.snapToPage(toPage, duration, mZoomInInterpolator);
    310 
    311         for (int i = 0; i < childCount; i++) {
    312             final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i);
    313             boolean isCurrentPage = (i == toPage);
    314             float initialAlpha = cl.getShortcutsAndWidgets().getAlpha();
    315             float finalAlpha;
    316             if (states.stateIsNormalHidden || states.stateIsOverviewHidden) {
    317                 finalAlpha = 0f;
    318             } else if (states.stateIsNormal && mWorkspaceFadeInAdjacentScreens) {
    319                 finalAlpha = (i == toPage || i < customPageCount) ? 1f : 0f;
    320             } else {
    321                 finalAlpha = 1f;
    322             }
    323 
    324             // If we are animating to/from the small state, then hide the side pages and fade the
    325             // current page in
    326             if (!mWorkspace.isSwitchingState()) {
    327                 if (states.workspaceToAllApps || states.allAppsToWorkspace) {
    328                     if (states.allAppsToWorkspace && isCurrentPage) {
    329                         initialAlpha = 0f;
    330                     } else if (!isCurrentPage) {
    331                         initialAlpha = finalAlpha = 0f;
    332                     }
    333                     cl.setShortcutAndWidgetAlpha(initialAlpha);
    334                 }
    335             }
    336 
    337             mOldAlphas[i] = initialAlpha;
    338             mNewAlphas[i] = finalAlpha;
    339             if (animated) {
    340                 mOldBackgroundAlphas[i] = cl.getBackgroundAlpha();
    341                 mNewBackgroundAlphas[i] = finalBackgroundAlpha;
    342             } else {
    343                 cl.setBackgroundAlpha(finalBackgroundAlpha);
    344                 cl.setShortcutAndWidgetAlpha(finalAlpha);
    345             }
    346         }
    347 
    348         final ViewGroup overviewPanel = mLauncher.getOverviewPanel();
    349         final View hotseat = mLauncher.getHotseat();
    350         final View pageIndicator = mWorkspace.getPageIndicator();
    351         if (animated) {
    352             LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(mWorkspace);
    353             scale.scaleX(mNewScale)
    354                     .scaleY(mNewScale)
    355                     .translationY(finalWorkspaceTranslationY)
    356                     .setDuration(duration)
    357                     .setInterpolator(mZoomInInterpolator);
    358             mStateAnimator.play(scale);
    359             for (int index = 0; index < childCount; index++) {
    360                 final int i = index;
    361                 final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i);
    362                 float currentAlpha = cl.getShortcutsAndWidgets().getAlpha();
    363                 if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) {
    364                     cl.setBackgroundAlpha(mNewBackgroundAlphas[i]);
    365                     cl.setShortcutAndWidgetAlpha(mNewAlphas[i]);
    366                 } else {
    367                     if (layerViews != null) {
    368                         layerViews.put(cl, LauncherStateTransitionAnimation.BUILD_LAYER);
    369                     }
    370                     if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) {
    371                         LauncherViewPropertyAnimator alphaAnim =
    372                                 new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets());
    373                         alphaAnim.alpha(mNewAlphas[i])
    374                                 .setDuration(duration)
    375                                 .setInterpolator(mZoomInInterpolator);
    376                         mStateAnimator.play(alphaAnim);
    377                     }
    378                     if (mOldBackgroundAlphas[i] != 0 ||
    379                             mNewBackgroundAlphas[i] != 0) {
    380                         ValueAnimator bgAnim = ObjectAnimator.ofFloat(cl, "backgroundAlpha",
    381                                 mOldBackgroundAlphas[i], mNewBackgroundAlphas[i]);
    382                                 LauncherAnimUtils.ofFloat(cl, 0f, 1f);
    383                         bgAnim.setInterpolator(mZoomInInterpolator);
    384                         bgAnim.setDuration(duration);
    385                         mStateAnimator.play(bgAnim);
    386                     }
    387                 }
    388             }
    389             Animator pageIndicatorAlpha;
    390             if (pageIndicator != null) {
    391                 pageIndicatorAlpha = new LauncherViewPropertyAnimator(pageIndicator)
    392                         .alpha(finalHotseatAndPageIndicatorAlpha).withLayer();
    393                 pageIndicatorAlpha.addListener(new AlphaUpdateListener(pageIndicator,
    394                         accessibilityEnabled));
    395             } else {
    396                 // create a dummy animation so we don't need to do null checks later
    397                 pageIndicatorAlpha = ValueAnimator.ofFloat(0, 0);
    398             }
    399 
    400             LauncherViewPropertyAnimator hotseatAlpha = new LauncherViewPropertyAnimator(hotseat)
    401                     .alpha(finalHotseatAndPageIndicatorAlpha);
    402             hotseatAlpha.addListener(new AlphaUpdateListener(hotseat, accessibilityEnabled));
    403 
    404             LauncherViewPropertyAnimator overviewPanelAlpha =
    405                     new LauncherViewPropertyAnimator(overviewPanel).alpha(finalOverviewPanelAlpha);
    406             overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel,
    407                     accessibilityEnabled));
    408 
    409             // For animation optimations, we may need to provide the Launcher transition
    410             // with a set of views on which to force build layers in certain scenarios.
    411             hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    412             overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    413             if (layerViews != null) {
    414                 // If layerViews is not null, we add these views, and indicate that
    415                 // the caller can manage layer state.
    416                 layerViews.put(hotseat, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
    417                 layerViews.put(overviewPanel, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
    418             } else {
    419                 // Otherwise let the animator handle layer management.
    420                 hotseatAlpha.withLayer();
    421                 overviewPanelAlpha.withLayer();
    422             }
    423 
    424             if (states.workspaceToOverview) {
    425                 pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2));
    426                 hotseatAlpha.setInterpolator(new DecelerateInterpolator(2));
    427                 overviewPanelAlpha.setInterpolator(null);
    428             } else if (states.overviewToWorkspace) {
    429                 pageIndicatorAlpha.setInterpolator(null);
    430                 hotseatAlpha.setInterpolator(null);
    431                 overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2));
    432             }
    433 
    434             overviewPanelAlpha.setDuration(duration);
    435             pageIndicatorAlpha.setDuration(duration);
    436             hotseatAlpha.setDuration(duration);
    437 
    438             mStateAnimator.play(overviewPanelAlpha);
    439             mStateAnimator.play(hotseatAlpha);
    440             mStateAnimator.play(pageIndicatorAlpha);
    441             mStateAnimator.addListener(new AnimatorListenerAdapter() {
    442                 @Override
    443                 public void onAnimationEnd(Animator animation) {
    444                     mStateAnimator = null;
    445 
    446                     if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) {
    447                         overviewPanel.getChildAt(0).performAccessibilityAction(
    448                                 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
    449                     }
    450                 }
    451             });
    452         } else {
    453             overviewPanel.setAlpha(finalOverviewPanelAlpha);
    454             AlphaUpdateListener.updateVisibility(overviewPanel, accessibilityEnabled);
    455             hotseat.setAlpha(finalHotseatAndPageIndicatorAlpha);
    456             AlphaUpdateListener.updateVisibility(hotseat, accessibilityEnabled);
    457             if (pageIndicator != null) {
    458                 pageIndicator.setAlpha(finalHotseatAndPageIndicatorAlpha);
    459                 AlphaUpdateListener.updateVisibility(pageIndicator, accessibilityEnabled);
    460             }
    461             mWorkspace.updateCustomContentVisibility();
    462             mWorkspace.setScaleX(mNewScale);
    463             mWorkspace.setScaleY(mNewScale);
    464             mWorkspace.setTranslationY(finalWorkspaceTranslationY);
    465 
    466             if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) {
    467                 overviewPanel.getChildAt(0).performAccessibilityAction(
    468                         AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
    469             }
    470         }
    471     }
    472 
    473     /**
    474      * Animates the background scrim. Add to the state animator to prevent jankiness.
    475      *
    476      * @param states the current and final workspace states
    477      * @param animated whether or not to set the background alpha immediately
    478      * @duration duration of the animation
    479      */
    480     private void animateBackgroundGradient(TransitionStates states,
    481             boolean animated, int duration) {
    482 
    483         final DragLayer dragLayer = mLauncher.getDragLayer();
    484         final float startAlpha = dragLayer.getBackgroundAlpha();
    485         float finalAlpha = states.stateIsNormal ? 0 : mWorkspaceScrimAlpha;
    486 
    487         if (finalAlpha != startAlpha) {
    488             if (animated) {
    489                 // These properties refer to the background protection gradient used for AllApps
    490                 // and Widget tray.
    491                 ValueAnimator bgFadeOutAnimation =
    492                         LauncherAnimUtils.ofFloat(mWorkspace, startAlpha, finalAlpha);
    493                 bgFadeOutAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    494                     @Override
    495                     public void onAnimationUpdate(ValueAnimator animation) {
    496                         dragLayer.setBackgroundAlpha(
    497                                 ((Float)animation.getAnimatedValue()).floatValue());
    498                     }
    499                 });
    500                 bgFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f));
    501                 bgFadeOutAnimation.setDuration(duration);
    502                 mStateAnimator.play(bgFadeOutAnimation);
    503             } else {
    504                 dragLayer.setBackgroundAlpha(finalAlpha);
    505             }
    506         }
    507     }
    508 
    509     /**
    510      * Cancels the current animation.
    511      */
    512     private void cancelAnimation() {
    513         if (mStateAnimator != null) {
    514             mStateAnimator.setDuration(0);
    515             mStateAnimator.cancel();
    516         }
    517         mStateAnimator = null;
    518     }
    519 }