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
17package com.android.launcher3.folder;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.graphics.Canvas;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.support.annotation.NonNull;
26import android.view.View;
27import android.widget.TextView;
28
29import com.android.launcher3.BubbleTextView;
30import com.android.launcher3.ShortcutInfo;
31import com.android.launcher3.Utilities;
32import com.android.launcher3.config.FeatureFlags;
33
34import java.util.ArrayList;
35import java.util.List;
36
37import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION;
38
39/**
40 * Manages the drawing and animations of {@link PreviewItemDrawingParams} for a {@link FolderIcon}.
41 */
42public class PreviewItemManager {
43
44    private FolderIcon mIcon;
45
46    // These variables are all associated with the drawing of the preview; they are stored
47    // as member variables for shared usage and to avoid computation on each frame
48    private float mIntrinsicIconSize = -1;
49    private int mTotalWidth = -1;
50    private int mPrevTopPadding = -1;
51    private Drawable mReferenceDrawable = null;
52
53    // These hold the first page preview items
54    private ArrayList<PreviewItemDrawingParams> mFirstPageParams = new ArrayList<>();
55    // These hold the current page preview items. It is empty if the current page is the first page.
56    private ArrayList<PreviewItemDrawingParams> mCurrentPageParams = new ArrayList<>();
57
58    private float mCurrentPageItemsTransX = 0;
59    private boolean mShouldSlideInFirstPage;
60
61    static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
62    private static final int FINAL_ITEM_ANIMATION_DURATION = 200;
63
64    private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY = 100;
65    private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION = 300;
66    private static final int ITEM_SLIDE_IN_OUT_DISTANCE_PX = 200;
67
68    public PreviewItemManager(FolderIcon icon) {
69        mIcon = icon;
70    }
71
72    /**
73     * @param reverse If true, animates the final item in the preview to be full size. If false,
74     *                animates the first item to its position in the preview.
75     */
76    public FolderPreviewItemAnim createFirstItemAnimation(final boolean reverse,
77            final Runnable onCompleteRunnable) {
78        return reverse
79                ? new FolderPreviewItemAnim(this, mFirstPageParams.get(0), 0, 2, -1, -1,
80                        FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable)
81                : new FolderPreviewItemAnim(this, mFirstPageParams.get(0), -1, -1, 0, 2,
82                        INITIAL_ITEM_ANIMATION_DURATION, onCompleteRunnable);
83    }
84
85    Drawable prepareCreateAnimation(final View destView) {
86        Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1];
87        computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
88                destView.getMeasuredWidth());
89        mReferenceDrawable = animateDrawable;
90        return animateDrawable;
91    }
92
93    public void recomputePreviewDrawingParams() {
94        if (mReferenceDrawable != null) {
95            computePreviewDrawingParams(mReferenceDrawable.getIntrinsicWidth(),
96                    mIcon.getMeasuredWidth());
97        }
98    }
99
100    private void computePreviewDrawingParams(int drawableSize, int totalSize) {
101        if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize ||
102                mPrevTopPadding != mIcon.getPaddingTop()) {
103            mIntrinsicIconSize = drawableSize;
104            mTotalWidth = totalSize;
105            mPrevTopPadding = mIcon.getPaddingTop();
106
107            mIcon.mBackground.setup(mIcon.mLauncher, mIcon, mTotalWidth, mIcon.getPaddingTop());
108            mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize,
109                    Utilities.isRtl(mIcon.getResources()));
110
111            updateItemDrawingParams(false);
112        }
113    }
114
115    PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
116            PreviewItemDrawingParams params) {
117        // We use an index of -1 to represent an icon on the workspace for the destroy and
118        // create animations
119        if (index == -1) {
120            return getFinalIconParams(params);
121        }
122        return mIcon.mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params);
123    }
124
125    private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
126        float iconSize = mIcon.mLauncher.getDeviceProfile().iconSizePx;
127
128        final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
129        final float trans = (mIcon.mBackground.previewSize - iconSize) / 2;
130
131        params.update(trans, trans, scale);
132        return params;
133    }
134
135    public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params,
136            float transX) {
137        canvas.translate(transX, 0);
138        // The first item should be drawn last (ie. on top of later items)
139        for (int i = params.size() - 1; i >= 0; i--) {
140            PreviewItemDrawingParams p = params.get(i);
141            if (!p.hidden) {
142                drawPreviewItem(canvas, p);
143            }
144        }
145        canvas.translate(-transX, 0);
146    }
147
148    public void draw(Canvas canvas) {
149        // The items are drawn in coordinates relative to the preview offset
150        PreviewBackground bg = mIcon.getFolderBackground();
151        canvas.translate(bg.basePreviewOffsetX, bg.basePreviewOffsetY);
152
153        float firstPageItemsTransX = 0;
154        if (mShouldSlideInFirstPage) {
155            drawParams(canvas, mCurrentPageParams, mCurrentPageItemsTransX);
156
157            firstPageItemsTransX = -ITEM_SLIDE_IN_OUT_DISTANCE_PX + mCurrentPageItemsTransX;
158        }
159
160        drawParams(canvas, mFirstPageParams, firstPageItemsTransX);
161        canvas.translate(-bg.basePreviewOffsetX, -bg.basePreviewOffsetY);
162    }
163
164    public void onParamsChanged() {
165        mIcon.invalidate();
166    }
167
168    private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) {
169        canvas.save(Canvas.MATRIX_SAVE_FLAG);
170        canvas.translate(params.transX, params.transY);
171        canvas.scale(params.scale, params.scale);
172        Drawable d = params.drawable;
173
174        if (d != null) {
175            Rect bounds = d.getBounds();
176            canvas.save();
177            canvas.translate(-bounds.left, -bounds.top);
178            canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height());
179            d.draw(canvas);
180            canvas.restore();
181        }
182        canvas.restore();
183    }
184
185    public void hidePreviewItem(int index, boolean hidden) {
186        PreviewItemDrawingParams params = index < mFirstPageParams.size() ?
187                mFirstPageParams.get(index) : null;
188        if (params != null) {
189            params.hidden = hidden;
190        }
191    }
192
193    void buildParamsForPage(int page, ArrayList<PreviewItemDrawingParams> params, boolean animate) {
194        List<BubbleTextView> items = mIcon.getPreviewItemsOnPage(page);
195        int prevNumItems = params.size();
196
197        // We adjust the size of the list to match the number of items in the preview.
198        while (items.size() < params.size()) {
199            params.remove(params.size() - 1);
200        }
201        while (items.size() > params.size()) {
202            params.add(new PreviewItemDrawingParams(0, 0, 0, 0));
203        }
204
205        int numItemsInFirstPagePreview = page == 0 ? items.size() : FolderIcon.NUM_ITEMS_IN_PREVIEW;
206        for (int i = 0; i < params.size(); i++) {
207            PreviewItemDrawingParams p = params.get(i);
208            p.drawable = items.get(i).getCompoundDrawables()[1];
209
210            if (p.drawable != null && !mIcon.mFolder.isOpen()) {
211                // Set the callback to FolderIcon as it is responsible to drawing the icon. The
212                // callback will be released when the folder is opened.
213                p.drawable.setCallback(mIcon);
214            }
215
216            if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) {
217                computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p);
218                if (mReferenceDrawable == null) {
219                    mReferenceDrawable = p.drawable;
220                }
221            } else {
222                FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, i, prevNumItems, i,
223                        numItemsInFirstPagePreview, DROP_IN_ANIMATION_DURATION, null);
224
225                if (p.anim != null) {
226                    if (p.anim.hasEqualFinalState(anim)) {
227                        // do nothing, let the current animation finish
228                        continue;
229                    }
230                    p.anim.cancel();
231                }
232                p.anim = anim;
233                p.anim.start();
234            }
235        }
236    }
237
238    void onFolderClose(int currentPage) {
239        // If we are not closing on the first page, we animate the current page preview items
240        // out, and animate the first page preview items in.
241        mShouldSlideInFirstPage = currentPage != 0;
242        if (mShouldSlideInFirstPage) {
243            mCurrentPageItemsTransX = 0;
244            buildParamsForPage(currentPage, mCurrentPageParams, false);
245            onParamsChanged();
246
247            ValueAnimator slideAnimator = ValueAnimator.ofFloat(0, ITEM_SLIDE_IN_OUT_DISTANCE_PX);
248            slideAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
249                @Override
250                public void onAnimationUpdate(ValueAnimator valueAnimator) {
251                    mCurrentPageItemsTransX = (float) valueAnimator.getAnimatedValue();
252                    onParamsChanged();
253                }
254            });
255            slideAnimator.addListener(new AnimatorListenerAdapter() {
256                @Override
257                public void onAnimationEnd(Animator animation) {
258                    mCurrentPageParams.clear();
259                }
260            });
261            slideAnimator.setStartDelay(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY);
262            slideAnimator.setDuration(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION);
263            slideAnimator.start();
264        }
265    }
266
267    void updateItemDrawingParams(boolean animate) {
268        buildParamsForPage(0, mFirstPageParams, animate);
269    }
270
271    boolean verifyDrawable(@NonNull Drawable who) {
272        for (int i = 0; i < mFirstPageParams.size(); i++) {
273            if (mFirstPageParams.get(i).drawable == who) {
274                return true;
275            }
276        }
277        return false;
278    }
279
280    float getIntrinsicIconSize() {
281        return mIntrinsicIconSize;
282    }
283
284    /**
285     * Handles the case where items in the preview are either:
286     *  - Moving into the preview
287     *  - Moving into a new position
288     *  - Moving out of the preview
289     *
290     * @param oldParams The list of items in the old preview.
291     * @param newParams The list of items in the new preview.
292     * @param dropped The item that was dropped onto the FolderIcon.
293     */
294    public void onDrop(List<BubbleTextView> oldParams, List<BubbleTextView> newParams,
295            ShortcutInfo dropped) {
296        int numItems = newParams.size();
297        final ArrayList<PreviewItemDrawingParams> params = mFirstPageParams;
298        buildParamsForPage(0, params, false);
299
300        // New preview items for items that are moving in (except for the dropped item).
301        List<BubbleTextView> moveIn = new ArrayList<>();
302        for (BubbleTextView btv : newParams) {
303            if (!oldParams.contains(btv) && !btv.getTag().equals(dropped)) {
304                moveIn.add(btv);
305            }
306        }
307        for (int i = 0; i < moveIn.size(); ++i) {
308            int prevIndex = newParams.indexOf(moveIn.get(i));
309            PreviewItemDrawingParams p = params.get(prevIndex);
310            computePreviewItemDrawingParams(prevIndex, numItems, p);
311            updateTransitionParam(p, moveIn.get(i), mIcon.mPreviewLayoutRule.getEnterIndex(),
312                    newParams.indexOf(moveIn.get(i)));
313        }
314
315        // Items that are moving into new positions within the preview.
316        for (int newIndex = 0; newIndex < newParams.size(); ++newIndex) {
317            int oldIndex = oldParams.indexOf(newParams.get(newIndex));
318            if (oldIndex >= 0 && newIndex != oldIndex) {
319                PreviewItemDrawingParams p = params.get(newIndex);
320                updateTransitionParam(p, newParams.get(newIndex), oldIndex, newIndex);
321            }
322        }
323
324        // Old preview items that need to be moved out.
325        List<BubbleTextView> moveOut = new ArrayList<>(oldParams);
326        moveOut.removeAll(newParams);
327        for (int i = 0; i < moveOut.size(); ++i) {
328            BubbleTextView item = moveOut.get(i);
329            int oldIndex = oldParams.indexOf(item);
330            PreviewItemDrawingParams p = computePreviewItemDrawingParams(oldIndex, numItems, null);
331            updateTransitionParam(p, item, oldIndex, mIcon.mPreviewLayoutRule.getExitIndex());
332            params.add(0, p); // We want these items first so that they are on drawn last.
333        }
334
335        for (int i = 0; i < params.size(); ++i) {
336            if (params.get(i).anim != null) {
337                params.get(i).anim.start();
338            }
339        }
340    }
341
342    private void updateTransitionParam(final PreviewItemDrawingParams p, BubbleTextView btv,
343            int prevIndex, int newIndex) {
344        p.drawable = btv.getCompoundDrawables()[1];
345        if (!mIcon.mFolder.isOpen()) {
346            // Set the callback to FolderIcon as it is responsible to drawing the icon. The
347            // callback will be released when the folder is opened.
348            p.drawable.setCallback(mIcon);
349        }
350
351        FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, prevIndex,
352                FolderIcon.NUM_ITEMS_IN_PREVIEW, newIndex, FolderIcon.NUM_ITEMS_IN_PREVIEW,
353                DROP_IN_ANIMATION_DURATION, null);
354        if (p.anim != null && !p.anim.hasEqualFinalState(anim)) {
355            p.anim.cancel();
356        }
357        p.anim = anim;
358    }
359}
360