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