1 /* 2 * Copyright (C) 2008 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.launcher3.dragndrop; 18 19 import static android.view.View.MeasureSpec.EXACTLY; 20 import static android.view.View.MeasureSpec.makeMeasureSpec; 21 22 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; 23 import static com.android.launcher3.Utilities.getBadge; 24 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 25 26 import android.animation.Animator; 27 import android.animation.AnimatorListenerAdapter; 28 import android.animation.AnimatorSet; 29 import android.animation.ObjectAnimator; 30 import android.animation.ValueAnimator; 31 import android.animation.ValueAnimator.AnimatorUpdateListener; 32 import android.annotation.TargetApi; 33 import android.content.Context; 34 import android.graphics.Bitmap; 35 import android.graphics.Canvas; 36 import android.graphics.Color; 37 import android.graphics.Path; 38 import android.graphics.Picture; 39 import android.graphics.Point; 40 import android.graphics.Rect; 41 import android.graphics.drawable.AdaptiveIconDrawable; 42 import android.graphics.drawable.ColorDrawable; 43 import android.graphics.drawable.Drawable; 44 import android.graphics.drawable.PictureDrawable; 45 import android.os.Build; 46 import android.os.Handler; 47 import android.os.Looper; 48 import android.view.View; 49 import android.view.ViewGroup; 50 import android.widget.FrameLayout; 51 import android.widget.ImageView; 52 53 import androidx.annotation.Nullable; 54 import androidx.dynamicanimation.animation.FloatPropertyCompat; 55 import androidx.dynamicanimation.animation.SpringAnimation; 56 import androidx.dynamicanimation.animation.SpringForce; 57 58 import com.android.launcher3.LauncherSettings; 59 import com.android.launcher3.R; 60 import com.android.launcher3.Utilities; 61 import com.android.launcher3.anim.Interpolators; 62 import com.android.launcher3.icons.FastBitmapDrawable; 63 import com.android.launcher3.icons.LauncherIcons; 64 import com.android.launcher3.model.data.ItemInfo; 65 import com.android.launcher3.util.RunnableList; 66 import com.android.launcher3.views.ActivityContext; 67 import com.android.launcher3.views.BaseDragLayer; 68 69 /** A custom view for rendering an icon, folder, shortcut or widget during drag-n-drop. */ 70 public abstract class DragView<T extends Context & ActivityContext> extends FrameLayout { 71 72 public static final int VIEW_ZOOM_DURATION = 150; 73 74 private final View mContent; 75 // The following are only used for rendering mContent directly during drag-n-drop. 76 @Nullable private ViewGroup.LayoutParams mContentViewLayoutParams; 77 @Nullable private ViewGroup mContentViewParent; 78 private int mContentViewInParentViewIndex = -1; 79 private final int mWidth; 80 private final int mHeight; 81 82 private final int mBlurSizeOutline; 83 protected final int mRegistrationX; 84 protected final int mRegistrationY; 85 private final float mInitialScale; 86 protected final float mScaleOnDrop; 87 protected final int[] mTempLoc = new int[2]; 88 89 private final RunnableList mOnDragStartCallback = new RunnableList(); 90 91 private Point mDragVisualizeOffset = null; 92 private Rect mDragRegion = null; 93 protected final T mActivity; 94 private final BaseDragLayer<T> mDragLayer; 95 private boolean mHasDrawn = false; 96 97 final ValueAnimator mAnim; 98 // Whether mAnim has started. Unlike mAnim.isStarted(), this is true even after mAnim ends. 99 private boolean mAnimStarted; 100 101 private int mLastTouchX; 102 private int mLastTouchY; 103 private int mAnimatedShiftX; 104 private int mAnimatedShiftY; 105 106 // Below variable only needed IF FeatureFlags.LAUNCHER3_SPRING_ICONS is {@code true} 107 private Drawable mBgSpringDrawable, mFgSpringDrawable; 108 private SpringFloatValue mTranslateX, mTranslateY; 109 private Path mScaledMaskPath; 110 private Drawable mBadge; 111 DragView(T launcher, Drawable drawable, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)112 public DragView(T launcher, Drawable drawable, int registrationX, 113 int registrationY, final float initialScale, final float scaleOnDrop, 114 final float finalScaleDps) { 115 this(launcher, getViewFromDrawable(launcher, drawable), 116 drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), 117 registrationX, registrationY, initialScale, scaleOnDrop, finalScaleDps); 118 } 119 120 /** 121 * Construct the drag view. 122 * <p> 123 * The registration point is the point inside our view that the touch events should 124 * be centered upon. 125 * @param activity The Launcher instance/ActivityContext this DragView is in. 126 * @param content the view content that is attached to the drag view. 127 * @param width the width of the dragView 128 * @param height the height of the dragView 129 * @param initialScale The view that we're dragging around. We scale it up when we draw it. 130 * @param registrationX The x coordinate of the registration point. 131 * @param registrationY The y coordinate of the registration point. 132 * @param scaleOnDrop the scale used in the drop animation. 133 * @param finalScaleDps the scale used in the zoom out animation when the drag view is shown. 134 */ DragView(T activity, View content, int width, int height, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)135 public DragView(T activity, View content, int width, int height, int registrationX, 136 int registrationY, final float initialScale, final float scaleOnDrop, 137 final float finalScaleDps) { 138 super(activity); 139 mActivity = activity; 140 mDragLayer = activity.getDragLayer(); 141 142 mContent = content; 143 mWidth = width; 144 mHeight = height; 145 mContentViewLayoutParams = mContent.getLayoutParams(); 146 if (mContent.getParent() instanceof ViewGroup) { 147 mContentViewParent = (ViewGroup) mContent.getParent(); 148 mContentViewInParentViewIndex = mContentViewParent.indexOfChild(mContent); 149 mContentViewParent.removeView(mContent); 150 } 151 152 addView(content, new LayoutParams(width, height)); 153 154 // If there is already a scale set on the content, we don't want to clip the children. 155 if (content.getScaleX() != 1 || content.getScaleY() != 1) { 156 setClipChildren(false); 157 setClipToPadding(false); 158 } 159 160 final float scale = (width + finalScaleDps) / width; 161 162 // Set the initial scale to avoid any jumps 163 setScaleX(initialScale); 164 setScaleY(initialScale); 165 166 // Animate the view into the correct position 167 mAnim = ValueAnimator.ofFloat(0f, 1f); 168 mAnim.setDuration(VIEW_ZOOM_DURATION); 169 mAnim.addUpdateListener(animation -> { 170 final float value = (Float) animation.getAnimatedValue(); 171 setScaleX(initialScale + (value * (scale - initialScale))); 172 setScaleY(initialScale + (value * (scale - initialScale))); 173 if (!isAttachedToWindow()) { 174 animation.cancel(); 175 } 176 }); 177 mAnim.addListener(new AnimatorListenerAdapter() { 178 @Override 179 public void onAnimationStart(Animator animation) { 180 mAnimStarted = true; 181 } 182 }); 183 184 setDragRegion(new Rect(0, 0, width, height)); 185 186 // The point in our scaled bitmap that the touch events are located 187 mRegistrationX = registrationX; 188 mRegistrationY = registrationY; 189 190 mInitialScale = initialScale; 191 mScaleOnDrop = scaleOnDrop; 192 193 // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass 194 measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY)); 195 196 mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline); 197 setElevation(getResources().getDimension(R.dimen.drag_elevation)); 198 setWillNotDraw(false); 199 } 200 201 /** 202 * Initialize {@code #mIconDrawable} if the item can be represented using 203 * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}. 204 */ 205 @TargetApi(Build.VERSION_CODES.O) setItemInfo(final ItemInfo info)206 public void setItemInfo(final ItemInfo info) { 207 if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION 208 && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION 209 && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT 210 && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 211 return; 212 } 213 // Load the adaptive icon on a background thread and add the view in ui thread. 214 MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> { 215 Object[] outObj = new Object[1]; 216 int w = mWidth; 217 int h = mHeight; 218 Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h, outObj); 219 220 if (dr instanceof AdaptiveIconDrawable) { 221 int blurMargin = (int) mActivity.getResources() 222 .getDimension(R.dimen.blur_size_medium_outline) / 2; 223 224 Rect bounds = new Rect(0, 0, w, h); 225 bounds.inset(blurMargin, blurMargin); 226 // Badge is applied after icon normalization so the bounds for badge should not 227 // be scaled down due to icon normalization. 228 Rect badgeBounds = new Rect(bounds); 229 mBadge = getBadge(mActivity, info, outObj[0]); 230 mBadge.setBounds(badgeBounds); 231 232 // Do not draw the background in case of folder as its translucent 233 final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon); 234 235 try (LauncherIcons li = LauncherIcons.obtain(mActivity)) { 236 Drawable nDr; // drawable to be normalized 237 if (shouldDrawBackground) { 238 nDr = dr; 239 } else { 240 // Since we just want the scale, avoid heavy drawing operations 241 nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null); 242 } 243 Utilities.scaleRectAboutCenter(bounds, 244 li.getNormalizer().getScale(nDr, null, null, null)); 245 } 246 AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr; 247 248 // Shrink very tiny bit so that the clip path is smaller than the original bitmap 249 // that has anti aliased edges and shadows. 250 Rect shrunkBounds = new Rect(bounds); 251 Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f); 252 adaptiveIcon.setBounds(shrunkBounds); 253 final Path mask = adaptiveIcon.getIconMask(); 254 255 mTranslateX = new SpringFloatValue(DragView.this, 256 w * AdaptiveIconDrawable.getExtraInsetFraction()); 257 mTranslateY = new SpringFloatValue(DragView.this, 258 h * AdaptiveIconDrawable.getExtraInsetFraction()); 259 260 bounds.inset( 261 (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()), 262 (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction()) 263 ); 264 mBgSpringDrawable = adaptiveIcon.getBackground(); 265 if (mBgSpringDrawable == null) { 266 mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT); 267 } 268 mBgSpringDrawable.setBounds(bounds); 269 mFgSpringDrawable = adaptiveIcon.getForeground(); 270 if (mFgSpringDrawable == null) { 271 mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT); 272 } 273 mFgSpringDrawable.setBounds(bounds); 274 275 new Handler(Looper.getMainLooper()).post(() -> mOnDragStartCallback.add(() -> { 276 // TODO: Consider fade-in animation 277 // Assign the variable on the UI thread to avoid race conditions. 278 mScaledMaskPath = mask; 279 // Avoid relayout as we do not care about children affecting layout 280 removeAllViewsInLayout(); 281 282 if (info.isDisabled()) { 283 FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null); 284 d.setIsDisabled(true); 285 mBgSpringDrawable.setColorFilter(d.getColorFilter()); 286 mFgSpringDrawable.setColorFilter(d.getColorFilter()); 287 mBadge.setColorFilter(d.getColorFilter()); 288 } 289 invalidate(); 290 })); 291 } 292 }); 293 } 294 295 /** 296 * Called when pre-drag finishes for an icon 297 */ onDragStart()298 public void onDragStart() { 299 mOnDragStartCallback.executeAllAndDestroy(); 300 } 301 302 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)303 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 304 super.onMeasure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY)); 305 } 306 getDragRegionWidth()307 public int getDragRegionWidth() { 308 return mDragRegion.width(); 309 } 310 getDragRegionHeight()311 public int getDragRegionHeight() { 312 return mDragRegion.height(); 313 } 314 setDragVisualizeOffset(Point p)315 public void setDragVisualizeOffset(Point p) { 316 mDragVisualizeOffset = p; 317 } 318 getDragVisualizeOffset()319 public Point getDragVisualizeOffset() { 320 return mDragVisualizeOffset; 321 } 322 setDragRegion(Rect r)323 public void setDragRegion(Rect r) { 324 mDragRegion = r; 325 } 326 getDragRegion()327 public Rect getDragRegion() { 328 return mDragRegion; 329 } 330 331 @Override draw(Canvas canvas)332 public void draw(Canvas canvas) { 333 super.draw(canvas); 334 335 // Draw after the content 336 mHasDrawn = true; 337 if (mScaledMaskPath != null) { 338 int cnt = canvas.save(); 339 canvas.clipPath(mScaledMaskPath); 340 mBgSpringDrawable.draw(canvas); 341 canvas.translate(mTranslateX.mValue, mTranslateY.mValue); 342 mFgSpringDrawable.draw(canvas); 343 canvas.restoreToCount(cnt); 344 mBadge.draw(canvas); 345 } 346 } 347 crossFadeContent(Drawable crossFadeDrawable, int duration)348 public void crossFadeContent(Drawable crossFadeDrawable, int duration) { 349 if (mContent.getParent() == null) { 350 // If the content is already removed, ignore 351 return; 352 } 353 View newContent = getViewFromDrawable(getContext(), crossFadeDrawable); 354 newContent.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY)); 355 newContent.layout(0, 0, mWidth, mHeight); 356 addViewInLayout(newContent, 0, new LayoutParams(mWidth, mHeight)); 357 358 AnimatorSet anim = new AnimatorSet(); 359 anim.play(ObjectAnimator.ofFloat(newContent, VIEW_ALPHA, 0, 1)); 360 anim.play(ObjectAnimator.ofFloat(mContent, VIEW_ALPHA, 0)); 361 anim.setDuration(duration).setInterpolator(Interpolators.DEACCEL_1_5); 362 anim.start(); 363 } 364 hasDrawn()365 public boolean hasDrawn() { 366 return mHasDrawn; 367 } 368 369 /** 370 * Create a window containing this view and show it. 371 * 372 * @param touchX the x coordinate the user touched in DragLayer coordinates 373 * @param touchY the y coordinate the user touched in DragLayer coordinates 374 */ show(int touchX, int touchY)375 public void show(int touchX, int touchY) { 376 mDragLayer.addView(this); 377 378 // Start the pick-up animation 379 BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(mWidth, mHeight); 380 lp.customPosition = true; 381 setLayoutParams(lp); 382 383 if (mContent != null) { 384 // At the drag start, the source view visibility is set to invisible. 385 mContent.setVisibility(VISIBLE); 386 } 387 388 move(touchX, touchY); 389 // Post the animation to skip other expensive work happening on the first frame 390 post(mAnim::start); 391 } 392 cancelAnimation()393 public void cancelAnimation() { 394 if (mAnim != null && mAnim.isRunning()) { 395 mAnim.cancel(); 396 } 397 } 398 isAnimationFinished()399 public boolean isAnimationFinished() { 400 return mAnimStarted && !mAnim.isRunning(); 401 } 402 403 /** 404 * Move the window containing this view. 405 * 406 * @param touchX the x coordinate the user touched in DragLayer coordinates 407 * @param touchY the y coordinate the user touched in DragLayer coordinates 408 */ move(int touchX, int touchY)409 public void move(int touchX, int touchY) { 410 if (touchX > 0 && touchY > 0 && mLastTouchX > 0 && mLastTouchY > 0 411 && mScaledMaskPath != null) { 412 mTranslateX.animateToPos(mLastTouchX - touchX); 413 mTranslateY.animateToPos(mLastTouchY - touchY); 414 } 415 mLastTouchX = touchX; 416 mLastTouchY = touchY; 417 applyTranslation(); 418 } 419 420 /** 421 * Animate this DragView to the given DragLayer coordinates and then remove it. 422 */ animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration)423 public abstract void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, 424 int duration); 425 animateShift(final int shiftX, final int shiftY)426 public void animateShift(final int shiftX, final int shiftY) { 427 if (mAnim.isStarted()) { 428 return; 429 } 430 mAnimatedShiftX = shiftX; 431 mAnimatedShiftY = shiftY; 432 applyTranslation(); 433 mAnim.addUpdateListener(new AnimatorUpdateListener() { 434 @Override 435 public void onAnimationUpdate(ValueAnimator animation) { 436 float fraction = 1 - animation.getAnimatedFraction(); 437 mAnimatedShiftX = (int) (fraction * shiftX); 438 mAnimatedShiftY = (int) (fraction * shiftY); 439 applyTranslation(); 440 } 441 }); 442 } 443 applyTranslation()444 private void applyTranslation() { 445 setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX); 446 setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY); 447 } 448 449 /** 450 * Detaches {@link #mContent}, if previously attached, from this view. 451 * 452 * <p>In the case of no change in the drop position, sets {@code reattachToPreviousParent} to 453 * {@code true} to attach the {@link #mContent} back to its previous parent. 454 */ detachContentView(boolean reattachToPreviousParent)455 public void detachContentView(boolean reattachToPreviousParent) { 456 if (mContent != null && mContentViewParent != null && mContentViewInParentViewIndex >= 0) { 457 Picture picture = new Picture(); 458 mContent.draw(picture.beginRecording(mWidth, mHeight)); 459 picture.endRecording(); 460 View view = new View(mActivity); 461 view.setBackground(new PictureDrawable(picture)); 462 view.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY)); 463 view.layout(mContent.getLeft(), mContent.getTop(), 464 mContent.getRight(), mContent.getBottom()); 465 setClipToOutline(mContent.getClipToOutline()); 466 setOutlineProvider(mContent.getOutlineProvider()); 467 addViewInLayout(view, indexOfChild(mContent), mContent.getLayoutParams(), true); 468 469 removeViewInLayout(mContent); 470 mContent.setVisibility(INVISIBLE); 471 mContent.setLayoutParams(mContentViewLayoutParams); 472 if (reattachToPreviousParent) { 473 mContentViewParent.addView(mContent, mContentViewInParentViewIndex); 474 } 475 mContentViewParent = null; 476 mContentViewInParentViewIndex = -1; 477 } 478 } 479 480 /** 481 * Removes this view from the {@link DragLayer}. 482 * 483 * <p>If the drag content is a {@link #mContent}, this call doesn't reattach the 484 * {@link #mContent} back to its previous parent. To reattach to previous parent, the caller 485 * should call {@link #detachContentView} with {@code reattachToPreviousParent} sets to true 486 * before this call. 487 */ remove()488 public void remove() { 489 if (getParent() != null) { 490 mDragLayer.removeView(DragView.this); 491 } 492 } 493 getBlurSizeOutline()494 public int getBlurSizeOutline() { 495 return mBlurSizeOutline; 496 } 497 getInitialScale()498 public float getInitialScale() { 499 return mInitialScale; 500 } 501 502 @Override hasOverlappingRendering()503 public boolean hasOverlappingRendering() { 504 return false; 505 } 506 507 /** Returns the current content view that is rendered in the drag view. */ getContentView()508 public View getContentView() { 509 return mContent; 510 } 511 512 /** 513 * Returns the previous {@link ViewGroup} parent of the {@link #mContent} before the drag 514 * content is attached to this view. 515 */ 516 @Nullable getContentViewParent()517 public ViewGroup getContentViewParent() { 518 return mContentViewParent; 519 } 520 521 private static class SpringFloatValue { 522 523 private static final FloatPropertyCompat<SpringFloatValue> VALUE = 524 new FloatPropertyCompat<SpringFloatValue>("value") { 525 @Override 526 public float getValue(SpringFloatValue object) { 527 return object.mValue; 528 } 529 530 @Override 531 public void setValue(SpringFloatValue object, float value) { 532 object.mValue = value; 533 object.mView.invalidate(); 534 } 535 }; 536 537 // Following three values are fine tuned with motion ux designer 538 private static final int STIFFNESS = 4000; 539 private static final float DAMPENING_RATIO = 1f; 540 private static final int PARALLAX_MAX_IN_DP = 8; 541 542 private final View mView; 543 private final SpringAnimation mSpring; 544 private final float mDelta; 545 546 private float mValue; 547 SpringFloatValue(View view, float range)548 public SpringFloatValue(View view, float range) { 549 mView = view; 550 mSpring = new SpringAnimation(this, VALUE, 0) 551 .setMinValue(-range).setMaxValue(range) 552 .setSpring(new SpringForce(0) 553 .setDampingRatio(DAMPENING_RATIO) 554 .setStiffness(STIFFNESS)); 555 mDelta = view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP; 556 } 557 animateToPos(float value)558 public void animateToPos(float value) { 559 mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta)); 560 } 561 } 562 getViewFromDrawable(Context context, Drawable drawable)563 private static View getViewFromDrawable(Context context, Drawable drawable) { 564 ImageView iv = new ImageView(context); 565 iv.setImageDrawable(drawable); 566 return iv; 567 } 568 569 /** 570 * Removes any stray DragView from the DragLayer. 571 */ removeAllViews(ActivityContext activity)572 public static void removeAllViews(ActivityContext activity) { 573 BaseDragLayer dragLayer = activity.getDragLayer(); 574 // Iterate in reverse order. DragView is added later to the dragLayer, 575 // and will be one of the last views. 576 for (int i = dragLayer.getChildCount() - 1; i >= 0; i--) { 577 View child = dragLayer.getChildAt(i); 578 if (child instanceof DragView) { 579 dragLayer.removeView(child); 580 } 581 } 582 } 583 } 584