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.os.UserManager;
     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             // If the user is not allowed to access the app details page or uninstall, then don't
    156             // let them uninstall from here either.
    157             UserManager userManager = (UserManager)
    158                     getContext().getSystemService(Context.USER_SERVICE);
    159             if (userManager.hasUserRestriction(UserManager.DISALLOW_APPS_CONTROL)
    160                     || userManager.hasUserRestriction(UserManager.DISALLOW_UNINSTALL_APPS)) {
    161                 isVisible = false;
    162             }
    163         }
    164 
    165         if (isUninstall) {
    166             setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null);
    167         } else {
    168             setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null);
    169         }
    170         mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
    171 
    172         mActive = isVisible;
    173         resetHoverColor();
    174         ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE);
    175         if (getText().length() > 0) {
    176             setText(isUninstall ? R.string.delete_target_uninstall_label
    177                 : R.string.delete_target_label);
    178         }
    179     }
    180 
    181     @Override
    182     public void onDragEnd() {
    183         super.onDragEnd();
    184         mActive = false;
    185     }
    186 
    187     public void onDragEnter(DragObject d) {
    188         super.onDragEnter(d);
    189 
    190         setHoverColor();
    191     }
    192 
    193     public void onDragExit(DragObject d) {
    194         super.onDragExit(d);
    195 
    196         if (!d.dragComplete) {
    197             resetHoverColor();
    198         } else {
    199             // Restore the hover color if we are deleting
    200             d.dragView.setColor(mHoverColor);
    201         }
    202     }
    203 
    204     private void animateToTrashAndCompleteDrop(final DragObject d) {
    205         DragLayer dragLayer = mLauncher.getDragLayer();
    206         Rect from = new Rect();
    207         dragLayer.getViewRectRelativeToSelf(d.dragView, from);
    208         Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
    209                 mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
    210         float scale = (float) to.width() / from.width();
    211 
    212         mSearchDropTargetBar.deferOnDragEnd();
    213         Runnable onAnimationEndRunnable = new Runnable() {
    214             @Override
    215             public void run() {
    216                 mSearchDropTargetBar.onDragEnd();
    217                 mLauncher.exitSpringLoadedDragMode();
    218                 completeDrop(d);
    219             }
    220         };
    221         dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
    222                 DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2),
    223                 new LinearInterpolator(), onAnimationEndRunnable,
    224                 DragLayer.ANIMATION_END_DISAPPEAR, null);
    225     }
    226 
    227     private void completeDrop(DragObject d) {
    228         ItemInfo item = (ItemInfo) d.dragInfo;
    229 
    230         if (isAllAppsApplication(d.dragSource, item)) {
    231             // Uninstall the application if it is being dragged from AppsCustomize
    232             mLauncher.startApplicationUninstallActivity((ApplicationInfo) item, item.user);
    233         } else if (isWorkspaceOrFolderApplication(d)) {
    234             LauncherModel.deleteItemFromDatabase(mLauncher, item);
    235         } else if (isWorkspaceFolder(d)) {
    236             // Remove the folder from the workspace and delete the contents from launcher model
    237             FolderInfo folderInfo = (FolderInfo) item;
    238             mLauncher.removeFolder(folderInfo);
    239             LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo);
    240         } else if (isWorkspaceOrFolderWidget(d)) {
    241             // Remove the widget from the workspace
    242             mLauncher.removeAppWidget((LauncherAppWidgetInfo) item);
    243             LauncherModel.deleteItemFromDatabase(mLauncher, item);
    244 
    245             final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item;
    246             final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost();
    247             if (appWidgetHost != null) {
    248                 // Deleting an app widget ID is a void call but writes to disk before returning
    249                 // to the caller...
    250                 new Thread("deleteAppWidgetId") {
    251                     public void run() {
    252                         appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId);
    253                     }
    254                 }.start();
    255             }
    256         }
    257     }
    258 
    259     public void onDrop(DragObject d) {
    260         animateToTrashAndCompleteDrop(d);
    261     }
    262 
    263     /**
    264      * Creates an animation from the current drag view to the delete trash icon.
    265      */
    266     private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer,
    267             DragObject d, PointF vel, ViewConfiguration config) {
    268         final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
    269                 mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
    270         final Rect from = new Rect();
    271         dragLayer.getViewRectRelativeToSelf(d.dragView, from);
    272 
    273         // Calculate how far along the velocity vector we should put the intermediate point on
    274         // the bezier curve
    275         float velocity = Math.abs(vel.length());
    276         float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f));
    277         int offsetY = (int) (-from.top * vp);
    278         int offsetX = (int) (offsetY / (vel.y / vel.x));
    279         final float y2 = from.top + offsetY;                        // intermediate t/l
    280         final float x2 = from.left + offsetX;
    281         final float x1 = from.left;                                 // drag view t/l
    282         final float y1 = from.top;
    283         final float x3 = to.left;                                   // delete target t/l
    284         final float y3 = to.top;
    285 
    286         final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() {
    287             @Override
    288             public float getInterpolation(float t) {
    289                 return t * t * t * t * t * t * t * t;
    290             }
    291         };
    292         return new AnimatorUpdateListener() {
    293             @Override
    294             public void onAnimationUpdate(ValueAnimator animation) {
    295                 final DragView dragView = (DragView) dragLayer.getAnimatedView();
    296                 float t = ((Float) animation.getAnimatedValue()).floatValue();
    297                 float tp = scaleAlphaInterpolator.getInterpolation(t);
    298                 float initialScale = dragView.getInitialScale();
    299                 float finalAlpha = 0.5f;
    300                 float scale = dragView.getScaleX();
    301                 float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f;
    302                 float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f;
    303                 float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) +
    304                         (t * t) * x3;
    305                 float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) +
    306                         (t * t) * y3;
    307 
    308                 dragView.setTranslationX(x);
    309                 dragView.setTranslationY(y);
    310                 dragView.setScaleX(initialScale * (1f - tp));
    311                 dragView.setScaleY(initialScale * (1f - tp));
    312                 dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp));
    313             }
    314         };
    315     }
    316 
    317     /**
    318      * Creates an animation from the current drag view along its current velocity vector.
    319      * For this animation, the alpha runs for a fixed duration and we update the position
    320      * progressively.
    321      */
    322     private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener {
    323         private DragLayer mDragLayer;
    324         private PointF mVelocity;
    325         private Rect mFrom;
    326         private long mPrevTime;
    327         private boolean mHasOffsetForScale;
    328         private float mFriction;
    329 
    330         private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f);
    331 
    332         public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from,
    333                 long startTime, float friction) {
    334             mDragLayer = dragLayer;
    335             mVelocity = vel;
    336             mFrom = from;
    337             mPrevTime = startTime;
    338             mFriction = 1f - (dragLayer.getResources().getDisplayMetrics().density * friction);
    339         }
    340 
    341         @Override
    342         public void onAnimationUpdate(ValueAnimator animation) {
    343             final DragView dragView = (DragView) mDragLayer.getAnimatedView();
    344             float t = ((Float) animation.getAnimatedValue()).floatValue();
    345             long curTime = AnimationUtils.currentAnimationTimeMillis();
    346 
    347             if (!mHasOffsetForScale) {
    348                 mHasOffsetForScale = true;
    349                 float scale = dragView.getScaleX();
    350                 float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f;
    351                 float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f;
    352 
    353                 mFrom.left += xOffset;
    354                 mFrom.top += yOffset;
    355             }
    356 
    357             mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f);
    358             mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f);
    359 
    360             dragView.setTranslationX(mFrom.left);
    361             dragView.setTranslationY(mFrom.top);
    362             dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t));
    363 
    364             mVelocity.x *= mFriction;
    365             mVelocity.y *= mFriction;
    366             mPrevTime = curTime;
    367         }
    368     };
    369     private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer,
    370             DragObject d, PointF vel, final long startTime, final int duration,
    371             ViewConfiguration config) {
    372         final Rect from = new Rect();
    373         dragLayer.getViewRectRelativeToSelf(d.dragView, from);
    374 
    375         return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime,
    376                 FLING_TO_DELETE_FRICTION);
    377     }
    378 
    379     public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) {
    380         final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView;
    381 
    382         // Don't highlight the icon as it's animating
    383         d.dragView.setColor(0);
    384         d.dragView.updateInitialScaleToCurrentScale();
    385         // Don't highlight the target if we are flinging from AllApps
    386         if (isAllApps) {
    387             resetHoverColor();
    388         }
    389 
    390         if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
    391             // Defer animating out the drop target if we are animating to it
    392             mSearchDropTargetBar.deferOnDragEnd();
    393             mSearchDropTargetBar.finishAnimations();
    394         }
    395 
    396         final ViewConfiguration config = ViewConfiguration.get(mLauncher);
    397         final DragLayer dragLayer = mLauncher.getDragLayer();
    398         final int duration = FLING_DELETE_ANIMATION_DURATION;
    399         final long startTime = AnimationUtils.currentAnimationTimeMillis();
    400 
    401         // NOTE: Because it takes time for the first frame of animation to actually be
    402         // called and we expect the animation to be a continuation of the fling, we have
    403         // to account for the time that has elapsed since the fling finished.  And since
    404         // we don't have a startDelay, we will always get call to update when we call
    405         // start() (which we want to ignore).
    406         final TimeInterpolator tInterpolator = new TimeInterpolator() {
    407             private int mCount = -1;
    408             private float mOffset = 0f;
    409 
    410             @Override
    411             public float getInterpolation(float t) {
    412                 if (mCount < 0) {
    413                     mCount++;
    414                 } else if (mCount == 0) {
    415                     mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() -
    416                             startTime) / duration);
    417                     mCount++;
    418                 }
    419                 return Math.min(1f, mOffset + t);
    420             }
    421         };
    422         AnimatorUpdateListener updateCb = null;
    423         if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
    424             updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config);
    425         } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) {
    426             updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime,
    427                     duration, config);
    428         }
    429         Runnable onAnimationEndRunnable = new Runnable() {
    430             @Override
    431             public void run() {
    432                 // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up
    433                 // itself, otherwise, complete the drop to initiate the deletion process
    434                 if (!isAllApps) {
    435                     mLauncher.exitSpringLoadedDragMode();
    436                     completeDrop(d);
    437                 }
    438                 mLauncher.getDragController().onDeferredEndFling(d);
    439             }
    440         };
    441         dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable,
    442                 DragLayer.ANIMATION_END_DISAPPEAR, null);
    443     }
    444 }
    445