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