• 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.folder.ClippedFolderIconLayoutRule.ENTER_INDEX;
20 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX;
21 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
22 import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION;
23 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorListenerAdapter;
27 import android.animation.ObjectAnimator;
28 import android.animation.ValueAnimator;
29 import android.content.Context;
30 import android.graphics.Canvas;
31 import android.graphics.Path;
32 import android.graphics.PointF;
33 import android.graphics.Rect;
34 import android.graphics.drawable.Drawable;
35 import android.util.FloatProperty;
36 import android.view.View;
37 
38 import androidx.annotation.NonNull;
39 
40 import com.android.launcher3.BubbleTextView;
41 import com.android.launcher3.Utilities;
42 import com.android.launcher3.graphics.PreloadIconDrawable;
43 import com.android.launcher3.model.data.ItemInfoWithIcon;
44 import com.android.launcher3.model.data.WorkspaceItemInfo;
45 import com.android.launcher3.views.ActivityContext;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 import java.util.function.Predicate;
50 
51 /**
52  * Manages the drawing and animations of {@link PreviewItemDrawingParams} for a {@link FolderIcon}.
53  */
54 public class PreviewItemManager {
55 
56     private static final FloatProperty<PreviewItemManager> CURRENT_PAGE_ITEMS_TRANS_X =
57             new FloatProperty<PreviewItemManager>("currentPageItemsTransX") {
58                 @Override
59                 public void setValue(PreviewItemManager manager, float v) {
60                     manager.mCurrentPageItemsTransX = v;
61                     manager.onParamsChanged();
62                 }
63 
64                 @Override
65                 public Float get(PreviewItemManager manager) {
66                     return manager.mCurrentPageItemsTransX;
67                 }
68             };
69 
70     private final Context mContext;
71     private final FolderIcon mIcon;
72     private final int mIconSize;
73 
74     // These variables are all associated with the drawing of the preview; they are stored
75     // as member variables for shared usage and to avoid computation on each frame
76     private float mIntrinsicIconSize = -1;
77     private int mTotalWidth = -1;
78     private int mPrevTopPadding = -1;
79     private Drawable mReferenceDrawable = null;
80 
81     // These hold the first page preview items
82     private ArrayList<PreviewItemDrawingParams> mFirstPageParams = new ArrayList<>();
83     // These hold the current page preview items. It is empty if the current page is the first page.
84     private ArrayList<PreviewItemDrawingParams> mCurrentPageParams = new ArrayList<>();
85 
86     // We clip the preview items during the middle of the animation, so that it does not go outside
87     // of the visual shape. We stop clipping at this threshold, since the preview items ultimately
88     // do not get cropped in their resting state.
89     private final float mClipThreshold;
90     private float mCurrentPageItemsTransX = 0;
91     private boolean mShouldSlideInFirstPage;
92 
93     static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
94     private static final int FINAL_ITEM_ANIMATION_DURATION = 200;
95 
96     private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY = 100;
97     private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION = 300;
98     private static final int ITEM_SLIDE_IN_OUT_DISTANCE_PX = 200;
99 
PreviewItemManager(FolderIcon icon)100     public PreviewItemManager(FolderIcon icon) {
101         mContext = icon.getContext();
102         mIcon = icon;
103         mIconSize = ActivityContext.lookupContext(
104                 mContext).getDeviceProfile().folderChildIconSizePx;
105         mClipThreshold = Utilities.dpToPx(1f);
106     }
107 
108     /**
109      * @param reverse If true, animates the final item in the preview to be full size. If false,
110      *                animates the first item to its position in the preview.
111      */
createFirstItemAnimation(final boolean reverse, final Runnable onCompleteRunnable)112     public FolderPreviewItemAnim createFirstItemAnimation(final boolean reverse,
113             final Runnable onCompleteRunnable) {
114         return reverse
115                 ? new FolderPreviewItemAnim(this, mFirstPageParams.get(0), 0, 2, -1, -1,
116                         FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable)
117                 : new FolderPreviewItemAnim(this, mFirstPageParams.get(0), -1, -1, 0, 2,
118                         INITIAL_ITEM_ANIMATION_DURATION, onCompleteRunnable);
119     }
120 
prepareCreateAnimation(final View destView)121     Drawable prepareCreateAnimation(final View destView) {
122         Drawable animateDrawable = ((BubbleTextView) destView).getIcon();
123         computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
124                 destView.getMeasuredWidth());
125         mReferenceDrawable = animateDrawable;
126         return animateDrawable;
127     }
128 
recomputePreviewDrawingParams()129     public void recomputePreviewDrawingParams() {
130         if (mReferenceDrawable != null) {
131             computePreviewDrawingParams(mReferenceDrawable.getIntrinsicWidth(),
132                     mIcon.getMeasuredWidth());
133         }
134     }
135 
computePreviewDrawingParams(int drawableSize, int totalSize)136     private void computePreviewDrawingParams(int drawableSize, int totalSize) {
137         if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize ||
138                 mPrevTopPadding != mIcon.getPaddingTop()) {
139             mIntrinsicIconSize = drawableSize;
140             mTotalWidth = totalSize;
141             mPrevTopPadding = mIcon.getPaddingTop();
142 
143             mIcon.mBackground.setup(mIcon.getContext(), mIcon.mActivity, mIcon, mTotalWidth,
144                     mIcon.getPaddingTop());
145             mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize,
146                     Utilities.isRtl(mIcon.getResources()));
147 
148             updatePreviewItems(false);
149         }
150     }
151 
computePreviewItemDrawingParams(int index, int curNumItems, PreviewItemDrawingParams params)152     PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
153             PreviewItemDrawingParams params) {
154         // We use an index of -1 to represent an icon on the workspace for the destroy and
155         // create animations
156         if (index == -1) {
157             return getFinalIconParams(params);
158         }
159         return mIcon.mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params);
160     }
161 
getFinalIconParams(PreviewItemDrawingParams params)162     private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
163         float iconSize = mIcon.mActivity.getDeviceProfile().iconSizePx;
164 
165         final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
166         final float trans = (mIcon.mBackground.previewSize - iconSize) / 2;
167 
168         params.update(trans, trans, scale);
169         return params;
170     }
171 
drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params, PointF offset, boolean shouldClipPath, Path clipPath)172     public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params,
173             PointF offset, boolean shouldClipPath, Path clipPath) {
174         // The first item should be drawn last (ie. on top of later items)
175         for (int i = params.size() - 1; i >= 0; i--) {
176             PreviewItemDrawingParams p = params.get(i);
177             if (!p.hidden) {
178                 // Exiting param should always be clipped.
179                 boolean isExiting = p.index == EXIT_INDEX;
180                 drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath);
181             }
182         }
183     }
184 
185     /**
186      * Draws the preview items on {@param canvas}.
187      */
draw(Canvas canvas)188     public void draw(Canvas canvas) {
189         int saveCount = canvas.getSaveCount();
190         // The items are drawn in coordinates relative to the preview offset
191         PreviewBackground bg = mIcon.getFolderBackground();
192         Path clipPath = bg.getClipPath();
193         float firstPageItemsTransX = 0;
194         if (mShouldSlideInFirstPage) {
195             PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + mCurrentPageItemsTransX,
196                     bg.basePreviewOffsetY);
197             boolean shouldClip = mCurrentPageItemsTransX > mClipThreshold;
198             drawParams(canvas, mCurrentPageParams, firstPageOffset, shouldClip, clipPath);
199             firstPageItemsTransX = -ITEM_SLIDE_IN_OUT_DISTANCE_PX + mCurrentPageItemsTransX;
200         }
201 
202         PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + firstPageItemsTransX,
203                 bg.basePreviewOffsetY);
204         boolean shouldClipFirstPage = firstPageItemsTransX < -mClipThreshold;
205         drawParams(canvas, mFirstPageParams, firstPageOffset, shouldClipFirstPage, clipPath);
206         canvas.restoreToCount(saveCount);
207     }
208 
209     public void onParamsChanged() {
210         mIcon.invalidate();
211     }
212 
213     /**
214      * Draws each preview item.
215      *
216      * @param offset The offset needed to draw the preview items.
217      * @param shouldClipPath Iff true, clip path using {@param clipPath}.
218      * @param clipPath The clip path of the folder icon.
219      */
220     private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset,
221             boolean shouldClipPath, Path clipPath) {
222         canvas.save();
223         if (shouldClipPath) {
224             canvas.clipPath(clipPath);
225         }
226         canvas.translate(offset.x + params.transX, offset.y + params.transY);
227         canvas.scale(params.scale, params.scale);
228         Drawable d = params.drawable;
229 
230         if (d != null) {
231             Rect bounds = d.getBounds();
232             canvas.save();
233             canvas.translate(-bounds.left, -bounds.top);
234             canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height());
235             d.draw(canvas);
236             canvas.restore();
237         }
238         canvas.restore();
239     }
240 
241     public void hidePreviewItem(int index, boolean hidden) {
242         // If there are more params than visible in the preview, they are used for enter/exit
243         // animation purposes and they were added to the front of the list.
244         // To index the params properly, we need to skip these params.
245         index = index + Math.max(mFirstPageParams.size() - MAX_NUM_ITEMS_IN_PREVIEW, 0);
246 
247         PreviewItemDrawingParams params = index < mFirstPageParams.size() ?
248                 mFirstPageParams.get(index) : null;
249         if (params != null) {
250             params.hidden = hidden;
251         }
252     }
253 
254     void buildParamsForPage(int page, ArrayList<PreviewItemDrawingParams> params, boolean animate) {
255         List<WorkspaceItemInfo> items = mIcon.getPreviewItemsOnPage(page);
256         int prevNumItems = params.size();
257 
258         // We adjust the size of the list to match the number of items in the preview.
259         while (items.size() < params.size()) {
260             params.remove(params.size() - 1);
261         }
262         while (items.size() > params.size()) {
263             params.add(new PreviewItemDrawingParams(0, 0, 0));
264         }
265 
266         int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW;
267         for (int i = 0; i < params.size(); i++) {
268             PreviewItemDrawingParams p = params.get(i);
269             setDrawable(p, items.get(i));
270 
271             if (!animate) {
272                 if (p.anim != null) {
273                     p.anim.cancel();
274                 }
275                 computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p);
276                 if (mReferenceDrawable == null) {
277                     mReferenceDrawable = p.drawable;
278                 }
279             } else {
280                 FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, i, prevNumItems, i,
281                         numItemsInFirstPagePreview, DROP_IN_ANIMATION_DURATION, null);
282 
283                 if (p.anim != null) {
284                     if (p.anim.hasEqualFinalState(anim)) {
285                         // do nothing, let the current animation finish
286                         continue;
287                     }
288                     p.anim.cancel();
289                 }
290                 p.anim = anim;
291                 p.anim.start();
292             }
293         }
294     }
295 
296     void onFolderClose(int currentPage) {
297         // If we are not closing on the first page, we animate the current page preview items
298         // out, and animate the first page preview items in.
299         mShouldSlideInFirstPage = currentPage != 0;
300         if (mShouldSlideInFirstPage) {
301             mCurrentPageItemsTransX = 0;
302             buildParamsForPage(currentPage, mCurrentPageParams, false);
303             onParamsChanged();
304 
305             ValueAnimator slideAnimator = ObjectAnimator
306                     .ofFloat(this, CURRENT_PAGE_ITEMS_TRANS_X, 0, ITEM_SLIDE_IN_OUT_DISTANCE_PX);
307             slideAnimator.addListener(new AnimatorListenerAdapter() {
308                 @Override
309                 public void onAnimationEnd(Animator animation) {
310                     mCurrentPageParams.clear();
311                 }
312             });
313             slideAnimator.setStartDelay(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY);
314             slideAnimator.setDuration(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION);
315             slideAnimator.start();
316         }
317     }
318 
319     void updatePreviewItems(boolean animate) {
320         buildParamsForPage(0, mFirstPageParams, animate);
321     }
322 
323     void updatePreviewItems(Predicate<WorkspaceItemInfo> itemCheck) {
324         boolean modified = false;
325         for (PreviewItemDrawingParams param : mFirstPageParams) {
326             if (itemCheck.test(param.item)) {
327                 setDrawable(param, param.item);
328                 modified = true;
329             }
330         }
331         for (PreviewItemDrawingParams param : mCurrentPageParams) {
332             if (itemCheck.test(param.item)) {
333                 setDrawable(param, param.item);
334                 modified = true;
335             }
336         }
337         if (modified) {
338             mIcon.invalidate();
339         }
340     }
341 
342     boolean verifyDrawable(@NonNull Drawable who) {
343         for (int i = 0; i < mFirstPageParams.size(); i++) {
344             if (mFirstPageParams.get(i).drawable == who) {
345                 return true;
346             }
347         }
348         return false;
349     }
350 
351     float getIntrinsicIconSize() {
352         return mIntrinsicIconSize;
353     }
354 
355     /**
356      * Handles the case where items in the preview are either:
357      *  - Moving into the preview
358      *  - Moving into a new position
359      *  - Moving out of the preview
360      *
361      * @param oldItems The list of items in the old preview.
362      * @param newItems The list of items in the new preview.
363      * @param dropped The item that was dropped onto the FolderIcon.
364      */
365     public void onDrop(List<WorkspaceItemInfo> oldItems, List<WorkspaceItemInfo> newItems,
366             WorkspaceItemInfo dropped) {
367         int numItems = newItems.size();
368         final ArrayList<PreviewItemDrawingParams> params = mFirstPageParams;
369         buildParamsForPage(0, params, false);
370 
371         // New preview items for items that are moving in (except for the dropped item).
372         List<WorkspaceItemInfo> moveIn = new ArrayList<>();
373         for (WorkspaceItemInfo newItem : newItems) {
374             if (!oldItems.contains(newItem) && !newItem.equals(dropped)) {
375                 moveIn.add(newItem);
376             }
377         }
378         for (int i = 0; i < moveIn.size(); ++i) {
379             int prevIndex = newItems.indexOf(moveIn.get(i));
380             PreviewItemDrawingParams p = params.get(prevIndex);
381             computePreviewItemDrawingParams(prevIndex, numItems, p);
382             updateTransitionParam(p, moveIn.get(i), ENTER_INDEX, newItems.indexOf(moveIn.get(i)),
383                     numItems);
384         }
385 
386         // Items that are moving into new positions within the preview.
387         for (int newIndex = 0; newIndex < newItems.size(); ++newIndex) {
388             int oldIndex = oldItems.indexOf(newItems.get(newIndex));
389             if (oldIndex >= 0 && newIndex != oldIndex) {
390                 PreviewItemDrawingParams p = params.get(newIndex);
391                 updateTransitionParam(p, newItems.get(newIndex), oldIndex, newIndex, numItems);
392             }
393         }
394 
395         // Old preview items that need to be moved out.
396         List<WorkspaceItemInfo> moveOut = new ArrayList<>(oldItems);
397         moveOut.removeAll(newItems);
398         for (int i = 0; i < moveOut.size(); ++i) {
399             WorkspaceItemInfo item = moveOut.get(i);
400             int oldIndex = oldItems.indexOf(item);
401             PreviewItemDrawingParams p = computePreviewItemDrawingParams(oldIndex, numItems, null);
402             updateTransitionParam(p, item, oldIndex, EXIT_INDEX, numItems);
403             params.add(0, p); // We want these items first so that they are on drawn last.
404         }
405 
406         for (int i = 0; i < params.size(); ++i) {
407             if (params.get(i).anim != null) {
408                 params.get(i).anim.start();
409             }
410         }
411     }
412 
413     private void updateTransitionParam(final PreviewItemDrawingParams p, WorkspaceItemInfo item,
414             int prevIndex, int newIndex, int numItems) {
415         setDrawable(p, item);
416 
417         FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, prevIndex, numItems,
418                 newIndex, numItems, DROP_IN_ANIMATION_DURATION, null);
419         if (p.anim != null && !p.anim.hasEqualFinalState(anim)) {
420             p.anim.cancel();
421         }
422         p.anim = anim;
423     }
424 
425     private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
426         if (item.hasPromiseIconUi() || (item.runtimeStatusFlags
427                     & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
428             PreloadIconDrawable drawable = newPendingIcon(mContext, item);
429             drawable.setLevel(item.getProgressLevel());
430             p.drawable = drawable;
431         } else {
432             p.drawable = item.newIcon(mContext, true);
433         }
434         p.drawable.setBounds(0, 0, mIconSize, mIconSize);
435         p.item = item;
436 
437         // Set the callback to FolderIcon as it is responsible to drawing the icon. The
438         // callback will be released when the folder is opened.
439         p.drawable.setCallback(mIcon);
440     }
441 }
442