OSDN Git Service

25123fb1da99f429f2857af49c92424e5b09f436
[android-x86/packages-apps-Launcher3.git] / src / com / android / launcher3 / folder / FolderIcon.java
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
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             mBadgeRenderer.draw(canvas, IconPalette.FOLDER_ICON_PALETTE, mBadgeInfo, mTempBounds,
890                     badgeScale, mTempSpaceForBadgeOffset);
891         }
892     }
893
894     class FolderPreviewItemAnim {
895         ValueAnimator mValueAnimator;
896         float finalScale;
897         float finalTransX;
898         float finalTransY;
899
900         /**
901          *
902          * @param params layout params to animate
903          * @param index0 original index of the item to be animated
904          * @param nItems0 original number of items in the preview
905          * @param index1 new index of the item to be animated
906          * @param nItems1 new number of items in the preview
907          * @param duration duration in ms of the animation
908          * @param onCompleteRunnable runnable to execute upon animation completion
909          */
910         public FolderPreviewItemAnim(final PreviewItemDrawingParams params, int index0, int nItems0,
911                 int index1, int nItems1, int duration, final Runnable onCompleteRunnable) {
912
913             computePreviewItemDrawingParams(index1, nItems1, mTmpParams);
914
915             finalScale = mTmpParams.scale;
916             finalTransX = mTmpParams.transX;
917             finalTransY = mTmpParams.transY;
918
919             computePreviewItemDrawingParams(index0, nItems0, mTmpParams);
920
921             final float scale0 = mTmpParams.scale;
922             final float transX0 = mTmpParams.transX;
923             final float transY0 = mTmpParams.transY;
924
925             mValueAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f);
926             mValueAnimator.addUpdateListener(new AnimatorUpdateListener(){
927                 public void onAnimationUpdate(ValueAnimator animation) {
928                     float progress = animation.getAnimatedFraction();
929
930                     params.transX = transX0 + progress * (finalTransX - transX0);
931                     params.transY = transY0 + progress * (finalTransY - transY0);
932                     params.scale = scale0 + progress * (finalScale - scale0);
933                     invalidate();
934                 }
935             });
936
937             mValueAnimator.addListener(new AnimatorListenerAdapter() {
938                 @Override
939                 public void onAnimationStart(Animator animation) {
940                 }
941
942                 @Override
943                 public void onAnimationEnd(Animator animation) {
944                     if (onCompleteRunnable != null) {
945                         onCompleteRunnable.run();
946                     }
947                     params.anim = null;
948                 }
949             });
950             mValueAnimator.setDuration(duration);
951         }
952
953         public void start() {
954             mValueAnimator.start();
955         }
956
957         public void cancel() {
958             mValueAnimator.cancel();
959         }
960
961         public boolean hasEqualFinalState(FolderPreviewItemAnim anim) {
962             return finalTransY == anim.finalTransY && finalTransX == anim.finalTransX &&
963                     finalScale == anim.finalScale;
964
965         }
966     }
967
968     private void animateFirstItem(final Drawable d, int duration, final boolean reverse,
969             final Runnable onCompleteRunnable) {
970
971         FolderPreviewItemAnim anim;
972         if (!reverse) {
973             anim = new FolderPreviewItemAnim(mDrawingParams.get(0), -1, -1, 0, 2, duration,
974                     onCompleteRunnable);
975         } else {
976             anim = new FolderPreviewItemAnim(mDrawingParams.get(0), 0, 2, -1, -1, duration,
977                     onCompleteRunnable);
978         }
979         anim.start();
980     }
981
982     public void setTextVisible(boolean visible) {
983         if (visible) {
984             mFolderName.setVisibility(VISIBLE);
985         } else {
986             mFolderName.setVisibility(INVISIBLE);
987         }
988     }
989
990     public boolean getTextVisible() {
991         return mFolderName.getVisibility() == VISIBLE;
992     }
993
994     private void updateItemDrawingParams(boolean animate) {
995         List<View> items = mPreviewLayoutRule.getItemsToDisplay(mFolder);
996         int nItemsInPreview = items.size();
997
998         int prevNumItems = mDrawingParams.size();
999
1000         // We adjust the size of the list to match the number of items in the preview
1001         while (nItemsInPreview < mDrawingParams.size()) {
1002             mDrawingParams.remove(mDrawingParams.size() - 1);
1003         }
1004         while (nItemsInPreview > mDrawingParams.size()) {
1005             mDrawingParams.add(new PreviewItemDrawingParams(0, 0, 0, 0));
1006         }
1007
1008         for (int i = 0; i < mDrawingParams.size(); i++) {
1009             PreviewItemDrawingParams p = mDrawingParams.get(i);
1010             p.drawable = ((TextView) items.get(i)).getCompoundDrawables()[1];
1011
1012             if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) {
1013                 computePreviewItemDrawingParams(i, nItemsInPreview, p);
1014                 if (mReferenceDrawable == null) {
1015                     mReferenceDrawable = p.drawable;
1016                 }
1017             } else {
1018                 FolderPreviewItemAnim anim = new FolderPreviewItemAnim(p, i, prevNumItems, i,
1019                         nItemsInPreview, DROP_IN_ANIMATION_DURATION, null);
1020
1021                 if (p.anim != null) {
1022                     if (p.anim.hasEqualFinalState(anim)) {
1023                         // do nothing, let the current animation finish
1024                         continue;
1025                     }
1026                     p.anim.cancel();
1027                 }
1028                 p.anim = anim;
1029                 p.anim.start();
1030             }
1031         }
1032     }
1033
1034     @Override
1035     public void onItemsChanged(boolean animate) {
1036         updateItemDrawingParams(animate);
1037         invalidate();
1038         requestLayout();
1039     }
1040
1041     @Override
1042     public void prepareAutoUpdate() {
1043     }
1044
1045     @Override
1046     public void onAdd(ShortcutInfo item) {
1047         boolean wasBadged = mBadgeInfo.hasBadge();
1048         mBadgeInfo.addBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item));
1049         boolean isBadged = mBadgeInfo.hasBadge();
1050         updateBadgeScale(wasBadged, isBadged);
1051         invalidate();
1052         requestLayout();
1053     }
1054
1055     @Override
1056     public void onRemove(ShortcutInfo item) {
1057         boolean wasBadged = mBadgeInfo.hasBadge();
1058         mBadgeInfo.subtractBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item));
1059         boolean isBadged = mBadgeInfo.hasBadge();
1060         updateBadgeScale(wasBadged, isBadged);
1061         invalidate();
1062         requestLayout();
1063     }
1064
1065     @Override
1066     public void onTitleChanged(CharSequence title) {
1067         mFolderName.setText(title);
1068         setContentDescription(getContext().getString(R.string.folder_name_format, title));
1069     }
1070
1071     @Override
1072     public boolean onTouchEvent(MotionEvent event) {
1073         // Call the superclass onTouchEvent first, because sometimes it changes the state to
1074         // isPressed() on an ACTION_UP
1075         boolean result = super.onTouchEvent(event);
1076
1077         // Check for a stylus button press, if it occurs cancel any long press checks.
1078         if (mStylusEventHelper.onMotionEvent(event)) {
1079             mLongPressHelper.cancelLongPress();
1080             return true;
1081         }
1082
1083         switch (event.getAction()) {
1084             case MotionEvent.ACTION_DOWN:
1085                 mLongPressHelper.postCheckForLongPress();
1086                 break;
1087             case MotionEvent.ACTION_CANCEL:
1088             case MotionEvent.ACTION_UP:
1089                 mLongPressHelper.cancelLongPress();
1090                 break;
1091             case MotionEvent.ACTION_MOVE:
1092                 if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
1093                     mLongPressHelper.cancelLongPress();
1094                 }
1095                 break;
1096         }
1097         return result;
1098     }
1099
1100     @Override
1101     public void cancelLongPress() {
1102         super.cancelLongPress();
1103         mLongPressHelper.cancelLongPress();
1104     }
1105
1106     public void removeListeners() {
1107         mInfo.removeListener(this);
1108         mInfo.removeListener(mFolder);
1109     }
1110
1111     public void shrinkAndFadeIn(boolean animate) {
1112         final CellLayout cl = (CellLayout) getParent().getParent();
1113         ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
1114
1115         // We remove and re-draw the FolderIcon in-case it has changed
1116         final PreviewImageView previewImage = PreviewImageView.get(getContext());
1117         previewImage.removeFromParent();
1118         copyToPreview(previewImage);
1119
1120         if (cl != null) {
1121             cl.clearFolderLeaveBehind();
1122         }
1123
1124         ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(previewImage, 1, 1, 1);
1125         oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration));
1126         oa.addListener(new AnimatorListenerAdapter() {
1127             @Override
1128             public void onAnimationEnd(Animator animation) {
1129                 if (cl != null) {
1130                     // Remove the ImageView copy of the FolderIcon and make the original visible.
1131                     previewImage.removeFromParent();
1132                     setVisibility(View.VISIBLE);
1133                 }
1134             }
1135         });
1136         oa.start();
1137         if (!animate) {
1138             oa.end();
1139         }
1140     }
1141
1142     public void growAndFadeOut() {
1143         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
1144         // While the folder is open, the position of the icon cannot change.
1145         lp.canReorder = false;
1146         if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
1147             CellLayout cl = (CellLayout) getParent().getParent();
1148             cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
1149         }
1150
1151         // Push an ImageView copy of the FolderIcon into the DragLayer and hide the original
1152         PreviewImageView previewImage = PreviewImageView.get(getContext());
1153         copyToPreview(previewImage);
1154         setVisibility(View.INVISIBLE);
1155
1156         ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(previewImage, 0, 1.5f, 1.5f);
1157         oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration));
1158         oa.start();
1159     }
1160
1161     /**
1162      * This method draws the FolderIcon to an ImageView and then adds and positions that ImageView
1163      * in the DragLayer in the exact absolute location of the original FolderIcon.
1164      */
1165     private void copyToPreview(PreviewImageView previewImageView) {
1166         previewImageView.copy(this);
1167         if (mFolder != null) {
1168             previewImageView.setPivotX(mFolder.getPivotXForIconAnimation());
1169             previewImageView.setPivotY(mFolder.getPivotYForIconAnimation());
1170             mFolder.bringToFront();
1171         }
1172     }
1173
1174     public interface PreviewLayoutRule {
1175         PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
1176             PreviewItemDrawingParams params);
1177         void init(int availableSpace, int intrinsicIconSize, boolean rtl);
1178         float scaleForItem(int index, int totalNumItems);
1179         int maxNumItems();
1180         boolean clipToBackground();
1181         List<View> getItemsToDisplay(Folder folder);
1182     }
1183 }