Home | History | Annotate | Download | only in launcher2
      1 /*
      2  * Copyright (C) 2011 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.launcher2;
     18 
     19 import android.animation.TimeInterpolator;
     20 import android.animation.ValueAnimator;
     21 import android.animation.ValueAnimator.AnimatorUpdateListener;
     22 import android.content.Context;
     23 import android.content.res.ColorStateList;
     24 import android.content.res.Configuration;
     25 import android.content.res.Resources;
     26 import android.graphics.PointF;
     27 import android.graphics.Rect;
     28 import android.graphics.drawable.TransitionDrawable;
     29 import android.util.AttributeSet;
     30 import android.view.View;
     31 import android.view.ViewConfiguration;
     32 import android.view.ViewGroup;
     33 import android.view.animation.AnimationUtils;
     34 import android.view.animation.DecelerateInterpolator;
     35 import android.view.animation.LinearInterpolator;
     36 
     37 import com.android.launcher.R;
     38 
     39 public class DeleteDropTarget extends ButtonDropTarget {
     40     private static int DELETE_ANIMATION_DURATION = 285;
     41     private static int FLING_DELETE_ANIMATION_DURATION = 350;
     42     private static float FLING_TO_DELETE_FRICTION = 0.035f;
     43     private static int MODE_FLING_DELETE_TO_TRASH = 0;
     44     private static int MODE_FLING_DELETE_ALONG_VECTOR = 1;
     45 
     46     private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR;
     47 
     48     private ColorStateList mOriginalTextColor;
     49     private TransitionDrawable mUninstallDrawable;
     50     private TransitionDrawable mRemoveDrawable;
     51     private TransitionDrawable mCurrentDrawable;
     52 
     53     public DeleteDropTarget(Context context, AttributeSet attrs) {
     54         this(context, attrs, 0);
     55     }
     56 
     57     public DeleteDropTarget(Context context, AttributeSet attrs, int defStyle) {
     58         super(context, attrs, defStyle);
     59     }
     60 
     61     @Override
     62     protected void onFinishInflate() {
     63         super.onFinishInflate();
     64 
     65         // Get the drawable
     66         mOriginalTextColor = getTextColors();
     67 
     68         // Get the hover color
     69         Resources r = getResources();
     70         mHoverColor = r.getColor(R.color.delete_target_hover_tint);
     71         mUninstallDrawable = (TransitionDrawable)
     72                 r.getDrawable(R.drawable.uninstall_target_selector);
     73         mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector);
     74 
     75         mRemoveDrawable.setCrossFadeEnabled(true);
     76         mUninstallDrawable.setCrossFadeEnabled(true);
     77 
     78         // The current drawable is set to either the remove drawable or the uninstall drawable
     79         // and is initially set to the remove drawable, as set in the layout xml.
     80         mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
     81 
     82         // Remove the text in the Phone UI in landscape
     83         int orientation = getResources().getConfiguration().orientation;
     84         if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
     85             if (!LauncherApplication.isScreenLarge()) {
     86                 setText("");
     87             }
     88         }
     89     }
     90 
     91     private boolean isAllAppsApplication(DragSource source, Object info) {
     92         return (source instanceof AppsCustomizePagedView) && (info instanceof ApplicationInfo);
     93     }
     94     private boolean isAllAppsWidget(DragSource source, Object info) {
     95         if (source instanceof AppsCustomizePagedView) {
     96             if (info instanceof PendingAddItemInfo) {
     97                 PendingAddItemInfo addInfo = (PendingAddItemInfo) info;
     98                 switch (addInfo.itemType) {
     99                     case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
    100                     case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
    101                         return true;
    102                 }
    103             }
    104         }
    105         return false;
    106     }
    107     private boolean isDragSourceWorkspaceOrFolder(DragObject d) {
    108         return (d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder);
    109     }
    110     private boolean isWorkspaceOrFolderApplication(DragObject d) {
    111         return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof ShortcutInfo);
    112     }
    113     private boolean isWorkspaceOrFolderWidget(DragObject d) {
    114         return isDragSourceWorkspaceOrFolder(d) && (d.dragInfo instanceof LauncherAppWidgetInfo);
    115     }
    116     private boolean isWorkspaceFolder(DragObject d) {
    117         return (d.dragSource instanceof Workspace) && (d.dragInfo instanceof FolderInfo);
    118     }
    119 
    120     private void setHoverColor() {
    121         mCurrentDrawable.startTransition(mTransitionDuration);
    122         setTextColor(mHoverColor);
    123     }
    124     private void resetHoverColor() {
    125         mCurrentDrawable.resetTransition();
    126         setTextColor(mOriginalTextColor);
    127     }
    128 
    129     @Override
    130     public boolean acceptDrop(DragObject d) {
    131         // We can remove everything including App shortcuts, folders, widgets, etc.
    132         return true;
    133     }
    134 
    135     @Override
    136     public void onDragStart(DragSource source, Object info, int dragAction) {
    137         boolean isVisible = true;
    138         boolean isUninstall = false;
    139 
    140         // If we are dragging a widget from AppsCustomize, hide the delete target
    141         if (isAllAppsWidget(source, info)) {
    142             isVisible = false;
    143         }
    144 
    145         // If we are dragging an application from AppsCustomize, only show the control if we can
    146         // delete the app (it was downloaded), and rename the string to "uninstall" in such a case
    147         if (isAllAppsApplication(source, info)) {
    148             ApplicationInfo appInfo = (ApplicationInfo) info;
    149             if ((appInfo.flags & ApplicationInfo.DOWNLOADED_FLAG) != 0) {
    150                 isUninstall = true;
    151             } else {
    152                 isVisible = false;
    153             }
    154         }
    155 
    156         if (isUninstall) {
    157             setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null);
    158         } else {
    159             setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null);
    160         }
    161         mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
    162 
    163         mActive = isVisible;
    164         resetHoverColor();
    165         ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE);
    166         if (getText().length() > 0) {
    167             setText(isUninstall ? R.string.delete_target_uninstall_label
    168                 : R.string.delete_target_label);
    169         }
    170     }
    171 
    172     @Override
    173     public void onDragEnd() {
    174         super.onDragEnd();
    175         mActive = false;
    176     }
    177 
    178     public void onDragEnter(DragObject d) {
    179         super.onDragEnter(d);
    180 
    181         setHoverColor();
    182     }
    183 
    184     public void onDragExit(DragObject d) {
    185         super.onDragExit(d);
    186 
    187         if (!d.dragComplete) {
    188             resetHoverColor();
    189         } else {
    190             // Restore the hover color if we are deleting
    191             d.dragView.setColor(mHoverColor);
    192         }
    193     }
    194 
    195     private void animateToTrashAndCompleteDrop(final DragObject d) {
    196         DragLayer dragLayer = mLauncher.getDragLayer();
    197         Rect from = new Rect();
    198         dragLayer.getViewRectRelativeToSelf(d.dragView, from);
    199         Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
    200                 mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
    201         float scale = (float) to.width() / from.width();
    202 
    203         mSearchDropTargetBar.deferOnDragEnd();
    204         Runnable onAnimationEndRunnable = new Runnable() {
    205             @Override
    206             public void run() {
    207                 mSearchDropTargetBar.onDragEnd();
    208                 mLauncher.exitSpringLoadedDragMode();
    209                 completeDrop(d);
    210             }
    211         };
    212         dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
    213                 DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2),
    214                 new LinearInterpolator(), onAnimationEndRunnable,
    215                 DragLayer.ANIMATION_END_DISAPPEAR, null);
    216     }
    217 
    218     private void completeDrop(DragObject d) {
    219         ItemInfo item = (ItemInfo) d.dragInfo;
    220 
    221         if (isAllAppsApplication(d.dragSource, item)) {
    222             // Uninstall the application if it is being dragged from AppsCustomize
    223             mLauncher.startApplicationUninstallActivity((ApplicationInfo) item);
    224         } else if (isWorkspaceOrFolderApplication(d)) {
    225             LauncherModel.deleteItemFromDatabase(mLauncher, item);
    226         } else if (isWorkspaceFolder(d)) {
    227             // Remove the folder from the workspace and delete the contents from launcher model
    228             FolderInfo folderInfo = (FolderInfo) item;
    229             mLauncher.removeFolder(folderInfo);
    230             LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo);
    231         } else if (isWorkspaceOrFolderWidget(d)) {
    232             // Remove the widget from the workspace
    233             mLauncher.removeAppWidget((LauncherAppWidgetInfo) item);
    234             LauncherModel.deleteItemFromDatabase(mLauncher, item);
    235 
    236             final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item;
    237             final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost();
    238             if (appWidgetHost != null) {
    239                 // Deleting an app widget ID is a void call but writes to disk before returning
    240                 // to the caller...
    241                 new Thread("deleteAppWidgetId") {
    242                     public void run() {
    243                         appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId);
    244                     }
    245                 }.start();
    246             }
    247         }
    248     }
    249 
    250     public void onDrop(DragObject d) {
    251         animateToTrashAndCompleteDrop(d);
    252     }
    253 
    254     /**
    255      * Creates an animation from the current drag view to the delete trash icon.
    256      */
    257     private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer,
    258             DragObject d, PointF vel, ViewConfiguration config) {
    259         final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
    260                 mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
    261         final Rect from = new Rect();
    262         dragLayer.getViewRectRelativeToSelf(d.dragView, from);
    263 
    264         // Calculate how far along the velocity vector we should put the intermediate point on
    265         // the bezier curve
    266         float velocity = Math.abs(vel.length());
    267         float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f));
    268         int offsetY = (int) (-from.top * vp);
    269         int offsetX = (int) (offsetY / (vel.y / vel.x));
    270         final float y2 = from.top + offsetY;                        // intermediate t/l
    271         final float x2 = from.left + offsetX;
    272         final float x1 = from.left;                                 // drag view t/l
    273         final float y1 = from.top;
    274         final float x3 = to.left;                                   // delete target t/l
    275         final float y3 = to.top;
    276 
    277         final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() {
    278             @Override
    279             public float getInterpolation(float t) {
    280                 return t * t * t * t * t * t * t * t;
    281             }
    282         };
    283         return new AnimatorUpdateListener() {
    284             @Override
    285             public void onAnimationUpdate(ValueAnimator animation) {
    286                 final DragView dragView = (DragView) dragLayer.getAnimatedView();
    287                 float t = ((Float) animation.getAnimatedValue()).floatValue();
    288                 float tp = scaleAlphaInterpolator.getInterpolation(t);
    289                 float initialScale = dragView.getInitialScale();
    290                 float finalAlpha = 0.5f;
    291                 float scale = dragView.getScaleX();
    292                 float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f;
    293                 float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f;
    294                 float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) +
    295                         (t * t) * x3;
    296                 float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) +
    297                         (t * t) * y3;
    298 
    299                 dragView.setTranslationX(x);
    300                 dragView.setTranslationY(y);
    301                 dragView.setScaleX(initialScale * (1f - tp));
    302                 dragView.setScaleY(initialScale * (1f - tp));
    303                 dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp));
    304             }
    305         };
    306     }
    307 
    308     /**
    309      * Creates an animation from the current drag view along its current velocity vector.
    310      * For this animation, the alpha runs for a fixed duration and we update the position
    311      * progressively.
    312      */
    313     private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener {
    314         private DragLayer mDragLayer;
    315         private PointF mVelocity;
    316         private Rect mFrom;
    317         private long mPrevTime;
    318         private boolean mHasOffsetForScale;
    319         private float mFriction;
    320 
    321         private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f);
    322 
    323         public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from,
    324                 long startTime, float friction) {
    325             mDragLayer = dragLayer;
    326             mVelocity = vel;
    327             mFrom = from;
    328             mPrevTime = startTime;
    329             mFriction = 1f - (dragLayer.getResources().getDisplayMetrics().density * friction);
    330         }
    331 
    332         @Override
    333         public void onAnimationUpdate(ValueAnimator animation) {
    334             final DragView dragView = (DragView) mDragLayer.getAnimatedView();
    335             float t = ((Float) animation.getAnimatedValue()).floatValue();
    336             long curTime = AnimationUtils.currentAnimationTimeMillis();
    337 
    338             if (!mHasOffsetForScale) {
    339                 mHasOffsetForScale = true;
    340                 float scale = dragView.getScaleX();
    341                 float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f;
    342                 float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f;
    343 
    344                 mFrom.left += xOffset;
    345                 mFrom.top += yOffset;
    346             }
    347 
    348             mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f);
    349             mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f);
    350 
    351             dragView.setTranslationX(mFrom.left);
    352             dragView.setTranslationY(mFrom.top);
    353             dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t));
    354 
    355             mVelocity.x *= mFriction;
    356             mVelocity.y *= mFriction;
    357             mPrevTime = curTime;
    358         }
    359     };
    360     private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer,
    361             DragObject d, PointF vel, final long startTime, final int duration,
    362             ViewConfiguration config) {
    363         final Rect from = new Rect();
    364         dragLayer.getViewRectRelativeToSelf(d.dragView, from);
    365 
    366         return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime,
    367                 FLING_TO_DELETE_FRICTION);
    368     }
    369 
    370     public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) {
    371         final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView;
    372 
    373         // Don't highlight the icon as it's animating
    374         d.dragView.setColor(0);
    375         d.dragView.updateInitialScaleToCurrentScale();
    376         // Don't highlight the target if we are flinging from AllApps
    377         if (isAllApps) {
    378             resetHoverColor();
    379         }
    380 
    381         if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
    382             // Defer animating out the drop target if we are animating to it
    383             mSearchDropTargetBar.deferOnDragEnd();
    384             mSearchDropTargetBar.finishAnimations();
    385         }
    386 
    387         final ViewConfiguration config = ViewConfiguration.get(mLauncher);
    388         final DragLayer dragLayer = mLauncher.getDragLayer();
    389         final int duration = FLING_DELETE_ANIMATION_DURATION;
    390         final long startTime = AnimationUtils.currentAnimationTimeMillis();
    391 
    392         // NOTE: Because it takes time for the first frame of animation to actually be
    393         // called and we expect the animation to be a continuation of the fling, we have
    394         // to account for the time that has elapsed since the fling finished.  And since
    395         // we don't have a startDelay, we will always get call to update when we call
    396         // start() (which we want to ignore).
    397         final TimeInterpolator tInterpolator = new TimeInterpolator() {
    398             private int mCount = -1;
    399             private float mOffset = 0f;
    400 
    401             @Override
    402             public float getInterpolation(float t) {
    403                 if (mCount < 0) {
    404                     mCount++;
    405                 } else if (mCount == 0) {
    406                     mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() -
    407                             startTime) / duration);
    408                     mCount++;
    409                 }
    410                 return Math.min(1f, mOffset + t);
    411             }
    412         };
    413         AnimatorUpdateListener updateCb = null;
    414         if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
    415             updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config);
    416         } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) {
    417             updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime,
    418                     duration, config);
    419         }
    420         Runnable onAnimationEndRunnable = new Runnable() {
    421             @Override
    422             public void run() {
    423                 mSearchDropTargetBar.onDragEnd();
    424 
    425                 // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up
    426                 // itself, otherwise, complete the drop to initiate the deletion process
    427                 if (!isAllApps) {
    428                     mLauncher.exitSpringLoadedDragMode();
    429                     completeDrop(d);
    430                 }
    431                 mLauncher.getDragController().onDeferredEndFling(d);
    432             }
    433         };
    434         dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable,
    435                 DragLayer.ANIMATION_END_DISAPPEAR, null);
    436     }
    437 }
    438