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