• 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 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
26 
27 import android.animation.Animator;
28 import android.animation.AnimatorListenerAdapter;
29 import android.animation.AnimatorSet;
30 import android.animation.ObjectAnimator;
31 import android.animation.TimeInterpolator;
32 import android.content.Context;
33 import android.content.res.Resources;
34 import android.graphics.Rect;
35 import android.graphics.drawable.GradientDrawable;
36 import android.util.Property;
37 import android.view.View;
38 import android.view.animation.AnimationUtils;
39 
40 import com.android.launcher3.BubbleTextView;
41 import com.android.launcher3.CellLayout;
42 import com.android.launcher3.DeviceProfile;
43 import com.android.launcher3.R;
44 import com.android.launcher3.ShortcutAndWidgetContainer;
45 import com.android.launcher3.Utilities;
46 import com.android.launcher3.anim.PropertyResetListener;
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.folder_interpolator);
109         mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext,
110                 R.interpolator.large_folder_preview_item_open_interpolator);
111         mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext,
112                 R.interpolator.large_folder_preview_item_close_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                 + mPreviewBackground.getOffsetX() - paddingOffsetX - previewItemOffsetX;
172         int initialY = folderIconPos.top + mFolder.getPaddingTop()
173                 + mPreviewBackground.getOffsetY() - paddingOffsetY;
174         final float xDistance = initialX - lp.x;
175         final float yDistance = initialY - lp.y;
176 
177         // Set up the Folder background.
178         final int finalColor;
179         int folderFillColor = Themes.getAttrColor(mContext, R.attr.folderFillColor);
180         if (mIsOpening) {
181             finalColor = folderFillColor;
182         } else {
183             finalColor = mFolderBackground.getColor().getDefaultColor();
184         }
185         final int initialColor = setColorAlphaBound(
186                 folderFillColor, mPreviewBackground.getBackgroundAlpha());
187         mFolderBackground.mutate();
188         mFolderBackground.setColor(mIsOpening ? initialColor : finalColor);
189 
190         // Set up the reveal animation that clips the Folder.
191         int totalOffsetX = paddingOffsetX + previewItemOffsetX;
192         Rect startRect = new Rect(totalOffsetX,
193                 paddingOffsetY,
194                 Math.round((totalOffsetX + initialSize)),
195                 Math.round((paddingOffsetY + initialSize)));
196         Rect endRect = new Rect(0, 0, lp.width, lp.height);
197         float finalRadius = mFolderBackground.getCornerRadius();
198 
199         // Create the animators.
200         AnimatorSet a = new AnimatorSet();
201 
202         // Initialize the Folder items' text.
203         PropertyResetListener colorResetListener =
204                 new PropertyResetListener<>(TEXT_ALPHA_PROPERTY, 1f);
205         for (BubbleTextView icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) {
206             if (mIsOpening) {
207                 icon.setTextVisibility(false);
208             }
209             ObjectAnimator anim = icon.createTextAlphaAnimator(mIsOpening);
210             anim.addListener(colorResetListener);
211             play(a, anim);
212         }
213 
214         mBgColorAnimator = getAnimator(mFolderBackground, "color", initialColor, finalColor);
215         play(a, mBgColorAnimator);
216         play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f));
217         play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f));
218         play(a, getAnimator(mFolder.mContent, SCALE_PROPERTY, initialScale, finalScale));
219         play(a, getAnimator(mFolder.mFooter, SCALE_PROPERTY, initialScale, finalScale));
220 
221         final int footerAlphaDuration;
222         final int footerStartDelay;
223         if (isLargeFolder()) {
224             if (mIsOpening) {
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         // Create reveal animator for the folder background
238         play(a, getShape().createRevealAnimator(
239                 mFolder, startRect, endRect, finalRadius, !mIsOpening));
240 
241         // Create reveal animator for the folder content (capture the top 4 icons 2x2)
242         int width = mDeviceProfile.folderCellLayoutBorderSpacingPx
243                 + mDeviceProfile.folderCellWidthPx * 2;
244         int height = mDeviceProfile.folderCellLayoutBorderSpacingPx
245                 + mDeviceProfile.folderCellHeightPx * 2;
246         int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage();
247         int left = mContent.getPaddingLeft() + page * lp.width;
248         Rect contentStart = new Rect(left, 0, left + width, height);
249         Rect contentEnd = new Rect(left, 0, left + lp.width, lp.height);
250         play(a, getShape().createRevealAnimator(
251                 mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening));
252 
253 
254         // Fade in the folder name, as the text can overlap the icons when grid size is small.
255         mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f);
256         play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1),
257                 mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0,
258                 mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION);
259 
260         // Translate the footer so that it tracks the bottom of the content.
261         float normalHeight = mFolder.getContentAreaHeight();
262         float scaledHeight = normalHeight * initialScale;
263         float diff = normalHeight - scaledHeight;
264         play(a, getAnimator(mFolder.mFooter, View.TRANSLATION_Y, -diff, 0f));
265 
266         // Animate the elevation midway so that the shadow is not noticeable in the background.
267         int midDuration = mDuration / 2;
268         Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0);
269         play(a, z, mIsOpening ? midDuration : 0, midDuration);
270 
271         // Store clip variables
272         CellLayout cellLayout = mContent.getCurrentCellLayout();
273         boolean folderClipChildren = mFolder.getClipChildren();
274         boolean folderClipToPadding = mFolder.getClipToPadding();
275         boolean contentClipChildren = mContent.getClipChildren();
276         boolean contentClipToPadding = mContent.getClipToPadding();
277         boolean cellLayoutClipChildren = cellLayout.getClipChildren();
278         boolean cellLayoutClipPadding = cellLayout.getClipToPadding();
279 
280         mFolder.setClipChildren(false);
281         mFolder.setClipToPadding(false);
282         mContent.setClipChildren(false);
283         mContent.setClipToPadding(false);
284         cellLayout.setClipChildren(false);
285         cellLayout.setClipToPadding(false);
286 
287         a.addListener(new AnimatorListenerAdapter() {
288             @Override
289             public void onAnimationEnd(Animator animation) {
290                 super.onAnimationEnd(animation);
291                 mFolder.setTranslationX(0.0f);
292                 mFolder.setTranslationY(0.0f);
293                 mFolder.setTranslationZ(0.0f);
294                 mFolder.mContent.setScaleX(1f);
295                 mFolder.mContent.setScaleY(1f);
296                 mFolder.mFooter.setScaleX(1f);
297                 mFolder.mFooter.setScaleY(1f);
298                 mFolder.mFooter.setTranslationX(0f);
299                 mFolder.mFolderName.setAlpha(1f);
300 
301                 mFolder.setClipChildren(folderClipChildren);
302                 mFolder.setClipToPadding(folderClipToPadding);
303                 mContent.setClipChildren(contentClipChildren);
304                 mContent.setClipToPadding(contentClipToPadding);
305                 cellLayout.setClipChildren(cellLayoutClipChildren);
306                 cellLayout.setClipToPadding(cellLayoutClipPadding);
307 
308             }
309         });
310 
311         // We set the interpolator on all current child animators here, because the preview item
312         // animators may use a different interpolator.
313         for (Animator animator : a.getChildAnimations()) {
314             animator.setInterpolator(mFolderInterpolator);
315         }
316 
317         int radiusDiff = scaledRadius - mPreviewBackground.getRadius();
318         addPreviewItemAnimators(a, initialScale / scaleRelativeToDragLayer,
319                 // Background can have a scaled radius in drag and drop mode, so we need to add the
320                 // difference to keep the preview items centered.
321                 previewItemOffsetX + radiusDiff, radiusDiff);
322         return a;
323     }
324 
325     /**
326      * Returns the list of "preview items" on {@param page}.
327      */
getPreviewIconsOnPage(int page)328     private List<BubbleTextView> getPreviewIconsOnPage(int page) {
329         return mPreviewVerifier.setFolderInfo(mFolder.mInfo)
330                 .previewItemsForPage(page, mFolder.getIconsInReadingOrder());
331     }
332 
333     /**
334      * Animate the items on the current page.
335      */
addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale, int previewItemOffsetX, int previewItemOffsetY)336     private void addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale,
337             int previewItemOffsetX, int previewItemOffsetY) {
338         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
339         boolean isOnFirstPage = mFolder.mContent.getCurrentPage() == 0;
340         final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(
341                 isOnFirstPage ? 0 : mFolder.mContent.getCurrentPage());
342         final int numItemsInPreview = itemsInPreview.size();
343         final int numItemsInFirstPagePreview = isOnFirstPage
344                 ? numItemsInPreview : MAX_NUM_ITEMS_IN_PREVIEW;
345 
346         TimeInterpolator previewItemInterpolator = getPreviewItemInterpolator();
347 
348         ShortcutAndWidgetContainer cwc = mContent.getPageAt(0).getShortcutsAndWidgets();
349         for (int i = 0; i < numItemsInPreview; ++i) {
350             final BubbleTextView btv = itemsInPreview.get(i);
351             CellLayout.LayoutParams btvLp = (CellLayout.LayoutParams) btv.getLayoutParams();
352 
353             // Calculate the final values in the LayoutParams.
354             btvLp.isLockedToGrid = true;
355             cwc.setupLp(btv);
356 
357             // Match scale of icons in the preview of the items on the first page.
358             float previewScale = rule.scaleForItem(numItemsInFirstPagePreview);
359             float previewSize = rule.getIconSize() * previewScale;
360             float iconScale = previewSize / itemsInPreview.get(i).getIconSize();
361 
362             final float initialScale = iconScale / folderScale;
363             final float finalScale = 1f;
364             float scale = mIsOpening ? initialScale : finalScale;
365             btv.setScaleX(scale);
366             btv.setScaleY(scale);
367 
368             // Match positions of the icons in the folder with their positions in the preview
369             rule.computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, mTmpParams);
370             // The PreviewLayoutRule assumes that the icon size takes up the entire width so we
371             // offset by the actual size.
372             int iconOffsetX = (int) ((btvLp.width - btv.getIconSize()) * iconScale) / 2;
373 
374             final int previewPosX =
375                     (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale);
376             final float paddingTop = btv.getPaddingTop() * iconScale;
377             final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY - paddingTop)
378                     / folderScale);
379 
380             final float xDistance = previewPosX - btvLp.x;
381             final float yDistance = previewPosY - btvLp.y;
382 
383             Animator translationX = getAnimator(btv, View.TRANSLATION_X, xDistance, 0f);
384             translationX.setInterpolator(previewItemInterpolator);
385             play(animatorSet, translationX);
386 
387             Animator translationY = getAnimator(btv, View.TRANSLATION_Y, yDistance, 0f);
388             translationY.setInterpolator(previewItemInterpolator);
389             play(animatorSet, translationY);
390 
391             Animator scaleAnimator = getAnimator(btv, SCALE_PROPERTY, initialScale, finalScale);
392             scaleAnimator.setInterpolator(previewItemInterpolator);
393             play(animatorSet, scaleAnimator);
394 
395             if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) {
396                 // These delays allows the preview items to move as part of the Folder's motion,
397                 // and its only necessary for large folders because of differing interpolators.
398                 int delay = mIsOpening ? mDelay : mDelay * 2;
399                 if (mIsOpening) {
400                     translationX.setStartDelay(delay);
401                     translationY.setStartDelay(delay);
402                     scaleAnimator.setStartDelay(delay);
403                 }
404                 translationX.setDuration(translationX.getDuration() - delay);
405                 translationY.setDuration(translationY.getDuration() - delay);
406                 scaleAnimator.setDuration(scaleAnimator.getDuration() - delay);
407             }
408 
409             animatorSet.addListener(new AnimatorListenerAdapter() {
410                 @Override
411                 public void onAnimationStart(Animator animation) {
412                     super.onAnimationStart(animation);
413                     // Necessary to initialize values here because of the start delay.
414                     if (mIsOpening) {
415                         btv.setTranslationX(xDistance);
416                         btv.setTranslationY(yDistance);
417                         btv.setScaleX(initialScale);
418                         btv.setScaleY(initialScale);
419                     }
420                 }
421 
422                 @Override
423                 public void onAnimationEnd(Animator animation) {
424                     super.onAnimationEnd(animation);
425                     btv.setTranslationX(0.0f);
426                     btv.setTranslationY(0.0f);
427                     btv.setScaleX(1f);
428                     btv.setScaleY(1f);
429                 }
430             });
431         }
432     }
433 
play(AnimatorSet as, Animator a)434     private void play(AnimatorSet as, Animator a) {
435         play(as, a, a.getStartDelay(), mDuration);
436     }
437 
play(AnimatorSet as, Animator a, long startDelay, int duration)438     private void play(AnimatorSet as, Animator a, long startDelay, int duration) {
439         a.setStartDelay(startDelay);
440         a.setDuration(duration);
441         as.play(a);
442     }
443 
isLargeFolder()444     private boolean isLargeFolder() {
445         return mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW;
446     }
447 
getPreviewItemInterpolator()448     private TimeInterpolator getPreviewItemInterpolator() {
449         if (isLargeFolder()) {
450             // With larger folders, we want the preview items to reach their final positions faster
451             // (when opening) and later (when closing) so that they appear aligned with the rest of
452             // the folder items when they are both visible.
453             return mIsOpening
454                     ? mLargeFolderPreviewItemOpenInterpolator
455                     : mLargeFolderPreviewItemCloseInterpolator;
456         }
457         return mFolderInterpolator;
458     }
459 
getAnimator(View view, Property property, float v1, float v2)460     private Animator getAnimator(View view, Property property, float v1, float v2) {
461         return mIsOpening
462                 ? ObjectAnimator.ofFloat(view, property, v1, v2)
463                 : ObjectAnimator.ofFloat(view, property, v2, v1);
464     }
465 
getAnimator(GradientDrawable drawable, String property, int v1, int v2)466     private ObjectAnimator getAnimator(GradientDrawable drawable, String property, int v1, int v2) {
467         return mIsOpening
468                 ? ObjectAnimator.ofArgb(drawable, property, v1, v2)
469                 : ObjectAnimator.ofArgb(drawable, property, v2, v1);
470     }
471 }
472