DeleteDropTarget.java revision b605102cfd360d9f5d0f003aaf7839b5ebd176df
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.pm.PackageManager; 25import android.content.pm.PackageManager.NameNotFoundException; 26import android.content.pm.ResolveInfo; 27import android.content.res.ColorStateList; 28import android.content.res.Configuration; 29import android.content.res.Resources; 30import android.graphics.PointF; 31import android.graphics.Rect; 32import android.graphics.drawable.TransitionDrawable; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.view.View; 36import android.view.ViewConfiguration; 37import android.view.ViewGroup; 38import android.view.animation.AnimationUtils; 39import android.view.animation.DecelerateInterpolator; 40import android.view.animation.LinearInterpolator; 41 42import java.util.List; 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 item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION)) { 152 return true; 153 } 154 155 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && 156 item instanceof ShortcutInfo) { 157 if (AppsCustomizePagedView.DISABLE_ALL_APPS) { 158 ShortcutInfo shortcutInfo = (ShortcutInfo) info; 159 return (shortcutInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0; 160 } else { 161 return true; 162 } 163 } 164 } 165 return false; 166 } 167 168 @Override 169 public void onDragStart(DragSource source, Object info, int dragAction) { 170 boolean isVisible = true; 171 boolean isUninstall = false; 172 173 // If we are dragging a widget from AppsCustomize, hide the delete target 174 if (isAllAppsWidget(source, info)) { 175 isVisible = false; 176 } 177 178 // If we are dragging an application from AppsCustomize, only show the control if we can 179 // delete the app (it was downloaded), and rename the string to "uninstall" in such a case 180 if (willAcceptDrop(info)) { 181 isVisible = true; 182 } else { 183 isVisible = false; 184 } 185 186 if (isUninstall) { 187 setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null); 188 } else { 189 setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null); 190 } 191 mCurrentDrawable = (TransitionDrawable) getCurrentDrawable(); 192 193 mActive = isVisible; 194 resetHoverColor(); 195 ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE); 196 if (getText().length() > 0) { 197 setText(isUninstall ? R.string.delete_target_uninstall_label 198 : R.string.delete_target_label); 199 } 200 } 201 202 @Override 203 public void onDragEnd() { 204 super.onDragEnd(); 205 mActive = false; 206 } 207 208 public void onDragEnter(DragObject d) { 209 super.onDragEnter(d); 210 211 setHoverColor(); 212 } 213 214 public void onDragExit(DragObject d) { 215 super.onDragExit(d); 216 217 if (!d.dragComplete) { 218 resetHoverColor(); 219 } else { 220 // Restore the hover color if we are deleting 221 d.dragView.setColor(mHoverColor); 222 } 223 } 224 225 private void animateToTrashAndCompleteDrop(final DragObject d) { 226 final DragLayer dragLayer = mLauncher.getDragLayer(); 227 final Rect from = new Rect(); 228 dragLayer.getViewRectRelativeToSelf(d.dragView, from); 229 final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), 230 mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); 231 final float scale = (float) to.width() / from.width(); 232 233 mSearchDropTargetBar.deferOnDragEnd(); 234 deferCompleteDropIfUninstalling(d); 235 236 Runnable onAnimationEndRunnable = new Runnable() { 237 @Override 238 public void run() { 239 completeDrop(d); 240 mSearchDropTargetBar.onDragEnd(); 241 mLauncher.exitSpringLoadedDragMode(); 242 } 243 }; 244 dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, 245 DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2), 246 new LinearInterpolator(), onAnimationEndRunnable, 247 DragLayer.ANIMATION_END_DISAPPEAR, null); 248 } 249 250 private void deferCompleteDropIfUninstalling(DragObject d) { 251 mWaitingForUninstall = false; 252 if (isUninstall(d)) { 253 if (d.dragSource instanceof Folder) { 254 ((Folder) d.dragSource).deferCompleteDropAfterUninstallActivity(); 255 } else if (d.dragSource instanceof Workspace) { 256 ((Workspace) d.dragSource).deferCompleteDropAfterUninstallActivity(); 257 } 258 mWaitingForUninstall = true; 259 } 260 } 261 262 private boolean isUninstall(DragObject d) { 263 return AppsCustomizePagedView.DISABLE_ALL_APPS && isWorkspaceOrFolderApplication(d); 264 } 265 266 private void completeDrop(DragObject d) { 267 ItemInfo item = (ItemInfo) d.dragInfo; 268 boolean wasWaitingForUninstall = mWaitingForUninstall; 269 mWaitingForUninstall = false; 270 if (isAllAppsApplication(d.dragSource, item)) { 271 // Uninstall the application if it is being dragged from AppsCustomize 272 AppInfo appInfo = (AppInfo) item; 273 mLauncher.startApplicationUninstallActivity(appInfo.componentName, appInfo.flags); 274 } else if (AppsCustomizePagedView.DISABLE_ALL_APPS && isWorkspaceOrFolderApplication(d)) { 275 ShortcutInfo shortcut = (ShortcutInfo) item; 276 if (shortcut.intent != null && shortcut.intent.getComponent() != null) { 277 final ComponentName componentName = shortcut.intent.getComponent(); 278 final DragSource dragSource = d.dragSource; 279 int flags = AppInfo.initFlags( 280 ShortcutInfo.getPackageInfo(getContext(), componentName.getPackageName())); 281 mWaitingForUninstall = 282 mLauncher.startApplicationUninstallActivity(componentName, flags); 283 if (mWaitingForUninstall) { 284 final Runnable checkIfUninstallWasSuccess = new Runnable() { 285 @Override 286 public void run() { 287 mWaitingForUninstall = false; 288 String packageName = componentName.getPackageName(); 289 List<ResolveInfo> activities = 290 AllAppsList.findActivitiesForPackage(getContext(), packageName); 291 boolean uninstallSuccessful = activities.size() == 0; 292 if (dragSource instanceof Folder) { 293 ((Folder) dragSource). 294 onUninstallActivityReturned(uninstallSuccessful); 295 } else if (dragSource instanceof Workspace) { 296 ((Workspace) dragSource). 297 onUninstallActivityReturned(uninstallSuccessful); 298 } 299 } 300 }; 301 mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess); 302 } 303 } 304 } else if (isWorkspaceOrFolderApplication(d)) { 305 LauncherModel.deleteItemFromDatabase(mLauncher, item); 306 } else if (isWorkspaceFolder(d)) { 307 // Remove the folder from the workspace and delete the contents from launcher model 308 FolderInfo folderInfo = (FolderInfo) item; 309 mLauncher.removeFolder(folderInfo); 310 LauncherModel.deleteFolderContentsFromDatabase(mLauncher, folderInfo); 311 } else if (isWorkspaceOrFolderWidget(d)) { 312 // Remove the widget from the workspace 313 mLauncher.removeAppWidget((LauncherAppWidgetInfo) item); 314 LauncherModel.deleteItemFromDatabase(mLauncher, item); 315 316 final LauncherAppWidgetInfo launcherAppWidgetInfo = (LauncherAppWidgetInfo) item; 317 final LauncherAppWidgetHost appWidgetHost = mLauncher.getAppWidgetHost(); 318 if (appWidgetHost != null) { 319 // Deleting an app widget ID is a void call but writes to disk before returning 320 // to the caller... 321 new Thread("deleteAppWidgetId") { 322 public void run() { 323 appWidgetHost.deleteAppWidgetId(launcherAppWidgetInfo.appWidgetId); 324 } 325 }.start(); 326 } 327 } 328 if (wasWaitingForUninstall && !mWaitingForUninstall) { 329 if (d.dragSource instanceof Folder) { 330 ((Folder) d.dragSource).onUninstallActivityReturned(false); 331 } else if (d.dragSource instanceof Workspace) { 332 ((Workspace) d.dragSource).onUninstallActivityReturned(false); 333 } 334 } 335 } 336 337 public void onDrop(DragObject d) { 338 animateToTrashAndCompleteDrop(d); 339 } 340 341 /** 342 * Creates an animation from the current drag view to the delete trash icon. 343 */ 344 private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer, 345 DragObject d, PointF vel, ViewConfiguration config) { 346 final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(), 347 mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight()); 348 final Rect from = new Rect(); 349 dragLayer.getViewRectRelativeToSelf(d.dragView, from); 350 351 // Calculate how far along the velocity vector we should put the intermediate point on 352 // the bezier curve 353 float velocity = Math.abs(vel.length()); 354 float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f)); 355 int offsetY = (int) (-from.top * vp); 356 int offsetX = (int) (offsetY / (vel.y / vel.x)); 357 final float y2 = from.top + offsetY; // intermediate t/l 358 final float x2 = from.left + offsetX; 359 final float x1 = from.left; // drag view t/l 360 final float y1 = from.top; 361 final float x3 = to.left; // delete target t/l 362 final float y3 = to.top; 363 364 final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() { 365 @Override 366 public float getInterpolation(float t) { 367 return t * t * t * t * t * t * t * t; 368 } 369 }; 370 return new AnimatorUpdateListener() { 371 @Override 372 public void onAnimationUpdate(ValueAnimator animation) { 373 final DragView dragView = (DragView) dragLayer.getAnimatedView(); 374 float t = ((Float) animation.getAnimatedValue()).floatValue(); 375 float tp = scaleAlphaInterpolator.getInterpolation(t); 376 float initialScale = dragView.getInitialScale(); 377 float finalAlpha = 0.5f; 378 float scale = dragView.getScaleX(); 379 float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f; 380 float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f; 381 float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) + 382 (t * t) * x3; 383 float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) + 384 (t * t) * y3; 385 386 dragView.setTranslationX(x); 387 dragView.setTranslationY(y); 388 dragView.setScaleX(initialScale * (1f - tp)); 389 dragView.setScaleY(initialScale * (1f - tp)); 390 dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp)); 391 } 392 }; 393 } 394 395 /** 396 * Creates an animation from the current drag view along its current velocity vector. 397 * For this animation, the alpha runs for a fixed duration and we update the position 398 * progressively. 399 */ 400 private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { 401 private DragLayer mDragLayer; 402 private PointF mVelocity; 403 private Rect mFrom; 404 private long mPrevTime; 405 private boolean mHasOffsetForScale; 406 private float mFriction; 407 408 private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); 409 410 public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from, 411 long startTime, float friction) { 412 mDragLayer = dragLayer; 413 mVelocity = vel; 414 mFrom = from; 415 mPrevTime = startTime; 416 mFriction = 1f - (dragLayer.getResources().getDisplayMetrics().density * friction); 417 } 418 419 @Override 420 public void onAnimationUpdate(ValueAnimator animation) { 421 final DragView dragView = (DragView) mDragLayer.getAnimatedView(); 422 float t = ((Float) animation.getAnimatedValue()).floatValue(); 423 long curTime = AnimationUtils.currentAnimationTimeMillis(); 424 425 if (!mHasOffsetForScale) { 426 mHasOffsetForScale = true; 427 float scale = dragView.getScaleX(); 428 float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f; 429 float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f; 430 431 mFrom.left += xOffset; 432 mFrom.top += yOffset; 433 } 434 435 mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); 436 mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); 437 438 dragView.setTranslationX(mFrom.left); 439 dragView.setTranslationY(mFrom.top); 440 dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); 441 442 mVelocity.x *= mFriction; 443 mVelocity.y *= mFriction; 444 mPrevTime = curTime; 445 } 446 }; 447 private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer, 448 DragObject d, PointF vel, final long startTime, final int duration, 449 ViewConfiguration config) { 450 final Rect from = new Rect(); 451 dragLayer.getViewRectRelativeToSelf(d.dragView, from); 452 453 return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime, 454 FLING_TO_DELETE_FRICTION); 455 } 456 457 public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) { 458 final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView; 459 460 // Don't highlight the icon as it's animating 461 d.dragView.setColor(0); 462 d.dragView.updateInitialScaleToCurrentScale(); 463 // Don't highlight the target if we are flinging from AllApps 464 if (isAllApps) { 465 resetHoverColor(); 466 } 467 468 if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { 469 // Defer animating out the drop target if we are animating to it 470 mSearchDropTargetBar.deferOnDragEnd(); 471 mSearchDropTargetBar.finishAnimations(); 472 } 473 474 final ViewConfiguration config = ViewConfiguration.get(mLauncher); 475 final DragLayer dragLayer = mLauncher.getDragLayer(); 476 final int duration = FLING_DELETE_ANIMATION_DURATION; 477 final long startTime = AnimationUtils.currentAnimationTimeMillis(); 478 479 // NOTE: Because it takes time for the first frame of animation to actually be 480 // called and we expect the animation to be a continuation of the fling, we have 481 // to account for the time that has elapsed since the fling finished. And since 482 // we don't have a startDelay, we will always get call to update when we call 483 // start() (which we want to ignore). 484 final TimeInterpolator tInterpolator = new TimeInterpolator() { 485 private int mCount = -1; 486 private float mOffset = 0f; 487 488 @Override 489 public float getInterpolation(float t) { 490 if (mCount < 0) { 491 mCount++; 492 } else if (mCount == 0) { 493 mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - 494 startTime) / duration); 495 mCount++; 496 } 497 return Math.min(1f, mOffset + t); 498 } 499 }; 500 AnimatorUpdateListener updateCb = null; 501 if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) { 502 updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config); 503 } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) { 504 updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime, 505 duration, config); 506 } 507 deferCompleteDropIfUninstalling(d); 508 509 Runnable onAnimationEndRunnable = new Runnable() { 510 @Override 511 public void run() { 512 // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up 513 // itself, otherwise, complete the drop to initiate the deletion process 514 if (!isAllApps) { 515 mLauncher.exitSpringLoadedDragMode(); 516 completeDrop(d); 517 } 518 mLauncher.getDragController().onDeferredEndFling(d); 519 } 520 }; 521 dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable, 522 DragLayer.ANIMATION_END_DISAPPEAR, null); 523 } 524} 525