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