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