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