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 
17 package com.android.launcher3.folder;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.animation.ValueAnimator;
23 import android.animation.ValueAnimator.AnimatorUpdateListener;
24 import android.content.Context;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Matrix;
28 import android.graphics.Paint;
29 import android.graphics.Path;
30 import android.graphics.Point;
31 import android.graphics.PorterDuff;
32 import android.graphics.PorterDuffXfermode;
33 import android.graphics.RadialGradient;
34 import android.graphics.Rect;
35 import android.graphics.Region;
36 import android.graphics.Shader;
37 import android.graphics.drawable.Drawable;
38 import android.os.Parcelable;
39 import android.util.AttributeSet;
40 import android.util.DisplayMetrics;
41 import android.util.Property;
42 import android.view.LayoutInflater;
43 import android.view.MotionEvent;
44 import android.view.View;
45 import android.view.ViewConfiguration;
46 import android.view.ViewGroup;
47 import android.view.animation.AccelerateInterpolator;
48 import android.view.animation.DecelerateInterpolator;
49 import android.widget.FrameLayout;
50 import android.widget.TextView;
51 
52 import com.android.launcher3.Alarm;
53 import com.android.launcher3.AppInfo;
54 import com.android.launcher3.BubbleTextView;
55 import com.android.launcher3.CellLayout;
56 import com.android.launcher3.CheckLongPressHelper;
57 import com.android.launcher3.DeviceProfile;
58 import com.android.launcher3.DropTarget.DragObject;
59 import com.android.launcher3.FastBitmapDrawable;
60 import com.android.launcher3.FolderInfo;
61 import com.android.launcher3.FolderInfo.FolderListener;
62 import com.android.launcher3.ItemInfo;
63 import com.android.launcher3.Launcher;
64 import com.android.launcher3.LauncherAnimUtils;
65 import com.android.launcher3.LauncherSettings;
66 import com.android.launcher3.OnAlarmListener;
67 import com.android.launcher3.R;
68 import com.android.launcher3.ShortcutInfo;
69 import com.android.launcher3.SimpleOnStylusPressListener;
70 import com.android.launcher3.StylusEventHelper;
71 import com.android.launcher3.Utilities;
72 import com.android.launcher3.Workspace;
73 import com.android.launcher3.badge.BadgeRenderer;
74 import com.android.launcher3.badge.FolderBadgeInfo;
75 import com.android.launcher3.config.FeatureFlags;
76 import com.android.launcher3.dragndrop.DragLayer;
77 import com.android.launcher3.dragndrop.DragView;
78 import com.android.launcher3.graphics.IconPalette;
79 import com.android.launcher3.util.Thunk;
80 
81 import java.util.ArrayList;
82 import java.util.List;
83 
84 /**
85  * An icon that can appear on in the workspace representing an {@link Folder}.
86  */
87 public 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 
FolderIcon(Context context, AttributeSet attrs)154     public FolderIcon(Context context, AttributeSet attrs) {
155         super(context, attrs);
156         init();
157     }
158 
FolderIcon(Context context)159     public FolderIcon(Context context) {
160         super(context);
161         init();
162     }
163 
init()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 
fromXml(int resId, Launcher launcher, ViewGroup group, FolderInfo folderInfo)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
onSaveInstanceState()213     protected Parcelable onSaveInstanceState() {
214         sStaticValuesDirty = true;
215         return super.onSaveInstanceState();
216     }
217 
getFolder()218     public Folder getFolder() {
219         return mFolder;
220     }
221 
setFolder(Folder folder)222     private void setFolder(Folder folder) {
223         mFolder = folder;
224         updateItemDrawingParams(false);
225     }
226 
willAcceptItem(ItemInfo item)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 
acceptDrop(ItemInfo dragInfo)235     public boolean acceptDrop(ItemInfo dragInfo) {
236         final ItemInfo item = dragInfo;
237         return !mFolder.isDestroyed() && willAcceptItem(item);
238     }
239 
addItem(ShortcutInfo item)240     public void addItem(ShortcutInfo item) {
241         mInfo.add(item, true);
242     }
243 
onDragEnter(ItemInfo dragInfo)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 
prepareCreate(final View destView)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 
performCreateAnimation(final ShortcutInfo destInfo, final View destView, final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect, float scaleRelativeToDragLayer, Runnable postAnimationRunnable)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 
performDestroyAnimation(final View finalView, Runnable onCompleteRunnable)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 
onDragExit()304     public void onDragExit() {
305         mBackground.animateToRest();
306         mOpenAlarm.cancelAlarm();
307     }
308 
onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect, float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable)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