Home | History | Annotate | Download | only in folder
      1 /*
      2  * Copyright (C) 2017 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.folder;
     18 
     19 import static com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY;
     20 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
     21 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
     22 
     23 import android.animation.Animator;
     24 import android.animation.AnimatorListenerAdapter;
     25 import android.animation.AnimatorSet;
     26 import android.animation.ObjectAnimator;
     27 import android.animation.TimeInterpolator;
     28 import android.content.Context;
     29 import android.content.res.Resources;
     30 import android.graphics.Color;
     31 import android.graphics.Rect;
     32 import android.graphics.drawable.GradientDrawable;
     33 import android.support.v4.graphics.ColorUtils;
     34 import android.util.Property;
     35 import android.view.View;
     36 import android.view.animation.AnimationUtils;
     37 
     38 import com.android.launcher3.BubbleTextView;
     39 import com.android.launcher3.CellLayout;
     40 import com.android.launcher3.Launcher;
     41 import com.android.launcher3.LauncherAnimUtils;
     42 import com.android.launcher3.R;
     43 import com.android.launcher3.ShortcutAndWidgetContainer;
     44 import com.android.launcher3.Utilities;
     45 import com.android.launcher3.anim.PropertyResetListener;
     46 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
     47 import com.android.launcher3.dragndrop.DragLayer;
     48 import com.android.launcher3.util.Themes;
     49 
     50 import java.util.List;
     51 
     52 /**
     53  * Manages the opening and closing animations for a {@link Folder}.
     54  *
     55  * All of the animations are done in the Folder.
     56  * ie. When the user taps on the FolderIcon, we immediately hide the FolderIcon and show the Folder
     57  * in its place before starting the animation.
     58  */
     59 public class FolderAnimationManager {
     60 
     61     private Folder mFolder;
     62     private FolderPagedView mContent;
     63     private GradientDrawable mFolderBackground;
     64 
     65     private FolderIcon mFolderIcon;
     66     private PreviewBackground mPreviewBackground;
     67 
     68     private Context mContext;
     69     private Launcher mLauncher;
     70 
     71     private final boolean mIsOpening;
     72 
     73     private final int mDuration;
     74     private final int mDelay;
     75 
     76     private final TimeInterpolator mFolderInterpolator;
     77     private final TimeInterpolator mLargeFolderPreviewItemOpenInterpolator;
     78     private final TimeInterpolator mLargeFolderPreviewItemCloseInterpolator;
     79 
     80     private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
     81 
     82 
     83     public FolderAnimationManager(Folder folder, boolean isOpening) {
     84         mFolder = folder;
     85         mContent = folder.mContent;
     86         mFolderBackground = (GradientDrawable) mFolder.getBackground();
     87 
     88         mFolderIcon = folder.mFolderIcon;
     89         mPreviewBackground = mFolderIcon.mBackground;
     90 
     91         mContext = folder.getContext();
     92         mLauncher = folder.mLauncher;
     93 
     94         mIsOpening = isOpening;
     95 
     96         Resources res = mContent.getResources();
     97         mDuration = res.getInteger(R.integer.config_materialFolderExpandDuration);
     98         mDelay = res.getInteger(R.integer.config_folderDelay);
     99 
    100         mFolderInterpolator = AnimationUtils.loadInterpolator(mContext,
    101                 R.interpolator.folder_interpolator);
    102         mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext,
    103                 R.interpolator.large_folder_preview_item_open_interpolator);
    104         mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext,
    105                 R.interpolator.large_folder_preview_item_close_interpolator);
    106     }
    107 
    108 
    109     /**
    110      * Prepares the Folder for animating between open / closed states.
    111      */
    112     public AnimatorSet getAnimator() {
    113         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams();
    114         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
    115         final List<BubbleTextView> itemsInPreview = mFolderIcon.getPreviewItems();
    116 
    117         // Match position of the FolderIcon
    118         final Rect folderIconPos = new Rect();
    119         float scaleRelativeToDragLayer = mLauncher.getDragLayer()
    120                 .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos);
    121         int scaledRadius = mPreviewBackground.getScaledRadius();
    122         float initialSize = (scaledRadius * 2) * scaleRelativeToDragLayer;
    123 
    124         // Match size/scale of icons in the preview
    125         float previewScale = rule.scaleForItem(itemsInPreview.size());
    126         float previewSize = rule.getIconSize() * previewScale;
    127         float initialScale = previewSize / itemsInPreview.get(0).getIconSize()
    128                 * scaleRelativeToDragLayer;
    129         final float finalScale = 1f;
    130         float scale = mIsOpening ? initialScale : finalScale;
    131         mFolder.setScaleX(scale);
    132         mFolder.setScaleY(scale);
    133         mFolder.setPivotX(0);
    134         mFolder.setPivotY(0);
    135 
    136         // We want to create a small X offset for the preview items, so that they follow their
    137         // expected path to their final locations. ie. an icon should not move right, if it's final
    138         // location is to its left. This value is arbitrarily defined.
    139         int previewItemOffsetX = (int) (previewSize / 2);
    140         if (Utilities.isRtl(mContext.getResources())) {
    141             previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX);
    142         }
    143 
    144         final int paddingOffsetX = (int) ((mFolder.getPaddingLeft() + mContent.getPaddingLeft())
    145                 * initialScale);
    146         final int paddingOffsetY = (int) ((mFolder.getPaddingTop() + mContent.getPaddingTop())
    147                 * initialScale);
    148 
    149         int initialX = folderIconPos.left + mPreviewBackground.getOffsetX() - paddingOffsetX
    150                 - previewItemOffsetX;
    151         int initialY = folderIconPos.top + mPreviewBackground.getOffsetY() - paddingOffsetY;
    152         final float xDistance = initialX - lp.x;
    153         final float yDistance = initialY - lp.y;
    154 
    155         // Set up the Folder background.
    156         final int finalColor = Themes.getAttrColor(mContext, android.R.attr.colorPrimary);
    157         final int initialColor =
    158                 ColorUtils.setAlphaComponent(finalColor, mPreviewBackground.getBackgroundAlpha());
    159         mFolderBackground.setColor(mIsOpening ? initialColor : finalColor);
    160 
    161         // Set up the reveal animation that clips the Folder.
    162         int totalOffsetX = paddingOffsetX + previewItemOffsetX;
    163         Rect startRect = new Rect(
    164                 Math.round(totalOffsetX / initialScale),
    165                 Math.round(paddingOffsetY / initialScale),
    166                 Math.round((totalOffsetX + initialSize) / initialScale),
    167                 Math.round((paddingOffsetY + initialSize) / initialScale));
    168         Rect endRect = new Rect(0, 0, lp.width, lp.height);
    169         float initialRadius = initialSize / initialScale / 2f;
    170         float finalRadius = Utilities.pxFromDp(2, mContext.getResources().getDisplayMetrics());
    171 
    172         // Create the animators.
    173         AnimatorSet a = LauncherAnimUtils.createAnimatorSet();
    174 
    175         // Initialize the Folder items' text.
    176         PropertyResetListener colorResetListener =
    177                 new PropertyResetListener<>(TEXT_ALPHA_PROPERTY, 1f);
    178         for (BubbleTextView icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) {
    179             if (mIsOpening) {
    180                 icon.setTextVisibility(false);
    181             }
    182             ObjectAnimator anim = icon.createTextAlphaAnimator(mIsOpening);
    183             anim.addListener(colorResetListener);
    184             play(a, anim);
    185         }
    186 
    187         play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f));
    188         play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f));
    189         play(a, getAnimator(mFolder, SCALE_PROPERTY, initialScale, finalScale));
    190         play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor));
    191         play(a, mFolderIcon.mFolderName.createTextAlphaAnimator(!mIsOpening));
    192         RoundedRectRevealOutlineProvider outlineProvider = new RoundedRectRevealOutlineProvider(
    193                 initialRadius, finalRadius, startRect, endRect) {
    194             @Override
    195             public boolean shouldRemoveElevationDuringAnimation() {
    196                 return true;
    197             }
    198         };
    199         play(a, outlineProvider.createRevealAnimator(mFolder, !mIsOpening));
    200 
    201         // Animate the elevation midway so that the shadow is not noticeable in the background.
    202         int midDuration = mDuration / 2;
    203         Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0);
    204         play(a, z, mIsOpening ? midDuration : 0, midDuration);
    205 
    206         a.addListener(new AnimatorListenerAdapter() {
    207             @Override
    208             public void onAnimationEnd(Animator animation) {
    209                 super.onAnimationEnd(animation);
    210                 mFolder.setTranslationX(0.0f);
    211                 mFolder.setTranslationY(0.0f);
    212                 mFolder.setTranslationZ(0.0f);
    213                 mFolder.setScaleX(1f);
    214                 mFolder.setScaleY(1f);
    215             }
    216         });
    217 
    218         // We set the interpolator on all current child animators here, because the preview item
    219         // animators may use a different interpolator.
    220         for (Animator animator : a.getChildAnimations()) {
    221             animator.setInterpolator(mFolderInterpolator);
    222         }
    223 
    224         int radiusDiff = scaledRadius - mPreviewBackground.getRadius();
    225         addPreviewItemAnimators(a, initialScale / scaleRelativeToDragLayer,
    226                 // Background can have a scaled radius in drag and drop mode, so we need to add the
    227                 // difference to keep the preview items centered.
    228                 previewItemOffsetX + radiusDiff, radiusDiff);
    229         return a;
    230     }
    231 
    232     /**
    233      * Animate the items on the current page.
    234      */
    235     private void addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale,
    236             int previewItemOffsetX, int previewItemOffsetY) {
    237         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
    238         boolean isOnFirstPage = mFolder.mContent.getCurrentPage() == 0;
    239         final List<BubbleTextView> itemsInPreview = isOnFirstPage
    240                 ? mFolderIcon.getPreviewItems()
    241                 : mFolderIcon.getPreviewItemsOnPage(mFolder.mContent.getCurrentPage());
    242         final int numItemsInPreview = itemsInPreview.size();
    243         final int numItemsInFirstPagePreview = isOnFirstPage
    244                 ? numItemsInPreview : MAX_NUM_ITEMS_IN_PREVIEW;
    245 
    246         TimeInterpolator previewItemInterpolator = getPreviewItemInterpolator();
    247 
    248         ShortcutAndWidgetContainer cwc = mContent.getPageAt(0).getShortcutsAndWidgets();
    249         for (int i = 0; i < numItemsInPreview; ++i) {
    250             final BubbleTextView btv = itemsInPreview.get(i);
    251             CellLayout.LayoutParams btvLp = (CellLayout.LayoutParams) btv.getLayoutParams();
    252 
    253             // Calculate the final values in the LayoutParams.
    254             btvLp.isLockedToGrid = true;
    255             cwc.setupLp(btv);
    256 
    257             // Match scale of icons in the preview of the items on the first page.
    258             float previewScale = rule.scaleForItem(numItemsInFirstPagePreview);
    259             float previewSize = rule.getIconSize() * previewScale;
    260             float iconScale = previewSize / itemsInPreview.get(i).getIconSize();
    261 
    262             final float initialScale = iconScale / folderScale;
    263             final float finalScale = 1f;
    264             float scale = mIsOpening ? initialScale : finalScale;
    265             btv.setScaleX(scale);
    266             btv.setScaleY(scale);
    267 
    268             // Match positions of the icons in the folder with their positions in the preview
    269             rule.computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, mTmpParams);
    270             // The PreviewLayoutRule assumes that the icon size takes up the entire width so we
    271             // offset by the actual size.
    272             int iconOffsetX = (int) ((btvLp.width - btv.getIconSize()) * iconScale) / 2;
    273 
    274             final int previewPosX =
    275                     (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale);
    276             final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY) / folderScale);
    277 
    278             final float xDistance = previewPosX - btvLp.x;
    279             final float yDistance = previewPosY - btvLp.y;
    280 
    281             Animator translationX = getAnimator(btv, View.TRANSLATION_X, xDistance, 0f);
    282             translationX.setInterpolator(previewItemInterpolator);
    283             play(animatorSet, translationX);
    284 
    285             Animator translationY = getAnimator(btv, View.TRANSLATION_Y, yDistance, 0f);
    286             translationY.setInterpolator(previewItemInterpolator);
    287             play(animatorSet, translationY);
    288 
    289             Animator scaleAnimator = getAnimator(btv, SCALE_PROPERTY, initialScale, finalScale);
    290             scaleAnimator.setInterpolator(previewItemInterpolator);
    291             play(animatorSet, scaleAnimator);
    292 
    293             if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) {
    294                 // These delays allows the preview items to move as part of the Folder's motion,
    295                 // and its only necessary for large folders because of differing interpolators.
    296                 int delay = mIsOpening ? mDelay : mDelay * 2;
    297                 if (mIsOpening) {
    298                     translationX.setStartDelay(delay);
    299                     translationY.setStartDelay(delay);
    300                     scaleAnimator.setStartDelay(delay);
    301                 }
    302                 translationX.setDuration(translationX.getDuration() - delay);
    303                 translationY.setDuration(translationY.getDuration() - delay);
    304                 scaleAnimator.setDuration(scaleAnimator.getDuration() - delay);
    305             }
    306 
    307             animatorSet.addListener(new AnimatorListenerAdapter() {
    308                 @Override
    309                 public void onAnimationStart(Animator animation) {
    310                     super.onAnimationStart(animation);
    311                     // Necessary to initialize values here because of the start delay.
    312                     if (mIsOpening) {
    313                         btv.setTranslationX(xDistance);
    314                         btv.setTranslationY(yDistance);
    315                         btv.setScaleX(initialScale);
    316                         btv.setScaleY(initialScale);
    317                     }
    318                 }
    319 
    320                 @Override
    321                 public void onAnimationEnd(Animator animation) {
    322                     super.onAnimationEnd(animation);
    323                     btv.setTranslationX(0.0f);
    324                     btv.setTranslationY(0.0f);
    325                     btv.setScaleX(1f);
    326                     btv.setScaleY(1f);
    327                 }
    328             });
    329         }
    330     }
    331 
    332     private void play(AnimatorSet as, Animator a) {
    333         play(as, a, a.getStartDelay(), mDuration);
    334     }
    335 
    336     private void play(AnimatorSet as, Animator a, long startDelay, int duration) {
    337         a.setStartDelay(startDelay);
    338         a.setDuration(duration);
    339         as.play(a);
    340     }
    341 
    342     private TimeInterpolator getPreviewItemInterpolator() {
    343         if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) {
    344             // With larger folders, we want the preview items to reach their final positions faster
    345             // (when opening) and later (when closing) so that they appear aligned with the rest of
    346             // the folder items when they are both visible.
    347             return mIsOpening
    348                     ? mLargeFolderPreviewItemOpenInterpolator
    349                     : mLargeFolderPreviewItemCloseInterpolator;
    350         }
    351         return mFolderInterpolator;
    352     }
    353 
    354     private Animator getAnimator(View view, Property property, float v1, float v2) {
    355         return mIsOpening
    356                 ? ObjectAnimator.ofFloat(view, property, v1, v2)
    357                 : ObjectAnimator.ofFloat(view, property, v2, v1);
    358     }
    359 
    360     private Animator getAnimator(GradientDrawable drawable, String property, int v1, int v2) {
    361         return mIsOpening
    362                 ? ObjectAnimator.ofArgb(drawable, property, v1, v2)
    363                 : ObjectAnimator.ofArgb(drawable, property, v2, v1);
    364     }
    365 }
    366