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