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