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