1/*
2 * Copyright (C) 2008 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.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.content.Context;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Matrix;
28import android.graphics.Paint;
29import android.graphics.Path;
30import android.graphics.Point;
31import android.graphics.PorterDuff;
32import android.graphics.PorterDuffXfermode;
33import android.graphics.RadialGradient;
34import android.graphics.Rect;
35import android.graphics.Region;
36import android.graphics.Shader;
37import android.graphics.drawable.Drawable;
38import android.os.Parcelable;
39import android.util.AttributeSet;
40import android.util.DisplayMetrics;
41import android.util.Property;
42import android.view.LayoutInflater;
43import android.view.MotionEvent;
44import android.view.View;
45import android.view.ViewConfiguration;
46import android.view.ViewGroup;
47import android.view.animation.AccelerateInterpolator;
48import android.view.animation.DecelerateInterpolator;
49import android.widget.FrameLayout;
50import android.widget.TextView;
51
52import com.android.launcher3.Alarm;
53import com.android.launcher3.AppInfo;
54import com.android.launcher3.BubbleTextView;
55import com.android.launcher3.CellLayout;
56import com.android.launcher3.CheckLongPressHelper;
57import com.android.launcher3.DeviceProfile;
58import com.android.launcher3.DropTarget.DragObject;
59import com.android.launcher3.FastBitmapDrawable;
60import com.android.launcher3.FolderInfo;
61import com.android.launcher3.FolderInfo.FolderListener;
62import com.android.launcher3.ItemInfo;
63import com.android.launcher3.Launcher;
64import com.android.launcher3.LauncherAnimUtils;
65import com.android.launcher3.LauncherSettings;
66import com.android.launcher3.OnAlarmListener;
67import com.android.launcher3.R;
68import com.android.launcher3.ShortcutInfo;
69import com.android.launcher3.SimpleOnStylusPressListener;
70import com.android.launcher3.StylusEventHelper;
71import com.android.launcher3.Utilities;
72import com.android.launcher3.Workspace;
73import com.android.launcher3.badge.BadgeRenderer;
74import com.android.launcher3.badge.FolderBadgeInfo;
75import com.android.launcher3.config.FeatureFlags;
76import com.android.launcher3.dragndrop.DragLayer;
77import com.android.launcher3.dragndrop.DragView;
78import com.android.launcher3.graphics.IconPalette;
79import com.android.launcher3.util.Thunk;
80
81import java.util.ArrayList;
82import java.util.List;
83
84/**
85 * An icon that can appear on in the workspace representing an {@link Folder}.
86 */
87public class FolderIcon extends FrameLayout implements FolderListener {
88    @Thunk Launcher mLauncher;
89    @Thunk Folder mFolder;
90    private FolderInfo mInfo;
91    @Thunk static boolean sStaticValuesDirty = true;
92
93    public static final int NUM_ITEMS_IN_PREVIEW = FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON ?
94            StackFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW :
95            ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
96
97    private CheckLongPressHelper mLongPressHelper;
98    private StylusEventHelper mStylusEventHelper;
99
100    // The number of icons to display in the
101    private static final int CONSUMPTION_ANIMATION_DURATION = 100;
102    private static final int DROP_IN_ANIMATION_DURATION = 400;
103    private static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
104    private static final int FINAL_ITEM_ANIMATION_DURATION = 200;
105
106    // Flag whether the folder should open itself when an item is dragged over is enabled.
107    public static final boolean SPRING_LOADING_ENABLED = true;
108
109    // Delay when drag enters until the folder opens, in miliseconds.
110    private static final int ON_OPEN_DELAY = 800;
111
112    @Thunk BubbleTextView mFolderName;
113
114    // These variables are all associated with the drawing of the preview; they are stored
115    // as member variables for shared usage and to avoid computation on each frame
116    private int mIntrinsicIconSize = -1;
117    private int mTotalWidth = -1;
118    private int mPrevTopPadding = -1;
119
120    PreviewBackground mBackground = new PreviewBackground();
121
122    private PreviewLayoutRule mPreviewLayoutRule;
123
124    boolean mAnimating = false;
125    private Rect mTempBounds = new Rect();
126
127    private float mSlop;
128
129    private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0);
130    private ArrayList<PreviewItemDrawingParams> mDrawingParams = new ArrayList<PreviewItemDrawingParams>();
131    private Drawable mReferenceDrawable = null;
132
133    private Alarm mOpenAlarm = new Alarm();
134
135    private FolderBadgeInfo mBadgeInfo = new FolderBadgeInfo();
136    private BadgeRenderer mBadgeRenderer;
137    private float mBadgeScale;
138    private Point mTempSpaceForBadgeOffset = new Point();
139
140    private static final Property<FolderIcon, Float> BADGE_SCALE_PROPERTY
141            = new Property<FolderIcon, Float>(Float.TYPE, "badgeScale") {
142        @Override
143        public Float get(FolderIcon folderIcon) {
144            return folderIcon.mBadgeScale;
145        }
146
147        @Override
148        public void set(FolderIcon folderIcon, Float value) {
149            folderIcon.mBadgeScale = value;
150            folderIcon.invalidate();
151        }
152    };
153
154    public FolderIcon(Context context, AttributeSet attrs) {
155        super(context, attrs);
156        init();
157    }
158
159    public FolderIcon(Context context) {
160        super(context);
161        init();
162    }
163
164    private void init() {
165        mLongPressHelper = new CheckLongPressHelper(this);
166        mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
167        mPreviewLayoutRule = FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON ?
168                new StackFolderIconLayoutRule() :
169                new ClippedFolderIconLayoutRule();
170        mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
171    }
172
173    public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
174            FolderInfo folderInfo) {
175        @SuppressWarnings("all") // suppress dead code warning
176        final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
177        if (error) {
178            throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " +
179                    "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " +
180                    "is dependent on this");
181        }
182
183        DeviceProfile grid = launcher.getDeviceProfile();
184        FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false);
185
186        icon.setClipToPadding(false);
187        icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name);
188        icon.mFolderName.setText(folderInfo.title);
189        icon.mFolderName.setCompoundDrawablePadding(0);
190        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
191        lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
192
193        icon.setTag(folderInfo);
194        icon.setOnClickListener(launcher);
195        icon.mInfo = folderInfo;
196        icon.mLauncher = launcher;
197        icon.mBadgeRenderer = launcher.getDeviceProfile().mBadgeRenderer;
198        icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
199        Folder folder = Folder.fromXml(launcher);
200        folder.setDragController(launcher.getDragController());
201        folder.setFolderIcon(icon);
202        folder.bind(folderInfo);
203        icon.setFolder(folder);
204        icon.setAccessibilityDelegate(launcher.getAccessibilityDelegate());
205
206        folderInfo.addListener(icon);
207
208        icon.setOnFocusChangeListener(launcher.mFocusHandler);
209        return icon;
210    }
211
212    @Override
213    protected Parcelable onSaveInstanceState() {
214        sStaticValuesDirty = true;
215        return super.onSaveInstanceState();
216    }
217
218    public Folder getFolder() {
219        return mFolder;
220    }
221
222    private void setFolder(Folder folder) {
223        mFolder = folder;
224        updateItemDrawingParams(false);
225    }
226
227    private boolean willAcceptItem(ItemInfo item) {
228        final int itemType = item.itemType;
229        return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
230                itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
231                itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) &&
232                !mFolder.isFull() && item != mInfo && !mFolder.isOpen());
233    }
234
235    public boolean acceptDrop(ItemInfo dragInfo) {
236        final ItemInfo item = dragInfo;
237        return !mFolder.isDestroyed() && willAcceptItem(item);
238    }
239
240    public void addItem(ShortcutInfo item) {
241        mInfo.add(item, true);
242    }
243
244    public void onDragEnter(ItemInfo dragInfo) {
245        if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return;
246        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
247        CellLayout cl = (CellLayout) getParent().getParent();
248
249        mBackground.animateToAccept(cl, lp.cellX, lp.cellY);
250        mOpenAlarm.setOnAlarmListener(mOnOpenListener);
251        if (SPRING_LOADING_ENABLED &&
252                ((dragInfo instanceof AppInfo) || (dragInfo instanceof ShortcutInfo))) {
253            // TODO: we currently don't support spring-loading for PendingAddShortcutInfos even
254            // though widget-style shortcuts can be added to folders. The issue is that we need
255            // to deal with configuration activities which are currently handled in
256            // Workspace#onDropExternal.
257            mOpenAlarm.setAlarm(ON_OPEN_DELAY);
258        }
259    }
260
261    OnAlarmListener mOnOpenListener = new OnAlarmListener() {
262        public void onAlarm(Alarm alarm) {
263            mFolder.beginExternalDrag();
264            mFolder.animateOpen();
265        }
266    };
267
268    public Drawable prepareCreate(final View destView) {
269        Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1];
270        computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
271                destView.getMeasuredWidth());
272        return animateDrawable;
273    }
274
275    public void performCreateAnimation(final ShortcutInfo destInfo, final View destView,
276            final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect,
277            float scaleRelativeToDragLayer, Runnable postAnimationRunnable) {
278
279        // These correspond two the drawable and view that the icon was dropped _onto_
280        Drawable animateDrawable = prepareCreate(destView);
281
282        mReferenceDrawable = animateDrawable;
283
284        addItem(destInfo);
285        // This will animate the first item from it's position as an icon into its
286        // position as the first item in the preview
287        animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION, false, null);
288
289        // This will animate the dragView (srcView) into the new folder
290        onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable);
291    }
292
293    public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) {
294        Drawable animateDrawable = ((TextView) finalView).getCompoundDrawables()[1];
295        computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
296                finalView.getMeasuredWidth());
297
298        // This will animate the first item from it's position as an icon into its
299        // position as the first item in the preview
300        animateFirstItem(animateDrawable, FINAL_ITEM_ANIMATION_DURATION, true,
301                onCompleteRunnable);
302    }
303
304    public void onDragExit() {
305        mBackground.animateToRest();
306        mOpenAlarm.cancelAlarm();
307    }
308
309    private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect,
310            float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable) {
311        item.cellX = -1;
312        item.cellY = -1;
313
314        // Typically, the animateView corresponds to the DragView; however, if this is being done
315        // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
316        // will not have a view to animate
317        if (animateView != null) {
318            DragLayer dragLayer = mLauncher.getDragLayer();
319            Rect from = new Rect();
320            dragLayer.getViewRectRelativeToSelf(animateView, from);
321            Rect to = finalRect;
322            if (to == null) {
323                to = new Rect();
324                Workspace workspace = mLauncher.getWorkspace();
325                // Set cellLayout and this to it's final state to compute final animation locations
326                workspace.setFinalTransitionTransform((CellLayout) getParent().getParent());
327                float scaleX = getScaleX();
328                float scaleY = getScaleY();
329                setScaleX(1.0f);
330                setScaleY(1.0f);
331                scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to);
332                // Finished computing final animation locations, restore current state
333                setScaleX(scaleX);
334                setScaleY(scaleY);
335                workspace.resetTransitionTransform((CellLayout) getParent().getParent());
336            }
337
338            int[] center = new int[2];
339            float scale = getLocalCenterForIndex(index, index + 1, center);
340            center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]);
341            center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]);
342
343            to.offset(center[0] - animateView.getMeasuredWidth() / 2,
344                      center[1] - animateView.getMeasuredHeight() / 2);
345
346            float finalAlpha = index < mPreviewLayoutRule.maxNumItems() ? 0.5f : 0f;
347
348            float finalScale = scale * scaleRelativeToDragLayer;
349            dragLayer.animateView(animateView, from, to, finalAlpha,
350                    1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
351                    new DecelerateInterpolator(2), new AccelerateInterpolator(2),
352                    postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null);
353            addItem(item);
354            mFolder.hideItem(item);
355
356            final PreviewItemDrawingParams params = index < mDrawingParams.size() ?
357                    mDrawingParams.get(index) : null;
358            if (params != null) params.hidden = true;
359            postDelayed(new Runnable() {
360                public void run() {
361                    if (params != null) params.hidden = false;
362                    mFolder.showItem(item);
363                    invalidate();
364                }
365            }, DROP_IN_ANIMATION_DURATION);
366        } else {
367            addItem(item);
368        }
369    }
370
371    public void onDrop(DragObject d) {
372        ShortcutInfo item;
373        if (d.dragInfo instanceof AppInfo) {
374            // Came from all apps -- make a copy
375            item = ((AppInfo) d.dragInfo).makeShortcut();
376        } else {
377            item = (ShortcutInfo) d.dragInfo;
378        }
379        mFolder.notifyDrop();
380        onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable);
381    }
382
383    private void computePreviewDrawingParams(int drawableSize, int totalSize) {
384        if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize ||
385                mPrevTopPadding != getPaddingTop()) {
386            DeviceProfile grid = mLauncher.getDeviceProfile();
387
388            mIntrinsicIconSize = drawableSize;
389            mTotalWidth = totalSize;
390            mPrevTopPadding = getPaddingTop();
391
392            mBackground.setup(getResources().getDisplayMetrics(), grid, this, mTotalWidth,
393                    getPaddingTop());
394            mPreviewLayoutRule.init(mBackground.previewSize, mIntrinsicIconSize,
395                    Utilities.isRtl(getResources()));
396
397            updateItemDrawingParams(false);
398        }
399    }
400
401    private void computePreviewDrawingParams(Drawable d) {
402        computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth());
403    }
404
405    public void setBadgeInfo(FolderBadgeInfo badgeInfo) {
406        updateBadgeScale(mBadgeInfo.hasBadge(), badgeInfo.hasBadge());
407        mBadgeInfo = badgeInfo;
408    }
409
410    /**
411     * Sets mBadgeScale to 1 or 0, animating if wasBadged or isBadged is false
412     * (the badge is being added or removed).
413     */
414    private void updateBadgeScale(boolean wasBadged, boolean isBadged) {
415        float newBadgeScale = isBadged ? 1f : 0f;
416        // Animate when a badge is first added or when it is removed.
417        if ((wasBadged ^ isBadged) && isShown()) {
418            ObjectAnimator.ofFloat(this, BADGE_SCALE_PROPERTY, newBadgeScale).start();
419        } else {
420            mBadgeScale = newBadgeScale;
421            invalidate();
422        }
423    }
424
425    static class PreviewItemDrawingParams {
426        PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
427            this.transX = transX;
428            this.transY = transY;
429            this.scale = scale;
430            this.overlayAlpha = overlayAlpha;
431        }
432
433        public void update(float transX, float transY, float scale) {
434            // We ensure the update will not interfere with an animation on the layout params
435            // If the final values differ, we cancel the animation.
436            if (anim != null) {
437                if (anim.finalTransX == transX || anim.finalTransY == transY
438                        || anim.finalScale == scale) {
439                    return;
440                }
441                anim.cancel();
442            }
443
444            this.transX = transX;
445            this.transY = transY;
446            this.scale = scale;
447        }
448
449        float transX;
450        float transY;
451        float scale;
452        public float overlayAlpha;
453        boolean hidden;
454        FolderPreviewItemAnim anim;
455        Drawable drawable;
456    }
457
458    private float getLocalCenterForIndex(int index, int curNumItems, int[] center) {
459        mTmpParams = computePreviewItemDrawingParams(
460                Math.min(mPreviewLayoutRule.maxNumItems(), index), curNumItems, mTmpParams);
461
462        mTmpParams.transX += mBackground.basePreviewOffsetX;
463        mTmpParams.transY += mBackground.basePreviewOffsetY;
464        float offsetX = mTmpParams.transX + (mTmpParams.scale * mIntrinsicIconSize) / 2;
465        float offsetY = mTmpParams.transY + (mTmpParams.scale * mIntrinsicIconSize) / 2;
466
467        center[0] = (int) Math.round(offsetX);
468        center[1] = (int) Math.round(offsetY);
469        return mTmpParams.scale;
470    }
471
472    private PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
473            PreviewItemDrawingParams params) {
474        // We use an index of -1 to represent an icon on the workspace for the destroy and
475        // create animations
476        if (index == -1) {
477            return getFinalIconParams(params);
478        }
479        return mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params);
480    }
481
482    private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
483        float iconSize = mLauncher.getDeviceProfile().iconSizePx;
484
485        final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
486        final float trans = (mBackground.previewSize - iconSize) / 2;
487
488        params.update(trans, trans, scale);
489        return params;
490    }
491
492    private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) {
493        canvas.save(Canvas.MATRIX_SAVE_FLAG);
494        canvas.translate(params.transX, params.transY);
495        canvas.scale(params.scale, params.scale);
496        Drawable d = params.drawable;
497
498        if (d != null) {
499            mTempBounds.set(d.getBounds());
500            d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
501            if (d instanceof FastBitmapDrawable) {
502                FastBitmapDrawable fd = (FastBitmapDrawable) d;
503                fd.drawWithBrightness(canvas, params.overlayAlpha);
504            } else {
505                d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255),
506                        PorterDuff.Mode.SRC_ATOP);
507                d.draw(canvas);
508                d.clearColorFilter();
509            }
510            d.setBounds(mTempBounds);
511        }
512        canvas.restore();
513    }
514
515    /**
516     * This object represents a FolderIcon preview background. It stores drawing / measurement
517     * information, handles drawing, and animation (accept state <--> rest state).
518     */
519    public static class PreviewBackground {
520
521        private final PorterDuffXfermode mClipPorterDuffXfermode
522                = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
523        // Create a RadialGradient such that it draws a black circle and then extends with
524        // transparent. To achieve this, we keep the gradient to black for the range [0, 1) and
525        // just at the edge quickly change it to transparent.
526        private final RadialGradient mClipShader = new RadialGradient(0, 0, 1,
527                new int[] {Color.BLACK, Color.BLACK, Color.TRANSPARENT },
528                new float[] {0, 0.999f, 1},
529                Shader.TileMode.CLAMP);
530
531        private final PorterDuffXfermode mShadowPorterDuffXfermode
532                = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
533        private RadialGradient mShadowShader = null;
534
535        private final Matrix mShaderMatrix = new Matrix();
536        private final Path mPath = new Path();
537
538        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
539
540        private float mScale = 1f;
541        private float mColorMultiplier = 1f;
542        private float mStrokeWidth;
543        private View mInvalidateDelegate;
544
545        public int previewSize;
546        private int basePreviewOffsetX;
547        private int basePreviewOffsetY;
548
549        private CellLayout mDrawingDelegate;
550        public int delegateCellX;
551        public int delegateCellY;
552
553        // When the PreviewBackground is drawn under an icon (for creating a folder) the border
554        // should not occlude the icon
555        public boolean isClipping = true;
556
557        // Drawing / animation configurations
558        private static final float ACCEPT_SCALE_FACTOR = 1.25f;
559        private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
560
561        // Expressed on a scale from 0 to 255.
562        private static final int BG_OPACITY = 160;
563        private static final int MAX_BG_OPACITY = 225;
564        private static final int BG_INTENSITY = 245;
565        private static final int SHADOW_OPACITY = 40;
566
567        ValueAnimator mScaleAnimator;
568
569        public void setup(DisplayMetrics dm, DeviceProfile grid, View invalidateDelegate,
570                   int availableSpace, int topPadding) {
571            mInvalidateDelegate = invalidateDelegate;
572
573            final int previewSize = grid.folderIconSizePx;
574            final int previewPadding = grid.folderIconPreviewPadding;
575
576            this.previewSize = (previewSize - 2 * previewPadding);
577
578            basePreviewOffsetX = (availableSpace - this.previewSize) / 2;
579            basePreviewOffsetY = previewPadding + grid.folderBackgroundOffset + topPadding;
580
581            // Stroke width is 1dp
582            mStrokeWidth = dm.density;
583
584            float radius = getScaledRadius();
585            float shadowRadius = radius + mStrokeWidth;
586            int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
587            mShadowShader = new RadialGradient(0, 0, 1,
588                    new int[] {shadowColor, Color.TRANSPARENT},
589                    new float[] {radius / shadowRadius, 1},
590                    Shader.TileMode.CLAMP);
591
592            invalidate();
593        }
594
595        int getRadius() {
596            return previewSize / 2;
597        }
598
599        int getScaledRadius() {
600            return (int) (mScale * getRadius());
601        }
602
603        int getOffsetX() {
604            return basePreviewOffsetX - (getScaledRadius() - getRadius());
605        }
606
607        int getOffsetY() {
608            return basePreviewOffsetY - (getScaledRadius() - getRadius());
609        }
610
611        /**
612         * Returns the progress of the scale animation, where 0 means the scale is at 1f
613         * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
614         */
615        float getScaleProgress() {
616            return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
617        }
618
619        void invalidate() {
620            if (mInvalidateDelegate != null) {
621                mInvalidateDelegate.invalidate();
622            }
623
624            if (mDrawingDelegate != null) {
625                mDrawingDelegate.invalidate();
626            }
627        }
628
629        void setInvalidateDelegate(View invalidateDelegate) {
630            mInvalidateDelegate = invalidateDelegate;
631            invalidate();
632        }
633
634        public void drawBackground(Canvas canvas) {
635            mPaint.setStyle(Paint.Style.FILL);
636            int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
637            mPaint.setColor(Color.argb(alpha, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY));
638
639            drawCircle(canvas, 0 /* deltaRadius */);
640
641            // Draw shadow.
642            if (mShadowShader == null) {
643                return;
644            }
645            float radius = getScaledRadius();
646            float shadowRadius = radius + mStrokeWidth;
647            mPaint.setColor(Color.BLACK);
648            int offsetX = getOffsetX();
649            int offsetY = getOffsetY();
650            final int saveCount;
651            if (canvas.isHardwareAccelerated()) {
652                saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
653                        offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius,
654                        null, Canvas.CLIP_TO_LAYER_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
655
656            } else {
657                saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
658                clipCanvasSoftware(canvas, Region.Op.DIFFERENCE);
659            }
660
661            mShaderMatrix.setScale(shadowRadius, shadowRadius);
662            mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
663            mShadowShader.setLocalMatrix(mShaderMatrix);
664            mPaint.setShader(mShadowShader);
665            canvas.drawPaint(mPaint);
666            mPaint.setShader(null);
667
668            if (canvas.isHardwareAccelerated()) {
669                mPaint.setXfermode(mShadowPorterDuffXfermode);
670                canvas.drawCircle(radius + offsetX, radius + offsetY, radius, mPaint);
671                mPaint.setXfermode(null);
672            }
673
674            canvas.restoreToCount(saveCount);
675        }
676
677        public void drawBackgroundStroke(Canvas canvas) {
678            mPaint.setColor(Color.argb(255, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY));
679            mPaint.setStyle(Paint.Style.STROKE);
680            mPaint.setStrokeWidth(mStrokeWidth);
681            drawCircle(canvas, 1 /* deltaRadius */);
682        }
683
684        public void drawLeaveBehind(Canvas canvas) {
685            float originalScale = mScale;
686            mScale = 0.5f;
687
688            mPaint.setStyle(Paint.Style.FILL);
689            mPaint.setColor(Color.argb(160, 245, 245, 245));
690            drawCircle(canvas, 0 /* deltaRadius */);
691
692            mScale = originalScale;
693        }
694
695        private void drawCircle(Canvas canvas,float deltaRadius) {
696            float radius = getScaledRadius();
697            canvas.drawCircle(radius + getOffsetX(), radius + getOffsetY(),
698                    radius - deltaRadius, mPaint);
699        }
700
701        // It is the callers responsibility to save and restore the canvas layers.
702        private void clipCanvasSoftware(Canvas canvas, Region.Op op) {
703            mPath.reset();
704            float r = getScaledRadius();
705            mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW);
706            canvas.clipPath(mPath, op);
707        }
708
709        // It is the callers responsibility to save and restore the canvas layers.
710        private void clipCanvasHardware(Canvas canvas) {
711            mPaint.setColor(Color.BLACK);
712            mPaint.setXfermode(mClipPorterDuffXfermode);
713
714            float radius = getScaledRadius();
715            mShaderMatrix.setScale(radius, radius);
716            mShaderMatrix.postTranslate(radius + getOffsetX(), radius + getOffsetY());
717            mClipShader.setLocalMatrix(mShaderMatrix);
718            mPaint.setShader(mClipShader);
719            canvas.drawPaint(mPaint);
720            mPaint.setXfermode(null);
721            mPaint.setShader(null);
722        }
723
724        private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
725            if (mDrawingDelegate != delegate) {
726                delegate.addFolderBackground(this);
727            }
728
729            mDrawingDelegate = delegate;
730            delegateCellX = cellX;
731            delegateCellY = cellY;
732
733            invalidate();
734        }
735
736        private void clearDrawingDelegate() {
737            if (mDrawingDelegate != null) {
738                mDrawingDelegate.removeFolderBackground(this);
739            }
740
741            mDrawingDelegate = null;
742            invalidate();
743        }
744
745        private boolean drawingDelegated() {
746            return mDrawingDelegate != null;
747        }
748
749        private void animateScale(float finalScale, float finalMultiplier,
750                final Runnable onStart, final Runnable onEnd) {
751            final float scale0 = mScale;
752            final float scale1 = finalScale;
753
754            final float bgMultiplier0 = mColorMultiplier;
755            final float bgMultiplier1 = finalMultiplier;
756
757            if (mScaleAnimator != null) {
758                mScaleAnimator.cancel();
759            }
760
761            mScaleAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f);
762
763            mScaleAnimator.addUpdateListener(new AnimatorUpdateListener() {
764                @Override
765                public void onAnimationUpdate(ValueAnimator animation) {
766                    float prog = animation.getAnimatedFraction();
767                    mScale = prog * scale1 + (1 - prog) * scale0;
768                    mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
769                    invalidate();
770                }
771            });
772            mScaleAnimator.addListener(new AnimatorListenerAdapter() {
773                @Override
774                public void onAnimationStart(Animator animation) {
775                    if (onStart != null) {
776                        onStart.run();
777                    }
778                }
779
780                @Override
781                public void onAnimationEnd(Animator animation) {
782                    if (onEnd != null) {
783                        onEnd.run();
784                    }
785                    mScaleAnimator = null;
786                }
787            });
788
789            mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
790            mScaleAnimator.start();
791        }
792
793        public void animateToAccept(final CellLayout cl, final int cellX, final int cellY) {
794            Runnable onStart = new Runnable() {
795                @Override
796                public void run() {
797                    delegateDrawing(cl, cellX, cellY);
798                }
799            };
800            animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER, onStart, null);
801        }
802
803        public void animateToRest() {
804            // This can be called multiple times -- we need to make sure the drawing delegate
805            // is saved and restored at the beginning of the animation, since cancelling the
806            // existing animation can clear the delgate.
807            final CellLayout cl = mDrawingDelegate;
808            final int cellX = delegateCellX;
809            final int cellY = delegateCellY;
810
811            Runnable onStart = new Runnable() {
812                @Override
813                public void run() {
814                    delegateDrawing(cl, cellX, cellY);
815                }
816            };
817            Runnable onEnd = new Runnable() {
818                @Override
819                public void run() {
820                    clearDrawingDelegate();
821                }
822            };
823            animateScale(1f, 1f, onStart, onEnd);
824        }
825    }
826
827    public void setFolderBackground(PreviewBackground bg) {
828        mBackground = bg;
829        mBackground.setInvalidateDelegate(this);
830    }
831
832    @Override
833    protected void dispatchDraw(Canvas canvas) {
834        super.dispatchDraw(canvas);
835
836        if (mReferenceDrawable != null) {
837            computePreviewDrawingParams(mReferenceDrawable);
838        }
839
840        if (!mBackground.drawingDelegated()) {
841            mBackground.drawBackground(canvas);
842        }
843
844        if (mFolder == null) return;
845        if (mFolder.getItemCount() == 0 && !mAnimating) return;
846
847        final int saveCount;
848
849        if (canvas.isHardwareAccelerated()) {
850            saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null,
851                    Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
852        } else {
853            saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
854            if (mPreviewLayoutRule.clipToBackground()) {
855                mBackground.clipCanvasSoftware(canvas, Region.Op.INTERSECT);
856            }
857        }
858
859        // The items are drawn in coordinates relative to the preview offset
860        canvas.translate(mBackground.basePreviewOffsetX, mBackground.basePreviewOffsetY);
861
862        // The first item should be drawn last (ie. on top of later items)
863        for (int i = mDrawingParams.size() - 1; i >= 0; i--) {
864            PreviewItemDrawingParams p = mDrawingParams.get(i);
865            if (!p.hidden) {
866                drawPreviewItem(canvas, p);
867            }
868        }
869        canvas.translate(-mBackground.basePreviewOffsetX, -mBackground.basePreviewOffsetY);
870
871        if (mPreviewLayoutRule.clipToBackground() && canvas.isHardwareAccelerated()) {
872            mBackground.clipCanvasHardware(canvas);
873        }
874        canvas.restoreToCount(saveCount);
875
876        if (mPreviewLayoutRule.clipToBackground() && !mBackground.drawingDelegated()) {
877            mBackground.drawBackgroundStroke(canvas);
878        }
879
880        if ((mBadgeInfo != null && mBadgeInfo.hasBadge()) || mBadgeScale > 0) {
881            int offsetX = mBackground.getOffsetX();
882            int offsetY = mBackground.getOffsetY();
883            int previewSize = (int) (mBackground.previewSize * mBackground.mScale);
884            mTempBounds.set(offsetX, offsetY, offsetX + previewSize, offsetY + previewSize);
885
886            // If we are animating to the accepting state, animate the badge out.
887            float badgeScale = Math.max(0, mBadgeScale - mBackground.getScaleProgress());
888            mTempSpaceForBadgeOffset.set(getWidth() - mTempBounds.right, mTempBounds.top);
889            IconPalette badgePalette = IconPalette.getFolderBadgePalette(getResources());
890            mBadgeRenderer.draw(canvas, badgePalette, mBadgeInfo, mTempBounds,
891                    badgeScale, mTempSpaceForBadgeOffset);
892        }
893    }
894
895    class FolderPreviewItemAnim {
896        ValueAnimator mValueAnimator;
897        float finalScale;
898        float finalTransX;
899        float finalTransY;
900
901        /**
902         *
903         * @param params layout params to animate
904         * @param index0 original index of the item to be animated
905         * @param nItems0 original number of items in the preview
906         * @param index1 new index of the item to be animated
907         * @param nItems1 new number of items in the preview
908         * @param duration duration in ms of the animation
909         * @param onCompleteRunnable runnable to execute upon animation completion
910         */
911        public FolderPreviewItemAnim(final PreviewItemDrawingParams params, int index0, int nItems0,
912                int index1, int nItems1, int duration, final Runnable onCompleteRunnable) {
913
914            computePreviewItemDrawingParams(index1, nItems1, mTmpParams);
915
916            finalScale = mTmpParams.scale;
917            finalTransX = mTmpParams.transX;
918            finalTransY = mTmpParams.transY;
919
920            computePreviewItemDrawingParams(index0, nItems0, mTmpParams);
921
922            final float scale0 = mTmpParams.scale;
923            final float transX0 = mTmpParams.transX;
924            final float transY0 = mTmpParams.transY;
925
926            mValueAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f);
927            mValueAnimator.addUpdateListener(new AnimatorUpdateListener(){
928                public void onAnimationUpdate(ValueAnimator animation) {
929                    float progress = animation.getAnimatedFraction();
930
931                    params.transX = transX0 + progress * (finalTransX - transX0);
932                    params.transY = transY0 + progress * (finalTransY - transY0);
933                    params.scale = scale0 + progress * (finalScale - scale0);
934                    invalidate();
935                }
936            });
937
938            mValueAnimator.addListener(new AnimatorListenerAdapter() {
939                @Override
940                public void onAnimationStart(Animator animation) {
941                }
942
943                @Override
944                public void onAnimationEnd(Animator animation) {
945                    if (onCompleteRunnable != null) {
946                        onCompleteRunnable.run();
947                    }
948                    params.anim = null;
949                }
950            });
951            mValueAnimator.setDuration(duration);
952        }
953
954        public void start() {
955            mValueAnimator.start();
956        }
957
958        public void cancel() {
959            mValueAnimator.cancel();
960        }
961
962        public boolean hasEqualFinalState(FolderPreviewItemAnim anim) {
963            return finalTransY == anim.finalTransY && finalTransX == anim.finalTransX &&
964                    finalScale == anim.finalScale;
965
966        }
967    }
968
969    private void animateFirstItem(final Drawable d, int duration, final boolean reverse,
970            final Runnable onCompleteRunnable) {
971
972        FolderPreviewItemAnim anim;
973        if (!reverse) {
974            anim = new FolderPreviewItemAnim(mDrawingParams.get(0), -1, -1, 0, 2, duration,
975                    onCompleteRunnable);
976        } else {
977            anim = new FolderPreviewItemAnim(mDrawingParams.get(0), 0, 2, -1, -1, duration,
978                    onCompleteRunnable);
979        }
980        anim.start();
981    }
982
983    public void setTextVisible(boolean visible) {
984        if (visible) {
985            mFolderName.setVisibility(VISIBLE);
986        } else {
987            mFolderName.setVisibility(INVISIBLE);
988        }
989    }
990
991    public boolean getTextVisible() {
992        return mFolderName.getVisibility() == VISIBLE;
993    }
994
995    private void updateItemDrawingParams(boolean animate) {
996        List<View> items = mPreviewLayoutRule.getItemsToDisplay(mFolder);
997        int nItemsInPreview = items.size();
998
999        int prevNumItems = mDrawingParams.size();
1000
1001        // We adjust the size of the list to match the number of items in the preview
1002        while (nItemsInPreview < mDrawingParams.size()) {
1003            mDrawingParams.remove(mDrawingParams.size() - 1);
1004        }
1005        while (nItemsInPreview > mDrawingParams.size()) {
1006            mDrawingParams.add(new PreviewItemDrawingParams(0, 0, 0, 0));
1007        }
1008
1009        for (int i = 0; i < mDrawingParams.size(); i++) {
1010            PreviewItemDrawingParams p = mDrawingParams.get(i);
1011            p.drawable = ((TextView) items.get(i)).getCompoundDrawables()[1];
1012
1013            if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) {
1014                computePreviewItemDrawingParams(i, nItemsInPreview, p);
1015                if (mReferenceDrawable == null) {
1016                    mReferenceDrawable = p.drawable;
1017                }
1018            } else {
1019                FolderPreviewItemAnim anim = new FolderPreviewItemAnim(p, i, prevNumItems, i,
1020                        nItemsInPreview, DROP_IN_ANIMATION_DURATION, null);
1021
1022                if (p.anim != null) {
1023                    if (p.anim.hasEqualFinalState(anim)) {
1024                        // do nothing, let the current animation finish
1025                        continue;
1026                    }
1027                    p.anim.cancel();
1028                }
1029                p.anim = anim;
1030                p.anim.start();
1031            }
1032        }
1033    }
1034
1035    @Override
1036    public void onItemsChanged(boolean animate) {
1037        updateItemDrawingParams(animate);
1038        invalidate();
1039        requestLayout();
1040    }
1041
1042    @Override
1043    public void prepareAutoUpdate() {
1044    }
1045
1046    @Override
1047    public void onAdd(ShortcutInfo item) {
1048        boolean wasBadged = mBadgeInfo.hasBadge();
1049        mBadgeInfo.addBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item));
1050        boolean isBadged = mBadgeInfo.hasBadge();
1051        updateBadgeScale(wasBadged, isBadged);
1052        invalidate();
1053        requestLayout();
1054    }
1055
1056    @Override
1057    public void onRemove(ShortcutInfo item) {
1058        boolean wasBadged = mBadgeInfo.hasBadge();
1059        mBadgeInfo.subtractBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item));
1060        boolean isBadged = mBadgeInfo.hasBadge();
1061        updateBadgeScale(wasBadged, isBadged);
1062        invalidate();
1063        requestLayout();
1064    }
1065
1066    @Override
1067    public void onTitleChanged(CharSequence title) {
1068        mFolderName.setText(title);
1069        setContentDescription(getContext().getString(R.string.folder_name_format, title));
1070    }
1071
1072    @Override
1073    public boolean onTouchEvent(MotionEvent event) {
1074        // Call the superclass onTouchEvent first, because sometimes it changes the state to
1075        // isPressed() on an ACTION_UP
1076        boolean result = super.onTouchEvent(event);
1077
1078        // Check for a stylus button press, if it occurs cancel any long press checks.
1079        if (mStylusEventHelper.onMotionEvent(event)) {
1080            mLongPressHelper.cancelLongPress();
1081            return true;
1082        }
1083
1084        switch (event.getAction()) {
1085            case MotionEvent.ACTION_DOWN:
1086                mLongPressHelper.postCheckForLongPress();
1087                break;
1088            case MotionEvent.ACTION_CANCEL:
1089            case MotionEvent.ACTION_UP:
1090                mLongPressHelper.cancelLongPress();
1091                break;
1092            case MotionEvent.ACTION_MOVE:
1093                if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
1094                    mLongPressHelper.cancelLongPress();
1095                }
1096                break;
1097        }
1098        return result;
1099    }
1100
1101    @Override
1102    public void cancelLongPress() {
1103        super.cancelLongPress();
1104        mLongPressHelper.cancelLongPress();
1105    }
1106
1107    public void removeListeners() {
1108        mInfo.removeListener(this);
1109        mInfo.removeListener(mFolder);
1110    }
1111
1112    public void shrinkAndFadeIn(boolean animate) {
1113        final CellLayout cl = (CellLayout) getParent().getParent();
1114        ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
1115
1116        // We remove and re-draw the FolderIcon in-case it has changed
1117        final PreviewImageView previewImage = PreviewImageView.get(getContext());
1118        previewImage.removeFromParent();
1119        copyToPreview(previewImage);
1120
1121        if (cl != null) {
1122            cl.clearFolderLeaveBehind();
1123        }
1124
1125        ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(previewImage, 1, 1, 1);
1126        oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration));
1127        oa.addListener(new AnimatorListenerAdapter() {
1128            @Override
1129            public void onAnimationEnd(Animator animation) {
1130                if (cl != null) {
1131                    // Remove the ImageView copy of the FolderIcon and make the original visible.
1132                    previewImage.removeFromParent();
1133                    setVisibility(View.VISIBLE);
1134                }
1135            }
1136        });
1137        oa.start();
1138        if (!animate) {
1139            oa.end();
1140        }
1141    }
1142
1143    public void growAndFadeOut() {
1144        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
1145        // While the folder is open, the position of the icon cannot change.
1146        lp.canReorder = false;
1147        if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
1148            CellLayout cl = (CellLayout) getParent().getParent();
1149            cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
1150        }
1151
1152        // Push an ImageView copy of the FolderIcon into the DragLayer and hide the original
1153        PreviewImageView previewImage = PreviewImageView.get(getContext());
1154        copyToPreview(previewImage);
1155        setVisibility(View.INVISIBLE);
1156
1157        ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(previewImage, 0, 1.5f, 1.5f);
1158        oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration));
1159        oa.start();
1160    }
1161
1162    /**
1163     * This method draws the FolderIcon to an ImageView and then adds and positions that ImageView
1164     * in the DragLayer in the exact absolute location of the original FolderIcon.
1165     */
1166    private void copyToPreview(PreviewImageView previewImageView) {
1167        previewImageView.copy(this);
1168        if (mFolder != null) {
1169            previewImageView.setPivotX(mFolder.getPivotXForIconAnimation());
1170            previewImageView.setPivotY(mFolder.getPivotYForIconAnimation());
1171            mFolder.bringToFront();
1172        }
1173    }
1174
1175    public interface PreviewLayoutRule {
1176        PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
1177            PreviewItemDrawingParams params);
1178        void init(int availableSpace, int intrinsicIconSize, boolean rtl);
1179        float scaleForItem(int index, int totalNumItems);
1180        int maxNumItems();
1181        boolean clipToBackground();
1182        List<View> getItemsToDisplay(Folder folder);
1183    }
1184}
1185