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