• 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 com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY;
20 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
21 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.AnimatorSet;
26 import android.animation.ObjectAnimator;
27 import android.animation.TimeInterpolator;
28 import android.content.Context;
29 import android.content.res.Resources;
30 import android.graphics.Color;
31 import android.graphics.Rect;
32 import android.graphics.drawable.GradientDrawable;
33 import android.support.v4.graphics.ColorUtils;
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.Launcher;
41 import com.android.launcher3.LauncherAnimUtils;
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.anim.RoundedRectRevealOutlineProvider;
47 import com.android.launcher3.dragndrop.DragLayer;
48 import com.android.launcher3.util.Themes;
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 Folder mFolder;
62     private FolderPagedView mContent;
63     private GradientDrawable mFolderBackground;
64 
65     private FolderIcon mFolderIcon;
66     private PreviewBackground mPreviewBackground;
67 
68     private Context mContext;
69     private Launcher mLauncher;
70 
71     private final boolean mIsOpening;
72 
73     private final int mDuration;
74     private final int mDelay;
75 
76     private final TimeInterpolator mFolderInterpolator;
77     private final TimeInterpolator mLargeFolderPreviewItemOpenInterpolator;
78     private final TimeInterpolator mLargeFolderPreviewItemCloseInterpolator;
79 
80     private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
81 
82 
FolderAnimationManager(Folder folder, boolean isOpening)83     public FolderAnimationManager(Folder folder, boolean isOpening) {
84         mFolder = folder;
85         mContent = folder.mContent;
86         mFolderBackground = (GradientDrawable) mFolder.getBackground();
87 
88         mFolderIcon = folder.mFolderIcon;
89         mPreviewBackground = mFolderIcon.mBackground;
90 
91         mContext = folder.getContext();
92         mLauncher = folder.mLauncher;
93 
94         mIsOpening = isOpening;
95 
96         Resources res = mContent.getResources();
97         mDuration = res.getInteger(R.integer.config_materialFolderExpandDuration);
98         mDelay = res.getInteger(R.integer.config_folderDelay);
99 
100         mFolderInterpolator = AnimationUtils.loadInterpolator(mContext,
101                 R.interpolator.folder_interpolator);
102         mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext,
103                 R.interpolator.large_folder_preview_item_open_interpolator);
104         mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext,
105                 R.interpolator.large_folder_preview_item_close_interpolator);
106     }
107 
108 
109     /**
110      * Prepares the Folder for animating between open / closed states.
111      */
getAnimator()112     public AnimatorSet getAnimator() {
113         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams();
114         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
115         final List<BubbleTextView> itemsInPreview = mFolderIcon.getPreviewItems();
116 
117         // Match position of the FolderIcon
118         final Rect folderIconPos = new Rect();
119         float scaleRelativeToDragLayer = mLauncher.getDragLayer()
120                 .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos);
121         int scaledRadius = mPreviewBackground.getScaledRadius();
122         float initialSize = (scaledRadius * 2) * scaleRelativeToDragLayer;
123 
124         // Match size/scale of icons in the preview
125         float previewScale = rule.scaleForItem(itemsInPreview.size());
126         float previewSize = rule.getIconSize() * previewScale;
127         float initialScale = previewSize / itemsInPreview.get(0).getIconSize()
128                 * scaleRelativeToDragLayer;
129         final float finalScale = 1f;
130         float scale = mIsOpening ? initialScale : finalScale;
131         mFolder.setScaleX(scale);
132         mFolder.setScaleY(scale);
133         mFolder.setPivotX(0);
134         mFolder.setPivotY(0);
135 
136         // We want to create a small X offset for the preview items, so that they follow their
137         // expected path to their final locations. ie. an icon should not move right, if it's final
138         // location is to its left. This value is arbitrarily defined.
139         int previewItemOffsetX = (int) (previewSize / 2);
140         if (Utilities.isRtl(mContext.getResources())) {
141             previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX);
142         }
143 
144         final int paddingOffsetX = (int) ((mFolder.getPaddingLeft() + mContent.getPaddingLeft())
145                 * initialScale);
146         final int paddingOffsetY = (int) ((mFolder.getPaddingTop() + mContent.getPaddingTop())
147                 * initialScale);
148 
149         int initialX = folderIconPos.left + mPreviewBackground.getOffsetX() - paddingOffsetX
150                 - previewItemOffsetX;
151         int initialY = folderIconPos.top + mPreviewBackground.getOffsetY() - paddingOffsetY;
152         final float xDistance = initialX - lp.x;
153         final float yDistance = initialY - lp.y;
154 
155         // Set up the Folder background.
156         final int finalColor = Themes.getAttrColor(mContext, android.R.attr.colorPrimary);
157         final int initialColor =
158                 ColorUtils.setAlphaComponent(finalColor, mPreviewBackground.getBackgroundAlpha());
159         mFolderBackground.setColor(mIsOpening ? initialColor : finalColor);
160 
161         // Set up the reveal animation that clips the Folder.
162         int totalOffsetX = paddingOffsetX + previewItemOffsetX;
163         Rect startRect = new Rect(
164                 Math.round(totalOffsetX / initialScale),
165                 Math.round(paddingOffsetY / initialScale),
166                 Math.round((totalOffsetX + initialSize) / initialScale),
167                 Math.round((paddingOffsetY + initialSize) / initialScale));
168         Rect endRect = new Rect(0, 0, lp.width, lp.height);
169         float initialRadius = initialSize / initialScale / 2f;
170         float finalRadius = Utilities.pxFromDp(2, mContext.getResources().getDisplayMetrics());
171 
172         // Create the animators.
173         AnimatorSet a = LauncherAnimUtils.createAnimatorSet();
174 
175         // Initialize the Folder items' text.
176         PropertyResetListener colorResetListener =
177                 new PropertyResetListener<>(TEXT_ALPHA_PROPERTY, 1f);
178         for (BubbleTextView icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) {
179             if (mIsOpening) {
180                 icon.setTextVisibility(false);
181             }
182             ObjectAnimator anim = icon.createTextAlphaAnimator(mIsOpening);
183             anim.addListener(colorResetListener);
184             play(a, anim);
185         }
186 
187         play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f));
188         play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f));
189         play(a, getAnimator(mFolder, SCALE_PROPERTY, initialScale, finalScale));
190         play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor));
191         play(a, mFolderIcon.mFolderName.createTextAlphaAnimator(!mIsOpening));
192         RoundedRectRevealOutlineProvider outlineProvider = new RoundedRectRevealOutlineProvider(
193                 initialRadius, finalRadius, startRect, endRect) {
194             @Override
195             public boolean shouldRemoveElevationDuringAnimation() {
196                 return true;
197             }
198         };
199         play(a, outlineProvider.createRevealAnimator(mFolder, !mIsOpening));
200 
201         // Animate the elevation midway so that the shadow is not noticeable in the background.
202         int midDuration = mDuration / 2;
203         Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0);
204         play(a, z, mIsOpening ? midDuration : 0, midDuration);
205 
206         a.addListener(new AnimatorListenerAdapter() {
207             @Override
208             public void onAnimationEnd(Animator animation) {
209                 super.onAnimationEnd(animation);
210                 mFolder.setTranslationX(0.0f);
211                 mFolder.setTranslationY(0.0f);
212                 mFolder.setTranslationZ(0.0f);
213                 mFolder.setScaleX(1f);
214                 mFolder.setScaleY(1f);
215             }
216         });
217 
218         // We set the interpolator on all current child animators here, because the preview item
219         // animators may use a different interpolator.
220         for (Animator animator : a.getChildAnimations()) {
221             animator.setInterpolator(mFolderInterpolator);
222         }
223 
224         int radiusDiff = scaledRadius - mPreviewBackground.getRadius();
225         addPreviewItemAnimators(a, initialScale / scaleRelativeToDragLayer,
226                 // Background can have a scaled radius in drag and drop mode, so we need to add the
227                 // difference to keep the preview items centered.
228                 previewItemOffsetX + radiusDiff, radiusDiff);
229         return a;
230     }
231 
232     /**
233      * Animate the items on the current page.
234      */
addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale, int previewItemOffsetX, int previewItemOffsetY)235     private void addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale,
236             int previewItemOffsetX, int previewItemOffsetY) {
237         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
238         boolean isOnFirstPage = mFolder.mContent.getCurrentPage() == 0;
239         final List<BubbleTextView> itemsInPreview = isOnFirstPage
240                 ? mFolderIcon.getPreviewItems()
241                 : mFolderIcon.getPreviewItemsOnPage(mFolder.mContent.getCurrentPage());
242         final int numItemsInPreview = itemsInPreview.size();
243         final int numItemsInFirstPagePreview = isOnFirstPage
244                 ? numItemsInPreview : MAX_NUM_ITEMS_IN_PREVIEW;
245 
246         TimeInterpolator previewItemInterpolator = getPreviewItemInterpolator();
247 
248         ShortcutAndWidgetContainer cwc = mContent.getPageAt(0).getShortcutsAndWidgets();
249         for (int i = 0; i < numItemsInPreview; ++i) {
250             final BubbleTextView btv = itemsInPreview.get(i);
251             CellLayout.LayoutParams btvLp = (CellLayout.LayoutParams) btv.getLayoutParams();
252 
253             // Calculate the final values in the LayoutParams.
254             btvLp.isLockedToGrid = true;
255             cwc.setupLp(btv);
256 
257             // Match scale of icons in the preview of the items on the first page.
258             float previewScale = rule.scaleForItem(numItemsInFirstPagePreview);
259             float previewSize = rule.getIconSize() * previewScale;
260             float iconScale = previewSize / itemsInPreview.get(i).getIconSize();
261 
262             final float initialScale = iconScale / folderScale;
263             final float finalScale = 1f;
264             float scale = mIsOpening ? initialScale : finalScale;
265             btv.setScaleX(scale);
266             btv.setScaleY(scale);
267 
268             // Match positions of the icons in the folder with their positions in the preview
269             rule.computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, mTmpParams);
270             // The PreviewLayoutRule assumes that the icon size takes up the entire width so we
271             // offset by the actual size.
272             int iconOffsetX = (int) ((btvLp.width - btv.getIconSize()) * iconScale) / 2;
273 
274             final int previewPosX =
275                     (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale);
276             final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY) / folderScale);
277 
278             final float xDistance = previewPosX - btvLp.x;
279             final float yDistance = previewPosY - btvLp.y;
280 
281             Animator translationX = getAnimator(btv, View.TRANSLATION_X, xDistance, 0f);
282             translationX.setInterpolator(previewItemInterpolator);
283             play(animatorSet, translationX);
284 
285             Animator translationY = getAnimator(btv, View.TRANSLATION_Y, yDistance, 0f);
286             translationY.setInterpolator(previewItemInterpolator);
287             play(animatorSet, translationY);
288 
289             Animator scaleAnimator = getAnimator(btv, SCALE_PROPERTY, initialScale, finalScale);
290             scaleAnimator.setInterpolator(previewItemInterpolator);
291             play(animatorSet, scaleAnimator);
292 
293             if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) {
294                 // These delays allows the preview items to move as part of the Folder's motion,
295                 // and its only necessary for large folders because of differing interpolators.
296                 int delay = mIsOpening ? mDelay : mDelay * 2;
297                 if (mIsOpening) {
298                     translationX.setStartDelay(delay);
299                     translationY.setStartDelay(delay);
300                     scaleAnimator.setStartDelay(delay);
301                 }
302                 translationX.setDuration(translationX.getDuration() - delay);
303                 translationY.setDuration(translationY.getDuration() - delay);
304                 scaleAnimator.setDuration(scaleAnimator.getDuration() - delay);
305             }
306 
307             animatorSet.addListener(new AnimatorListenerAdapter() {
308                 @Override
309                 public void onAnimationStart(Animator animation) {
310                     super.onAnimationStart(animation);
311                     // Necessary to initialize values here because of the start delay.
312                     if (mIsOpening) {
313                         btv.setTranslationX(xDistance);
314                         btv.setTranslationY(yDistance);
315                         btv.setScaleX(initialScale);
316                         btv.setScaleY(initialScale);
317                     }
318                 }
319 
320                 @Override
321                 public void onAnimationEnd(Animator animation) {
322                     super.onAnimationEnd(animation);
323                     btv.setTranslationX(0.0f);
324                     btv.setTranslationY(0.0f);
325                     btv.setScaleX(1f);
326                     btv.setScaleY(1f);
327                 }
328             });
329         }
330     }
331 
play(AnimatorSet as, Animator a)332     private void play(AnimatorSet as, Animator a) {
333         play(as, a, a.getStartDelay(), mDuration);
334     }
335 
play(AnimatorSet as, Animator a, long startDelay, int duration)336     private void play(AnimatorSet as, Animator a, long startDelay, int duration) {
337         a.setStartDelay(startDelay);
338         a.setDuration(duration);
339         as.play(a);
340     }
341 
getPreviewItemInterpolator()342     private TimeInterpolator getPreviewItemInterpolator() {
343         if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) {
344             // With larger folders, we want the preview items to reach their final positions faster
345             // (when opening) and later (when closing) so that they appear aligned with the rest of
346             // the folder items when they are both visible.
347             return mIsOpening
348                     ? mLargeFolderPreviewItemOpenInterpolator
349                     : mLargeFolderPreviewItemCloseInterpolator;
350         }
351         return mFolderInterpolator;
352     }
353 
getAnimator(View view, Property property, float v1, float v2)354     private Animator getAnimator(View view, Property property, float v1, float v2) {
355         return mIsOpening
356                 ? ObjectAnimator.ofFloat(view, property, v1, v2)
357                 : ObjectAnimator.ofFloat(view, property, v2, v1);
358     }
359 
getAnimator(GradientDrawable drawable, String property, int v1, int v2)360     private Animator getAnimator(GradientDrawable drawable, String property, int v1, int v2) {
361         return mIsOpening
362                 ? ObjectAnimator.ofArgb(drawable, property, v1, v2)
363                 : ObjectAnimator.ofArgb(drawable, property, v2, v1);
364     }
365 }
366