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