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;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.animation.ValueAnimator.AnimatorUpdateListener;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.PorterDuff;
28 import android.graphics.Rect;
29 import android.graphics.drawable.Drawable;
30 import android.os.Looper;
31 import android.os.Parcelable;
32 import android.util.AttributeSet;
33 import android.view.LayoutInflater;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewConfiguration;
37 import android.view.ViewGroup;
38 import android.view.animation.AccelerateInterpolator;
39 import android.view.animation.DecelerateInterpolator;
40 import android.widget.FrameLayout;
41 import android.widget.ImageView;
42 import android.widget.TextView;
43 
44 import com.android.launcher3.DropTarget.DragObject;
45 import com.android.launcher3.FolderInfo.FolderListener;
46 import com.android.launcher3.util.Thunk;
47 
48 import java.util.ArrayList;
49 
50 /**
51  * An icon that can appear on in the workspace representing an {@link UserFolder}.
52  */
53 public class FolderIcon extends FrameLayout implements FolderListener {
54     @Thunk Launcher mLauncher;
55     @Thunk Folder mFolder;
56     private FolderInfo mInfo;
57     @Thunk static boolean sStaticValuesDirty = true;
58 
59     private CheckLongPressHelper mLongPressHelper;
60     private StylusEventHelper mStylusEventHelper;
61 
62     // The number of icons to display in the
63     public static final int NUM_ITEMS_IN_PREVIEW = 3;
64     private static final int CONSUMPTION_ANIMATION_DURATION = 100;
65     private static final int DROP_IN_ANIMATION_DURATION = 400;
66     private static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
67     private static final int FINAL_ITEM_ANIMATION_DURATION = 200;
68 
69     // The degree to which the inner ring grows when accepting drop
70     private static final float INNER_RING_GROWTH_FACTOR = 0.15f;
71 
72     // The degree to which the outer ring is scaled in its natural state
73     private static final float OUTER_RING_GROWTH_FACTOR = 0.3f;
74 
75     // The amount of vertical spread between items in the stack [0...1]
76     private static final float PERSPECTIVE_SHIFT_FACTOR = 0.18f;
77 
78     // Flag as to whether or not to draw an outer ring. Currently none is designed.
79     public static final boolean HAS_OUTER_RING = true;
80 
81     // Flag whether the folder should open itself when an item is dragged over is enabled.
82     public static final boolean SPRING_LOADING_ENABLED = true;
83 
84     // The degree to which the item in the back of the stack is scaled [0...1]
85     // (0 means it's not scaled at all, 1 means it's scaled to nothing)
86     private static final float PERSPECTIVE_SCALE_FACTOR = 0.35f;
87 
88     // Delay when drag enters until the folder opens, in miliseconds.
89     private static final int ON_OPEN_DELAY = 800;
90 
91     public static Drawable sSharedFolderLeaveBehind = null;
92 
93     @Thunk ImageView mPreviewBackground;
94     @Thunk BubbleTextView mFolderName;
95 
96     FolderRingAnimator mFolderRingAnimator = null;
97 
98     // These variables are all associated with the drawing of the preview; they are stored
99     // as member variables for shared usage and to avoid computation on each frame
100     private int mIntrinsicIconSize;
101     private float mBaselineIconScale;
102     private int mBaselineIconSize;
103     private int mAvailableSpaceInPreview;
104     private int mTotalWidth = -1;
105     private int mPreviewOffsetX;
106     private int mPreviewOffsetY;
107     private float mMaxPerspectiveShift;
108     boolean mAnimating = false;
109     private Rect mOldBounds = new Rect();
110 
111     private float mSlop;
112 
113     private PreviewItemDrawingParams mParams = new PreviewItemDrawingParams(0, 0, 0, 0);
114     @Thunk PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0);
115     @Thunk ArrayList<ShortcutInfo> mHiddenItems = new ArrayList<ShortcutInfo>();
116 
117     private Alarm mOpenAlarm = new Alarm();
118     @Thunk ItemInfo mDragInfo;
119 
FolderIcon(Context context, AttributeSet attrs)120     public FolderIcon(Context context, AttributeSet attrs) {
121         super(context, attrs);
122         init();
123     }
124 
FolderIcon(Context context)125     public FolderIcon(Context context) {
126         super(context);
127         init();
128     }
129 
init()130     private void init() {
131         mLongPressHelper = new CheckLongPressHelper(this);
132         mStylusEventHelper = new StylusEventHelper(this);
133         setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
134     }
135 
isDropEnabled()136     public boolean isDropEnabled() {
137         final ViewGroup cellLayoutChildren = (ViewGroup) getParent();
138         final ViewGroup cellLayout = (ViewGroup) cellLayoutChildren.getParent();
139         final Workspace workspace = (Workspace) cellLayout.getParent();
140         return !workspace.workspaceInModalState();
141     }
142 
fromXml(int resId, Launcher launcher, ViewGroup group, FolderInfo folderInfo, IconCache iconCache)143     static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
144             FolderInfo folderInfo, IconCache iconCache) {
145         @SuppressWarnings("all") // suppress dead code warning
146         final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
147         if (error) {
148             throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " +
149                     "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " +
150                     "is dependent on this");
151         }
152 
153         DeviceProfile grid = launcher.getDeviceProfile();
154 
155         FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false);
156         icon.setClipToPadding(false);
157         icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name);
158         icon.mFolderName.setText(folderInfo.title);
159         icon.mFolderName.setCompoundDrawablePadding(0);
160         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
161         lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
162 
163         // Offset the preview background to center this view accordingly
164         icon.mPreviewBackground = (ImageView) icon.findViewById(R.id.preview_background);
165         lp = (FrameLayout.LayoutParams) icon.mPreviewBackground.getLayoutParams();
166         lp.topMargin = grid.folderBackgroundOffset;
167         lp.width = grid.folderIconSizePx;
168         lp.height = grid.folderIconSizePx;
169 
170         icon.setTag(folderInfo);
171         icon.setOnClickListener(launcher);
172         icon.mInfo = folderInfo;
173         icon.mLauncher = launcher;
174         icon.setContentDescription(String.format(launcher.getString(R.string.folder_name_format),
175                 folderInfo.title));
176         Folder folder = Folder.fromXml(launcher);
177         folder.setDragController(launcher.getDragController());
178         folder.setFolderIcon(icon);
179         folder.bind(folderInfo);
180         icon.mFolder = folder;
181 
182         icon.mFolderRingAnimator = new FolderRingAnimator(launcher, icon);
183         folderInfo.addListener(icon);
184 
185         icon.setOnFocusChangeListener(launcher.mFocusHandler);
186         return icon;
187     }
188 
189     @Override
onSaveInstanceState()190     protected Parcelable onSaveInstanceState() {
191         sStaticValuesDirty = true;
192         return super.onSaveInstanceState();
193     }
194 
195     public static class FolderRingAnimator {
196         public int mCellX;
197         public int mCellY;
198         @Thunk CellLayout mCellLayout;
199         public float mOuterRingSize;
200         public float mInnerRingSize;
201         public FolderIcon mFolderIcon = null;
202         public static Drawable sSharedOuterRingDrawable = null;
203         public static Drawable sSharedInnerRingDrawable = null;
204         public static int sPreviewSize = -1;
205         public static int sPreviewPadding = -1;
206 
207         private ValueAnimator mAcceptAnimator;
208         private ValueAnimator mNeutralAnimator;
209 
FolderRingAnimator(Launcher launcher, FolderIcon folderIcon)210         public FolderRingAnimator(Launcher launcher, FolderIcon folderIcon) {
211             mFolderIcon = folderIcon;
212             Resources res = launcher.getResources();
213 
214             // We need to reload the static values when configuration changes in case they are
215             // different in another configuration
216             if (sStaticValuesDirty) {
217                 if (Looper.myLooper() != Looper.getMainLooper()) {
218                     throw new RuntimeException("FolderRingAnimator loading drawables on non-UI thread "
219                             + Thread.currentThread());
220                 }
221 
222                 DeviceProfile grid = launcher.getDeviceProfile();
223                 sPreviewSize = grid.folderIconSizePx;
224                 sPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding);
225                 sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer);
226                 sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_nolip);
227                 sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest);
228                 sStaticValuesDirty = false;
229             }
230         }
231 
animateToAcceptState()232         public void animateToAcceptState() {
233             if (mNeutralAnimator != null) {
234                 mNeutralAnimator.cancel();
235             }
236             mAcceptAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f);
237             mAcceptAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
238 
239             final int previewSize = sPreviewSize;
240             mAcceptAnimator.addUpdateListener(new AnimatorUpdateListener() {
241                 public void onAnimationUpdate(ValueAnimator animation) {
242                     final float percent = (Float) animation.getAnimatedValue();
243                     mOuterRingSize = (1 + percent * OUTER_RING_GROWTH_FACTOR) * previewSize;
244                     mInnerRingSize = (1 + percent * INNER_RING_GROWTH_FACTOR) * previewSize;
245                     if (mCellLayout != null) {
246                         mCellLayout.invalidate();
247                     }
248                 }
249             });
250             mAcceptAnimator.addListener(new AnimatorListenerAdapter() {
251                 @Override
252                 public void onAnimationStart(Animator animation) {
253                     if (mFolderIcon != null) {
254                         mFolderIcon.mPreviewBackground.setVisibility(INVISIBLE);
255                     }
256                 }
257             });
258             mAcceptAnimator.start();
259         }
260 
animateToNaturalState()261         public void animateToNaturalState() {
262             if (mAcceptAnimator != null) {
263                 mAcceptAnimator.cancel();
264             }
265             mNeutralAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f);
266             mNeutralAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
267 
268             final int previewSize = sPreviewSize;
269             mNeutralAnimator.addUpdateListener(new AnimatorUpdateListener() {
270                 public void onAnimationUpdate(ValueAnimator animation) {
271                     final float percent = (Float) animation.getAnimatedValue();
272                     mOuterRingSize = (1 + (1 - percent) * OUTER_RING_GROWTH_FACTOR) * previewSize;
273                     mInnerRingSize = (1 + (1 - percent) * INNER_RING_GROWTH_FACTOR) * previewSize;
274                     if (mCellLayout != null) {
275                         mCellLayout.invalidate();
276                     }
277                 }
278             });
279             mNeutralAnimator.addListener(new AnimatorListenerAdapter() {
280                 @Override
281                 public void onAnimationEnd(Animator animation) {
282                     if (mCellLayout != null) {
283                         mCellLayout.hideFolderAccept(FolderRingAnimator.this);
284                     }
285                     if (mFolderIcon != null) {
286                         mFolderIcon.mPreviewBackground.setVisibility(VISIBLE);
287                     }
288                 }
289             });
290             mNeutralAnimator.start();
291         }
292 
293         // Location is expressed in window coordinates
getCell(int[] loc)294         public void getCell(int[] loc) {
295             loc[0] = mCellX;
296             loc[1] = mCellY;
297         }
298 
299         // Location is expressed in window coordinates
setCell(int x, int y)300         public void setCell(int x, int y) {
301             mCellX = x;
302             mCellY = y;
303         }
304 
setCellLayout(CellLayout layout)305         public void setCellLayout(CellLayout layout) {
306             mCellLayout = layout;
307         }
308 
getOuterRingSize()309         public float getOuterRingSize() {
310             return mOuterRingSize;
311         }
312 
getInnerRingSize()313         public float getInnerRingSize() {
314             return mInnerRingSize;
315         }
316     }
317 
getFolder()318     public Folder getFolder() {
319         return mFolder;
320     }
321 
getFolderInfo()322     FolderInfo getFolderInfo() {
323         return mInfo;
324     }
325 
willAcceptItem(ItemInfo item)326     private boolean willAcceptItem(ItemInfo item) {
327         final int itemType = item.itemType;
328         return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
329                 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
330                 !mFolder.isFull() && item != mInfo && !mInfo.opened);
331     }
332 
acceptDrop(Object dragInfo)333     public boolean acceptDrop(Object dragInfo) {
334         final ItemInfo item = (ItemInfo) dragInfo;
335         return !mFolder.isDestroyed() && willAcceptItem(item);
336     }
337 
addItem(ShortcutInfo item)338     public void addItem(ShortcutInfo item) {
339         mInfo.add(item);
340     }
341 
onDragEnter(Object dragInfo)342     public void onDragEnter(Object dragInfo) {
343         if (mFolder.isDestroyed() || !willAcceptItem((ItemInfo) dragInfo)) return;
344         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
345         CellLayout layout = (CellLayout) getParent().getParent();
346         mFolderRingAnimator.setCell(lp.cellX, lp.cellY);
347         mFolderRingAnimator.setCellLayout(layout);
348         mFolderRingAnimator.animateToAcceptState();
349         layout.showFolderAccept(mFolderRingAnimator);
350         mOpenAlarm.setOnAlarmListener(mOnOpenListener);
351         if (SPRING_LOADING_ENABLED &&
352                 ((dragInfo instanceof AppInfo) || (dragInfo instanceof ShortcutInfo))) {
353             // TODO: we currently don't support spring-loading for PendingAddShortcutInfos even
354             // though widget-style shortcuts can be added to folders. The issue is that we need
355             // to deal with configuration activities which are currently handled in
356             // Workspace#onDropExternal.
357             mOpenAlarm.setAlarm(ON_OPEN_DELAY);
358         }
359         mDragInfo = (ItemInfo) dragInfo;
360     }
361 
onDragOver(Object dragInfo)362     public void onDragOver(Object dragInfo) {
363     }
364 
365     OnAlarmListener mOnOpenListener = new OnAlarmListener() {
366         public void onAlarm(Alarm alarm) {
367             ShortcutInfo item;
368             if (mDragInfo instanceof AppInfo) {
369                 // Came from all apps -- make a copy.
370                 item = ((AppInfo) mDragInfo).makeShortcut();
371                 item.spanX = 1;
372                 item.spanY = 1;
373             } else {
374                 // ShortcutInfo
375                 item = (ShortcutInfo) mDragInfo;
376             }
377             mFolder.beginExternalDrag(item);
378             mLauncher.openFolder(FolderIcon.this);
379         }
380     };
381 
performCreateAnimation(final ShortcutInfo destInfo, final View destView, final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect, float scaleRelativeToDragLayer, Runnable postAnimationRunnable)382     public void performCreateAnimation(final ShortcutInfo destInfo, final View destView,
383             final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect,
384             float scaleRelativeToDragLayer, Runnable postAnimationRunnable) {
385 
386         // These correspond two the drawable and view that the icon was dropped _onto_
387         Drawable animateDrawable = getTopDrawable((TextView) destView);
388         computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
389                 destView.getMeasuredWidth());
390 
391         // This will animate the first item from it's position as an icon into its
392         // position as the first item in the preview
393         animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION, false, null);
394         addItem(destInfo);
395 
396         // This will animate the dragView (srcView) into the new folder
397         onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable, null);
398     }
399 
performDestroyAnimation(final View finalView, Runnable onCompleteRunnable)400     public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) {
401         Drawable animateDrawable = getTopDrawable((TextView) finalView);
402         computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
403                 finalView.getMeasuredWidth());
404 
405         // This will animate the first item from it's position as an icon into its
406         // position as the first item in the preview
407         animateFirstItem(animateDrawable, FINAL_ITEM_ANIMATION_DURATION, true,
408                 onCompleteRunnable);
409     }
410 
onDragExit(Object dragInfo)411     public void onDragExit(Object dragInfo) {
412         onDragExit();
413     }
414 
onDragExit()415     public void onDragExit() {
416         mFolderRingAnimator.animateToNaturalState();
417         mOpenAlarm.cancelAlarm();
418     }
419 
onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect, float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable, DragObject d)420     private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect,
421             float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable,
422             DragObject d) {
423         item.cellX = -1;
424         item.cellY = -1;
425 
426         // Typically, the animateView corresponds to the DragView; however, if this is being done
427         // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
428         // will not have a view to animate
429         if (animateView != null) {
430             DragLayer dragLayer = mLauncher.getDragLayer();
431             Rect from = new Rect();
432             dragLayer.getViewRectRelativeToSelf(animateView, from);
433             Rect to = finalRect;
434             if (to == null) {
435                 to = new Rect();
436                 Workspace workspace = mLauncher.getWorkspace();
437                 // Set cellLayout and this to it's final state to compute final animation locations
438                 workspace.setFinalTransitionTransform((CellLayout) getParent().getParent());
439                 float scaleX = getScaleX();
440                 float scaleY = getScaleY();
441                 setScaleX(1.0f);
442                 setScaleY(1.0f);
443                 scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to);
444                 // Finished computing final animation locations, restore current state
445                 setScaleX(scaleX);
446                 setScaleY(scaleY);
447                 workspace.resetTransitionTransform((CellLayout) getParent().getParent());
448             }
449 
450             int[] center = new int[2];
451             float scale = getLocalCenterForIndex(index, center);
452             center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]);
453             center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]);
454 
455             to.offset(center[0] - animateView.getMeasuredWidth() / 2,
456                       center[1] - animateView.getMeasuredHeight() / 2);
457 
458             float finalAlpha = index < NUM_ITEMS_IN_PREVIEW ? 0.5f : 0f;
459 
460             float finalScale = scale * scaleRelativeToDragLayer;
461             dragLayer.animateView(animateView, from, to, finalAlpha,
462                     1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
463                     new DecelerateInterpolator(2), new AccelerateInterpolator(2),
464                     postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null);
465             addItem(item);
466             mHiddenItems.add(item);
467             mFolder.hideItem(item);
468             postDelayed(new Runnable() {
469                 public void run() {
470                     mHiddenItems.remove(item);
471                     mFolder.showItem(item);
472                     invalidate();
473                 }
474             }, DROP_IN_ANIMATION_DURATION);
475         } else {
476             addItem(item);
477         }
478     }
479 
480     public void onDrop(DragObject d) {
481         ShortcutInfo item;
482         if (d.dragInfo instanceof AppInfo) {
483             // Came from all apps -- make a copy
484             item = ((AppInfo) d.dragInfo).makeShortcut();
485         } else {
486             item = (ShortcutInfo) d.dragInfo;
487         }
488         mFolder.notifyDrop();
489         onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable, d);
490     }
491 
492     private void computePreviewDrawingParams(int drawableSize, int totalSize) {
493         if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize) {
494             DeviceProfile grid = mLauncher.getDeviceProfile();
495 
496             mIntrinsicIconSize = drawableSize;
497             mTotalWidth = totalSize;
498 
499             final int previewSize = mPreviewBackground.getLayoutParams().height;
500             final int previewPadding = FolderRingAnimator.sPreviewPadding;
501 
502             mAvailableSpaceInPreview = (previewSize - 2 * previewPadding);
503             // cos(45) = 0.707  + ~= 0.1) = 0.8f
504             int adjustedAvailableSpace = (int) ((mAvailableSpaceInPreview / 2) * (1 + 0.8f));
505 
506             int unscaledHeight = (int) (mIntrinsicIconSize * (1 + PERSPECTIVE_SHIFT_FACTOR));
507 
508             mBaselineIconScale = (1.0f * adjustedAvailableSpace / unscaledHeight);
509 
510             mBaselineIconSize = (int) (mIntrinsicIconSize * mBaselineIconScale);
511             mMaxPerspectiveShift = mBaselineIconSize * PERSPECTIVE_SHIFT_FACTOR;
512 
513             mPreviewOffsetX = (mTotalWidth - mAvailableSpaceInPreview) / 2;
514             mPreviewOffsetY = previewPadding + grid.folderBackgroundOffset;
515         }
516     }
517 
518     private void computePreviewDrawingParams(Drawable d) {
519         computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth());
520     }
521 
522     class PreviewItemDrawingParams {
523         PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
524             this.transX = transX;
525             this.transY = transY;
526             this.scale = scale;
527             this.overlayAlpha = overlayAlpha;
528         }
529         float transX;
530         float transY;
531         float scale;
532         float overlayAlpha;
533         Drawable drawable;
534     }
535 
536     private float getLocalCenterForIndex(int index, int[] center) {
537         mParams = computePreviewItemDrawingParams(Math.min(NUM_ITEMS_IN_PREVIEW, index), mParams);
538 
539         mParams.transX += mPreviewOffsetX;
540         mParams.transY += mPreviewOffsetY;
541         float offsetX = mParams.transX + (mParams.scale * mIntrinsicIconSize) / 2;
542         float offsetY = mParams.transY + (mParams.scale * mIntrinsicIconSize) / 2;
543 
544         center[0] = (int) Math.round(offsetX);
545         center[1] = (int) Math.round(offsetY);
546         return mParams.scale;
547     }
548 
549     private PreviewItemDrawingParams computePreviewItemDrawingParams(int index,
550             PreviewItemDrawingParams params) {
551         index = NUM_ITEMS_IN_PREVIEW - index - 1;
552         float r = (index * 1.0f) / (NUM_ITEMS_IN_PREVIEW - 1);
553         float scale = (1 - PERSPECTIVE_SCALE_FACTOR * (1 - r));
554 
555         float offset = (1 - r) * mMaxPerspectiveShift;
556         float scaledSize = scale * mBaselineIconSize;
557         float scaleOffsetCorrection = (1 - scale) * mBaselineIconSize;
558 
559         // We want to imagine our coordinates from the bottom left, growing up and to the
560         // right. This is natural for the x-axis, but for the y-axis, we have to invert things.
561         float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection) + getPaddingTop();
562         float transX = (mAvailableSpaceInPreview - scaledSize) / 2;
563         float totalScale = mBaselineIconScale * scale;
564         final float overlayAlpha = (80 * (1 - r)) / 255f;
565 
566         if (params == null) {
567             params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha);
568         } else {
569             params.transX = transX;
570             params.transY = transY;
571             params.scale = totalScale;
572             params.overlayAlpha = overlayAlpha;
573         }
574         return params;
575     }
576 
577     private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) {
578         canvas.save();
579         canvas.translate(params.transX + mPreviewOffsetX, params.transY + mPreviewOffsetY);
580         canvas.scale(params.scale, params.scale);
581         Drawable d = params.drawable;
582 
583         if (d != null) {
584             mOldBounds.set(d.getBounds());
585             d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
586             if (d instanceof FastBitmapDrawable) {
587                 FastBitmapDrawable fd = (FastBitmapDrawable) d;
588                 float oldBrightness = fd.getBrightness();
589                 fd.setBrightness(params.overlayAlpha);
590                 d.draw(canvas);
591                 fd.setBrightness(oldBrightness);
592             } else {
593                 d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255),
594                         PorterDuff.Mode.SRC_ATOP);
595                 d.draw(canvas);
596                 d.clearColorFilter();
597             }
598             d.setBounds(mOldBounds);
599         }
600         canvas.restore();
601     }
602 
603     @Override
604     protected void dispatchDraw(Canvas canvas) {
605         super.dispatchDraw(canvas);
606 
607         if (mFolder == null) return;
608         if (mFolder.getItemCount() == 0 && !mAnimating) return;
609 
610         ArrayList<View> items = mFolder.getItemsInReadingOrder();
611         Drawable d;
612         TextView v;
613 
614         // Update our drawing parameters if necessary
615         if (mAnimating) {
616             computePreviewDrawingParams(mAnimParams.drawable);
617         } else {
618             v = (TextView) items.get(0);
619             d = getTopDrawable(v);
620             computePreviewDrawingParams(d);
621         }
622 
623         int nItemsInPreview = Math.min(items.size(), NUM_ITEMS_IN_PREVIEW);
624         if (!mAnimating) {
625             for (int i = nItemsInPreview - 1; i >= 0; i--) {
626                 v = (TextView) items.get(i);
627                 if (!mHiddenItems.contains(v.getTag())) {
628                     d = getTopDrawable(v);
629                     mParams = computePreviewItemDrawingParams(i, mParams);
630                     mParams.drawable = d;
631                     drawPreviewItem(canvas, mParams);
632                 }
633             }
634         } else {
635             drawPreviewItem(canvas, mAnimParams);
636         }
637     }
638 
getTopDrawable(TextView v)639     private Drawable getTopDrawable(TextView v) {
640         Drawable d = v.getCompoundDrawables()[1];
641         return (d instanceof PreloadIconDrawable) ? ((PreloadIconDrawable) d).mIcon : d;
642     }
643 
animateFirstItem(final Drawable d, int duration, final boolean reverse, final Runnable onCompleteRunnable)644     private void animateFirstItem(final Drawable d, int duration, final boolean reverse,
645             final Runnable onCompleteRunnable) {
646         final PreviewItemDrawingParams finalParams = computePreviewItemDrawingParams(0, null);
647 
648         float iconSize = mLauncher.getDeviceProfile().iconSizePx;
649         final float scale0 = iconSize / d.getIntrinsicWidth() ;
650         final float transX0 = (mAvailableSpaceInPreview - iconSize) / 2;
651         final float transY0 = (mAvailableSpaceInPreview - iconSize) / 2 + getPaddingTop();
652         mAnimParams.drawable = d;
653 
654         ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1.0f);
655         va.addUpdateListener(new AnimatorUpdateListener(){
656             public void onAnimationUpdate(ValueAnimator animation) {
657                 float progress = (Float) animation.getAnimatedValue();
658                 if (reverse) {
659                     progress = 1 - progress;
660                     mPreviewBackground.setAlpha(progress);
661                 }
662 
663                 mAnimParams.transX = transX0 + progress * (finalParams.transX - transX0);
664                 mAnimParams.transY = transY0 + progress * (finalParams.transY - transY0);
665                 mAnimParams.scale = scale0 + progress * (finalParams.scale - scale0);
666                 invalidate();
667             }
668         });
669         va.addListener(new AnimatorListenerAdapter() {
670             @Override
671             public void onAnimationStart(Animator animation) {
672                 mAnimating = true;
673             }
674             @Override
675             public void onAnimationEnd(Animator animation) {
676                 mAnimating = false;
677                 if (onCompleteRunnable != null) {
678                     onCompleteRunnable.run();
679                 }
680             }
681         });
682         va.setDuration(duration);
683         va.start();
684     }
685 
setTextVisible(boolean visible)686     public void setTextVisible(boolean visible) {
687         if (visible) {
688             mFolderName.setVisibility(VISIBLE);
689         } else {
690             mFolderName.setVisibility(INVISIBLE);
691         }
692     }
693 
getTextVisible()694     public boolean getTextVisible() {
695         return mFolderName.getVisibility() == VISIBLE;
696     }
697 
onItemsChanged()698     public void onItemsChanged() {
699         invalidate();
700         requestLayout();
701     }
702 
onAdd(ShortcutInfo item)703     public void onAdd(ShortcutInfo item) {
704         invalidate();
705         requestLayout();
706     }
707 
onRemove(ShortcutInfo item)708     public void onRemove(ShortcutInfo item) {
709         invalidate();
710         requestLayout();
711     }
712 
onTitleChanged(CharSequence title)713     public void onTitleChanged(CharSequence title) {
714         mFolderName.setText(title);
715         setContentDescription(String.format(getContext().getString(R.string.folder_name_format),
716                 title));
717     }
718 
719     @Override
onTouchEvent(MotionEvent event)720     public boolean onTouchEvent(MotionEvent event) {
721         // Call the superclass onTouchEvent first, because sometimes it changes the state to
722         // isPressed() on an ACTION_UP
723         boolean result = super.onTouchEvent(event);
724 
725         // Check for a stylus button press, if it occurs cancel any long press checks.
726         if (mStylusEventHelper.checkAndPerformStylusEvent(event)) {
727             mLongPressHelper.cancelLongPress();
728             return true;
729         }
730 
731         switch (event.getAction()) {
732             case MotionEvent.ACTION_DOWN:
733                 mLongPressHelper.postCheckForLongPress();
734                 break;
735             case MotionEvent.ACTION_CANCEL:
736             case MotionEvent.ACTION_UP:
737                 mLongPressHelper.cancelLongPress();
738                 break;
739             case MotionEvent.ACTION_MOVE:
740                 if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
741                     mLongPressHelper.cancelLongPress();
742                 }
743                 break;
744         }
745         return result;
746     }
747 
748     @Override
onAttachedToWindow()749     protected void onAttachedToWindow() {
750         super.onAttachedToWindow();
751         mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
752     }
753 
754     @Override
cancelLongPress()755     public void cancelLongPress() {
756         super.cancelLongPress();
757 
758         mLongPressHelper.cancelLongPress();
759     }
760 }
761