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