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 static final int FOLDER_NAME_ALPHA_DURATION = 32;
63 
64     private Folder mFolder;
65     private FolderPagedView mContent;
66     private GradientDrawable mFolderBackground;
67 
68     private FolderIcon mFolderIcon;
69     private PreviewBackground mPreviewBackground;
70 
71     private Context mContext;
72     private Launcher mLauncher;
73 
74     private final boolean mIsOpening;
75 
76     private final int mDuration;
77     private final int mDelay;
78 
79     private final TimeInterpolator mFolderInterpolator;
80     private final TimeInterpolator mLargeFolderPreviewItemOpenInterpolator;
81     private final TimeInterpolator mLargeFolderPreviewItemCloseInterpolator;
82 
83     private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
84     private final FolderGridOrganizer mPreviewVerifier;
85 
FolderAnimationManager(Folder folder, boolean isOpening)86     public FolderAnimationManager(Folder folder, boolean isOpening) {
87         mFolder = folder;
88         mContent = folder.mContent;
89         mFolderBackground = (GradientDrawable) mFolder.getBackground();
90 
91         mFolderIcon = folder.mFolderIcon;
92         mPreviewBackground = mFolderIcon.mBackground;
93 
94         mContext = folder.getContext();
95         mLauncher = folder.mLauncher;
96         mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
97 
98         mIsOpening = isOpening;
99 
100         Resources res = mContent.getResources();
101         mDuration = res.getInteger(R.integer.config_materialFolderExpandDuration);
102         mDelay = res.getInteger(R.integer.config_folderDelay);
103 
104         mFolderInterpolator = AnimationUtils.loadInterpolator(mContext,
105                 R.interpolator.folder_interpolator);
106         mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext,
107                 R.interpolator.large_folder_preview_item_open_interpolator);
108         mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext,
109                 R.interpolator.large_folder_preview_item_close_interpolator);
110     }
111 
112 
113     /**
114      * Prepares the Folder for animating between open / closed states.
115      */
getAnimator()116     public AnimatorSet getAnimator() {
117         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams();
118         mFolderIcon.getPreviewItemManager().recomputePreviewDrawingParams();
119         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
120         final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(0);
121 
122         // Match position of the FolderIcon
123         final Rect folderIconPos = new Rect();
124         float scaleRelativeToDragLayer = mLauncher.getDragLayer()
125                 .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos);
126         int scaledRadius = mPreviewBackground.getScaledRadius();
127         float initialSize = (scaledRadius * 2) * scaleRelativeToDragLayer;
128 
129         // Match size/scale of icons in the preview
130         float previewScale = rule.scaleForItem(itemsInPreview.size());
131         float previewSize = rule.getIconSize() * previewScale;
132         float initialScale = previewSize / itemsInPreview.get(0).getIconSize()
133                 * scaleRelativeToDragLayer;
134         final float finalScale = 1f;
135         float scale = mIsOpening ? initialScale : finalScale;
136         mFolder.setPivotX(0);
137         mFolder.setPivotY(0);
138 
139         // Scale the contents of the folder.
140         mFolder.mContent.setScaleX(scale);
141         mFolder.mContent.setScaleY(scale);
142         mFolder.mContent.setPivotX(0);
143         mFolder.mContent.setPivotY(0);
144         mFolder.mFooter.setScaleX(scale);
145         mFolder.mFooter.setScaleY(scale);
146         mFolder.mFooter.setPivotX(0);
147         mFolder.mFooter.setPivotY(0);
148 
149         // We want to create a small X offset for the preview items, so that they follow their
150         // expected path to their final locations. ie. an icon should not move right, if it's final
151         // location is to its left. This value is arbitrarily defined.
152         int previewItemOffsetX = (int) (previewSize / 2);
153         if (Utilities.isRtl(mContext.getResources())) {
154             previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX);
155         }
156 
157         final int paddingOffsetX = (int) (mContent.getPaddingLeft() * initialScale);
158         final int paddingOffsetY = (int) (mContent.getPaddingTop() * initialScale);
159 
160         int initialX = folderIconPos.left + mFolder.getPaddingLeft()
161                 + mPreviewBackground.getOffsetX() - paddingOffsetX - previewItemOffsetX;
162         int initialY = folderIconPos.top + mFolder.getPaddingTop()
163                 + mPreviewBackground.getOffsetY() - paddingOffsetY;
164         final float xDistance = initialX - lp.x;
165         final float yDistance = initialY - lp.y;
166 
167         // Set up the Folder background.
168         final int finalColor = ColorUtils.setAlphaComponent(
169                 Themes.getAttrColor(mContext, R.attr.folderFillColor), 255);
170         final int initialColor = setColorAlphaBound(
171                 finalColor, mPreviewBackground.getBackgroundAlpha());
172         mFolderBackground.mutate();
173         mFolderBackground.setColor(mIsOpening ? initialColor : finalColor);
174 
175         // Set up the reveal animation that clips the Folder.
176         int totalOffsetX = paddingOffsetX + previewItemOffsetX;
177         Rect startRect = new Rect(totalOffsetX,
178                 paddingOffsetY,
179                 Math.round((totalOffsetX + initialSize)),
180                 Math.round((paddingOffsetY + initialSize)));
181         Rect endRect = new Rect(0, 0, lp.width, lp.height);
182         float finalRadius = ResourceUtils.pxFromDp(2, mContext.getResources().getDisplayMetrics());
183 
184         // Create the animators.
185         AnimatorSet a = new AnimatorSet();
186 
187         // Initialize the Folder items' text.
188         PropertyResetListener colorResetListener =
189                 new PropertyResetListener<>(TEXT_ALPHA_PROPERTY, 1f);
190         for (BubbleTextView icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) {
191             if (mIsOpening) {
192                 icon.setTextVisibility(false);
193             }
194             ObjectAnimator anim = icon.createTextAlphaAnimator(mIsOpening);
195             anim.addListener(colorResetListener);
196             play(a, anim);
197         }
198 
199         play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f));
200         play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f));
201         play(a, getAnimator(mFolder.mContent, SCALE_PROPERTY, initialScale, finalScale));
202         play(a, getAnimator(mFolder.mFooter, SCALE_PROPERTY, initialScale, finalScale));
203         play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor));
204         play(a, mFolderIcon.mFolderName.createTextAlphaAnimator(!mIsOpening));
205         play(a, getShape().createRevealAnimator(
206                 mFolder, startRect, endRect, finalRadius, !mIsOpening));
207         // Fade in the folder name, as the text can overlap the icons when grid size is small.
208         mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f);
209         play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1),
210                 mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0,
211                 mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION);
212 
213         // Translate the footer so that it tracks the bottom of the content.
214         float normalHeight = mFolder.getContentAreaHeight();
215         float scaledHeight = normalHeight * initialScale;
216         float diff = normalHeight - scaledHeight;
217         play(a, getAnimator(mFolder.mFooter, View.TRANSLATION_Y, -diff, 0f));
218 
219         // Animate the elevation midway so that the shadow is not noticeable in the background.
220         int midDuration = mDuration / 2;
221         Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0);
222         play(a, z, mIsOpening ? midDuration : 0, midDuration);
223 
224         // Store clip variables
225         CellLayout cellLayout = mContent.getCurrentCellLayout();
226         boolean folderClipChildren = mFolder.getClipChildren();
227         boolean folderClipToPadding = mFolder.getClipToPadding();
228         boolean contentClipChildren = mContent.getClipChildren();
229         boolean contentClipToPadding = mContent.getClipToPadding();
230         boolean cellLayoutClipChildren = cellLayout.getClipChildren();
231         boolean cellLayoutClipPadding = cellLayout.getClipToPadding();
232 
233         mFolder.setClipChildren(false);
234         mFolder.setClipToPadding(false);
235         mContent.setClipChildren(false);
236         mContent.setClipToPadding(false);
237         cellLayout.setClipChildren(false);
238         cellLayout.setClipToPadding(false);
239 
240         a.addListener(new AnimatorListenerAdapter() {
241             @Override
242             public void onAnimationEnd(Animator animation) {
243                 super.onAnimationEnd(animation);
244                 mFolder.setTranslationX(0.0f);
245                 mFolder.setTranslationY(0.0f);
246                 mFolder.setTranslationZ(0.0f);
247                 mFolder.mContent.setScaleX(1f);
248                 mFolder.mContent.setScaleY(1f);
249                 mFolder.mFooter.setScaleX(1f);
250                 mFolder.mFooter.setScaleY(1f);
251                 mFolder.mFooter.setTranslationX(0f);
252                 mFolder.mFolderName.setAlpha(1f);
253 
254                 mFolder.setClipChildren(folderClipChildren);
255                 mFolder.setClipToPadding(folderClipToPadding);
256                 mContent.setClipChildren(contentClipChildren);
257                 mContent.setClipToPadding(contentClipToPadding);
258                 cellLayout.setClipChildren(cellLayoutClipChildren);
259                 cellLayout.setClipToPadding(cellLayoutClipPadding);
260 
261             }
262         });
263 
264         // We set the interpolator on all current child animators here, because the preview item
265         // animators may use a different interpolator.
266         for (Animator animator : a.getChildAnimations()) {
267             animator.setInterpolator(mFolderInterpolator);
268         }
269 
270         int radiusDiff = scaledRadius - mPreviewBackground.getRadius();
271         addPreviewItemAnimators(a, initialScale / scaleRelativeToDragLayer,
272                 // Background can have a scaled radius in drag and drop mode, so we need to add the
273                 // difference to keep the preview items centered.
274                 previewItemOffsetX + radiusDiff, radiusDiff);
275         return a;
276     }
277 
278     /**
279      * Returns the list of "preview items" on {@param page}.
280      */
getPreviewIconsOnPage(int page)281     private List<BubbleTextView> getPreviewIconsOnPage(int page) {
282         return mPreviewVerifier.setFolderInfo(mFolder.mInfo)
283                 .previewItemsForPage(page, mFolder.getIconsInReadingOrder());
284     }
285 
286     /**
287      * Animate the items on the current page.
288      */
addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale, int previewItemOffsetX, int previewItemOffsetY)289     private void addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale,
290             int previewItemOffsetX, int previewItemOffsetY) {
291         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
292         boolean isOnFirstPage = mFolder.mContent.getCurrentPage() == 0;
293         final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(
294                 isOnFirstPage ? 0 : mFolder.mContent.getCurrentPage());
295         final int numItemsInPreview = itemsInPreview.size();
296         final int numItemsInFirstPagePreview = isOnFirstPage
297                 ? numItemsInPreview : MAX_NUM_ITEMS_IN_PREVIEW;
298 
299         TimeInterpolator previewItemInterpolator = getPreviewItemInterpolator();
300 
301         ShortcutAndWidgetContainer cwc = mContent.getPageAt(0).getShortcutsAndWidgets();
302         for (int i = 0; i < numItemsInPreview; ++i) {
303             final BubbleTextView btv = itemsInPreview.get(i);
304             CellLayout.LayoutParams btvLp = (CellLayout.LayoutParams) btv.getLayoutParams();
305 
306             // Calculate the final values in the LayoutParams.
307             btvLp.isLockedToGrid = true;
308             cwc.setupLp(btv);
309 
310             // Match scale of icons in the preview of the items on the first page.
311             float previewScale = rule.scaleForItem(numItemsInFirstPagePreview);
312             float previewSize = rule.getIconSize() * previewScale;
313             float iconScale = previewSize / itemsInPreview.get(i).getIconSize();
314 
315             final float initialScale = iconScale / folderScale;
316             final float finalScale = 1f;
317             float scale = mIsOpening ? initialScale : finalScale;
318             btv.setScaleX(scale);
319             btv.setScaleY(scale);
320 
321             // Match positions of the icons in the folder with their positions in the preview
322             rule.computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, mTmpParams);
323             // The PreviewLayoutRule assumes that the icon size takes up the entire width so we
324             // offset by the actual size.
325             int iconOffsetX = (int) ((btvLp.width - btv.getIconSize()) * iconScale) / 2;
326 
327             final int previewPosX =
328                     (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale);
329             final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY) / folderScale);
330 
331             final float xDistance = previewPosX - btvLp.x;
332             final float yDistance = previewPosY - btvLp.y;
333 
334             Animator translationX = getAnimator(btv, View.TRANSLATION_X, xDistance, 0f);
335             translationX.setInterpolator(previewItemInterpolator);
336             play(animatorSet, translationX);
337 
338             Animator translationY = getAnimator(btv, View.TRANSLATION_Y, yDistance, 0f);
339             translationY.setInterpolator(previewItemInterpolator);
340             play(animatorSet, translationY);
341 
342             Animator scaleAnimator = getAnimator(btv, SCALE_PROPERTY, initialScale, finalScale);
343             scaleAnimator.setInterpolator(previewItemInterpolator);
344             play(animatorSet, scaleAnimator);
345 
346             if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) {
347                 // These delays allows the preview items to move as part of the Folder's motion,
348                 // and its only necessary for large folders because of differing interpolators.
349                 int delay = mIsOpening ? mDelay : mDelay * 2;
350                 if (mIsOpening) {
351                     translationX.setStartDelay(delay);
352                     translationY.setStartDelay(delay);
353                     scaleAnimator.setStartDelay(delay);
354                 }
355                 translationX.setDuration(translationX.getDuration() - delay);
356                 translationY.setDuration(translationY.getDuration() - delay);
357                 scaleAnimator.setDuration(scaleAnimator.getDuration() - delay);
358             }
359 
360             animatorSet.addListener(new AnimatorListenerAdapter() {
361                 @Override
362                 public void onAnimationStart(Animator animation) {
363                     super.onAnimationStart(animation);
364                     // Necessary to initialize values here because of the start delay.
365                     if (mIsOpening) {
366                         btv.setTranslationX(xDistance);
367                         btv.setTranslationY(yDistance);
368                         btv.setScaleX(initialScale);
369                         btv.setScaleY(initialScale);
370                     }
371                 }
372 
373                 @Override
374                 public void onAnimationEnd(Animator animation) {
375                     super.onAnimationEnd(animation);
376                     btv.setTranslationX(0.0f);
377                     btv.setTranslationY(0.0f);
378                     btv.setScaleX(1f);
379                     btv.setScaleY(1f);
380                 }
381             });
382         }
383     }
384 
play(AnimatorSet as, Animator a)385     private void play(AnimatorSet as, Animator a) {
386         play(as, a, a.getStartDelay(), mDuration);
387     }
388 
play(AnimatorSet as, Animator a, long startDelay, int duration)389     private void play(AnimatorSet as, Animator a, long startDelay, int duration) {
390         a.setStartDelay(startDelay);
391         a.setDuration(duration);
392         as.play(a);
393     }
394 
getPreviewItemInterpolator()395     private TimeInterpolator getPreviewItemInterpolator() {
396         if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) {
397             // With larger folders, we want the preview items to reach their final positions faster
398             // (when opening) and later (when closing) so that they appear aligned with the rest of
399             // the folder items when they are both visible.
400             return mIsOpening
401                     ? mLargeFolderPreviewItemOpenInterpolator
402                     : mLargeFolderPreviewItemCloseInterpolator;
403         }
404         return mFolderInterpolator;
405     }
406 
getAnimator(View view, Property property, float v1, float v2)407     private Animator getAnimator(View view, Property property, float v1, float v2) {
408         return mIsOpening
409                 ? ObjectAnimator.ofFloat(view, property, v1, v2)
410                 : ObjectAnimator.ofFloat(view, property, v2, v1);
411     }
412 
getAnimator(GradientDrawable drawable, String property, int v1, int v2)413     private Animator getAnimator(GradientDrawable drawable, String property, int v1, int v2) {
414         return mIsOpening
415                 ? ObjectAnimator.ofArgb(drawable, property, v1, v2)
416                 : ObjectAnimator.ofArgb(drawable, property, v2, v1);
417     }
418 }
419