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