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 com.android.launcher3.Utilities.getBadge; 20 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 21 22 import android.animation.FloatArrayEvaluator; 23 import android.animation.ValueAnimator; 24 import android.animation.ValueAnimator.AnimatorUpdateListener; 25 import android.annotation.TargetApi; 26 import android.graphics.Bitmap; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.ColorMatrix; 30 import android.graphics.ColorMatrixColorFilter; 31 import android.graphics.Paint; 32 import android.graphics.Path; 33 import android.graphics.Point; 34 import android.graphics.Rect; 35 import android.graphics.drawable.AdaptiveIconDrawable; 36 import android.graphics.drawable.ColorDrawable; 37 import android.graphics.drawable.Drawable; 38 import android.os.Build; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.view.View; 42 43 import androidx.dynamicanimation.animation.FloatPropertyCompat; 44 import androidx.dynamicanimation.animation.SpringAnimation; 45 import androidx.dynamicanimation.animation.SpringForce; 46 47 import com.android.launcher3.FastBitmapDrawable; 48 import com.android.launcher3.FirstFrameAnimatorHelper; 49 import com.android.launcher3.Launcher; 50 import com.android.launcher3.LauncherSettings; 51 import com.android.launcher3.LauncherState; 52 import com.android.launcher3.R; 53 import com.android.launcher3.Utilities; 54 import com.android.launcher3.anim.Interpolators; 55 import com.android.launcher3.icons.LauncherIcons; 56 import com.android.launcher3.model.data.ItemInfo; 57 import com.android.launcher3.statemanager.StateManager.StateListener; 58 import com.android.launcher3.util.Themes; 59 import com.android.launcher3.util.Thunk; 60 61 import java.util.Arrays; 62 63 public class DragView extends View implements StateListener<LauncherState> { 64 private static final ColorMatrix sTempMatrix1 = new ColorMatrix(); 65 private static final ColorMatrix sTempMatrix2 = new ColorMatrix(); 66 67 public static final int COLOR_CHANGE_DURATION = 120; 68 public static final int VIEW_ZOOM_DURATION = 150; 69 70 private boolean mDrawBitmap = true; 71 private Bitmap mBitmap; 72 private Bitmap mCrossFadeBitmap; 73 @Thunk Paint mPaint; 74 private final int mBlurSizeOutline; 75 private final int mRegistrationX; 76 private final int mRegistrationY; 77 private final float mInitialScale; 78 private final float mScaleOnDrop; 79 private final int[] mTempLoc = new int[2]; 80 81 private Point mDragVisualizeOffset = null; 82 private Rect mDragRegion = null; 83 private final Launcher mLauncher; 84 private final DragLayer mDragLayer; 85 @Thunk final DragController mDragController; 86 final FirstFrameAnimatorHelper mFirstFrameAnimatorHelper; 87 private boolean mHasDrawn = false; 88 @Thunk float mCrossFadeProgress = 0f; 89 private boolean mAnimationCancelled = false; 90 91 ValueAnimator mAnim; 92 // The intrinsic icon scale factor is the scale factor for a drag icon over the workspace 93 // size. This is ignored for non-icons. 94 private float mIntrinsicIconScale = 1f; 95 96 @Thunk float[] mCurrentFilter; 97 private ValueAnimator mFilterAnimator; 98 99 private int mLastTouchX; 100 private int mLastTouchY; 101 private int mAnimatedShiftX; 102 private int mAnimatedShiftY; 103 104 // Below variable only needed IF FeatureFlags.LAUNCHER3_SPRING_ICONS is {@code true} 105 private Drawable mBgSpringDrawable, mFgSpringDrawable; 106 private SpringFloatValue mTranslateX, mTranslateY; 107 private Path mScaledMaskPath; 108 private Drawable mBadge; 109 private ColorMatrixColorFilter mBaseFilter; 110 111 /** 112 * Construct the drag view. 113 * <p> 114 * The registration point is the point inside our view that the touch events should 115 * be centered upon. 116 * @param launcher The Launcher instance 117 * @param bitmap The view that we're dragging around. We scale it up when we draw it. 118 * @param registrationX The x coordinate of the registration point. 119 * @param registrationY The y coordinate of the registration point. 120 */ DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)121 public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, 122 final float initialScale, final float scaleOnDrop, final float finalScaleDps) { 123 super(launcher); 124 mLauncher = launcher; 125 mDragLayer = launcher.getDragLayer(); 126 mDragController = launcher.getDragController(); 127 mFirstFrameAnimatorHelper = new FirstFrameAnimatorHelper(this); 128 129 final float scale = (bitmap.getWidth() + finalScaleDps) / bitmap.getWidth(); 130 131 // Set the initial scale to avoid any jumps 132 setScaleX(initialScale); 133 setScaleY(initialScale); 134 135 // Animate the view into the correct position 136 mAnim = ValueAnimator.ofFloat(0f, 1f); 137 mAnim.setDuration(VIEW_ZOOM_DURATION); 138 mAnim.addUpdateListener(animation -> { 139 final float value = (Float) animation.getAnimatedValue(); 140 setScaleX(initialScale + (value * (scale - initialScale))); 141 setScaleY(initialScale + (value * (scale - initialScale))); 142 if (!isAttachedToWindow()) { 143 animation.cancel(); 144 } 145 }); 146 147 mBitmap = bitmap; 148 setDragRegion(new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight())); 149 150 // The point in our scaled bitmap that the touch events are located 151 mRegistrationX = registrationX; 152 mRegistrationY = registrationY; 153 154 mInitialScale = initialScale; 155 mScaleOnDrop = scaleOnDrop; 156 157 // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass 158 int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 159 measure(ms, ms); 160 mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 161 162 mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline); 163 setElevation(getResources().getDimension(R.dimen.drag_elevation)); 164 } 165 166 @Override onAttachedToWindow()167 protected void onAttachedToWindow() { 168 super.onAttachedToWindow(); 169 mLauncher.getStateManager().addStateListener(this); 170 } 171 172 @Override onDetachedFromWindow()173 protected void onDetachedFromWindow() { 174 super.onDetachedFromWindow(); 175 mLauncher.getStateManager().removeStateListener(this); 176 } 177 178 @Override onStateTransitionComplete(LauncherState finalState)179 public void onStateTransitionComplete(LauncherState finalState) { 180 setVisibility((finalState == LauncherState.NORMAL 181 || finalState == LauncherState.SPRING_LOADED) ? VISIBLE : INVISIBLE); 182 } 183 184 /** 185 * Initialize {@code #mIconDrawable} if the item can be represented using 186 * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}. 187 */ 188 @TargetApi(Build.VERSION_CODES.O) setItemInfo(final ItemInfo info)189 public void setItemInfo(final ItemInfo info) { 190 if (!Utilities.ATLEAST_OREO) { 191 return; 192 } 193 if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && 194 info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT && 195 info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 196 return; 197 } 198 // Load the adaptive icon on a background thread and add the view in ui thread. 199 MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(new Runnable() { 200 @Override 201 public void run() { 202 Object[] outObj = new Object[1]; 203 int w = mBitmap.getWidth(); 204 int h = mBitmap.getHeight(); 205 Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj); 206 207 if (dr instanceof AdaptiveIconDrawable) { 208 int blurMargin = (int) mLauncher.getResources() 209 .getDimension(R.dimen.blur_size_medium_outline) / 2; 210 211 Rect bounds = new Rect(0, 0, w, h); 212 bounds.inset(blurMargin, blurMargin); 213 // Badge is applied after icon normalization so the bounds for badge should not 214 // be scaled down due to icon normalization. 215 Rect badgeBounds = new Rect(bounds); 216 mBadge = getBadge(mLauncher, info, outObj[0]); 217 mBadge.setBounds(badgeBounds); 218 219 // Do not draw the background in case of folder as its translucent 220 mDrawBitmap = !(dr instanceof FolderAdaptiveIcon); 221 222 try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) { 223 Drawable nDr; // drawable to be normalized 224 if (mDrawBitmap) { 225 nDr = dr; 226 } else { 227 // Since we just want the scale, avoid heavy drawing operations 228 nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null); 229 } 230 Utilities.scaleRectAboutCenter(bounds, 231 li.getNormalizer().getScale(nDr, null, null, null)); 232 } 233 AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr; 234 235 // Shrink very tiny bit so that the clip path is smaller than the original bitmap 236 // that has anti aliased edges and shadows. 237 Rect shrunkBounds = new Rect(bounds); 238 Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f); 239 adaptiveIcon.setBounds(shrunkBounds); 240 final Path mask = adaptiveIcon.getIconMask(); 241 242 mTranslateX = new SpringFloatValue(DragView.this, 243 w * AdaptiveIconDrawable.getExtraInsetFraction()); 244 mTranslateY = new SpringFloatValue(DragView.this, 245 h * AdaptiveIconDrawable.getExtraInsetFraction()); 246 247 bounds.inset( 248 (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()), 249 (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction()) 250 ); 251 mBgSpringDrawable = adaptiveIcon.getBackground(); 252 if (mBgSpringDrawable == null) { 253 mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT); 254 } 255 mBgSpringDrawable.setBounds(bounds); 256 mFgSpringDrawable = adaptiveIcon.getForeground(); 257 if (mFgSpringDrawable == null) { 258 mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT); 259 } 260 mFgSpringDrawable.setBounds(bounds); 261 262 new Handler(Looper.getMainLooper()).post(new Runnable() { 263 @Override 264 public void run() { 265 // Assign the variable on the UI thread to avoid race conditions. 266 mScaledMaskPath = mask; 267 268 if (info.isDisabled()) { 269 FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null); 270 d.setIsDisabled(true); 271 mBaseFilter = (ColorMatrixColorFilter) d.getColorFilter(); 272 } 273 updateColorFilter(); 274 } 275 }); 276 } 277 }}); 278 } 279 280 @TargetApi(Build.VERSION_CODES.O) updateColorFilter()281 private void updateColorFilter() { 282 if (mCurrentFilter == null) { 283 mPaint.setColorFilter(null); 284 285 if (mScaledMaskPath != null) { 286 mBgSpringDrawable.setColorFilter(mBaseFilter); 287 mFgSpringDrawable.setColorFilter(mBaseFilter); 288 mBadge.setColorFilter(mBaseFilter); 289 } 290 } else { 291 ColorMatrixColorFilter currentFilter = new ColorMatrixColorFilter(mCurrentFilter); 292 mPaint.setColorFilter(currentFilter); 293 294 if (mScaledMaskPath != null) { 295 if (mBaseFilter != null) { 296 mBaseFilter.getColorMatrix(sTempMatrix1); 297 sTempMatrix2.set(mCurrentFilter); 298 sTempMatrix1.postConcat(sTempMatrix2); 299 300 currentFilter = new ColorMatrixColorFilter(sTempMatrix1); 301 } 302 303 mBgSpringDrawable.setColorFilter(currentFilter); 304 mFgSpringDrawable.setColorFilter(currentFilter); 305 mBadge.setColorFilter(currentFilter); 306 } 307 } 308 309 invalidate(); 310 } 311 312 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)313 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 314 setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight()); 315 } 316 317 /** Sets the scale of the view over the normal workspace icon size. */ setIntrinsicIconScaleFactor(float scale)318 public void setIntrinsicIconScaleFactor(float scale) { 319 mIntrinsicIconScale = scale; 320 } 321 getIntrinsicIconScaleFactor()322 public float getIntrinsicIconScaleFactor() { 323 return mIntrinsicIconScale; 324 } 325 getDragRegionLeft()326 public int getDragRegionLeft() { 327 return mDragRegion.left; 328 } 329 getDragRegionTop()330 public int getDragRegionTop() { 331 return mDragRegion.top; 332 } 333 getDragRegionWidth()334 public int getDragRegionWidth() { 335 return mDragRegion.width(); 336 } 337 getDragRegionHeight()338 public int getDragRegionHeight() { 339 return mDragRegion.height(); 340 } 341 setDragVisualizeOffset(Point p)342 public void setDragVisualizeOffset(Point p) { 343 mDragVisualizeOffset = p; 344 } 345 getDragVisualizeOffset()346 public Point getDragVisualizeOffset() { 347 return mDragVisualizeOffset; 348 } 349 setDragRegion(Rect r)350 public void setDragRegion(Rect r) { 351 mDragRegion = r; 352 } 353 getDragRegion()354 public Rect getDragRegion() { 355 return mDragRegion; 356 } 357 getPreviewBitmap()358 public Bitmap getPreviewBitmap() { 359 return mBitmap; 360 } 361 362 @Override onDraw(Canvas canvas)363 protected void onDraw(Canvas canvas) { 364 mHasDrawn = true; 365 366 if (mDrawBitmap) { 367 // Always draw the bitmap to mask anti aliasing due to clipPath 368 boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null; 369 if (crossFade) { 370 int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255; 371 mPaint.setAlpha(alpha); 372 } 373 canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint); 374 if (crossFade) { 375 mPaint.setAlpha((int) (255 * mCrossFadeProgress)); 376 final int saveCount = canvas.save(); 377 float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth(); 378 float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight(); 379 canvas.scale(sX, sY); 380 canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint); 381 canvas.restoreToCount(saveCount); 382 } 383 } 384 385 if (mScaledMaskPath != null) { 386 int cnt = canvas.save(); 387 canvas.clipPath(mScaledMaskPath); 388 mBgSpringDrawable.draw(canvas); 389 canvas.translate(mTranslateX.mValue, mTranslateY.mValue); 390 mFgSpringDrawable.draw(canvas); 391 canvas.restoreToCount(cnt); 392 mBadge.draw(canvas); 393 } 394 } 395 setCrossFadeBitmap(Bitmap crossFadeBitmap)396 public void setCrossFadeBitmap(Bitmap crossFadeBitmap) { 397 mCrossFadeBitmap = crossFadeBitmap; 398 } 399 crossFade(int duration)400 public void crossFade(int duration) { 401 ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); 402 va.setDuration(duration); 403 va.setInterpolator(Interpolators.DEACCEL_1_5); 404 va.addUpdateListener(a -> { 405 mCrossFadeProgress = a.getAnimatedFraction(); 406 invalidate(); 407 }); 408 va.start(); 409 } 410 setColor(int color)411 public void setColor(int color) { 412 if (mPaint == null) { 413 mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 414 } 415 if (color != 0) { 416 ColorMatrix m1 = new ColorMatrix(); 417 m1.setSaturation(0); 418 419 ColorMatrix m2 = new ColorMatrix(); 420 Themes.setColorScaleOnMatrix(color, m2); 421 m1.postConcat(m2); 422 423 animateFilterTo(m1.getArray()); 424 } else { 425 if (mCurrentFilter == null) { 426 updateColorFilter(); 427 } else { 428 animateFilterTo(new ColorMatrix().getArray()); 429 } 430 } 431 } 432 animateFilterTo(float[] targetFilter)433 private void animateFilterTo(float[] targetFilter) { 434 float[] oldFilter = mCurrentFilter == null ? new ColorMatrix().getArray() : mCurrentFilter; 435 mCurrentFilter = Arrays.copyOf(oldFilter, oldFilter.length); 436 437 if (mFilterAnimator != null) { 438 mFilterAnimator.cancel(); 439 } 440 mFilterAnimator = ValueAnimator.ofObject(new FloatArrayEvaluator(mCurrentFilter), 441 oldFilter, targetFilter); 442 mFilterAnimator.setDuration(COLOR_CHANGE_DURATION); 443 mFilterAnimator.addUpdateListener(new AnimatorUpdateListener() { 444 445 @Override 446 public void onAnimationUpdate(ValueAnimator animation) { 447 updateColorFilter(); 448 } 449 }); 450 mFilterAnimator.start(); 451 } 452 hasDrawn()453 public boolean hasDrawn() { 454 return mHasDrawn; 455 } 456 457 @Override setAlpha(float alpha)458 public void setAlpha(float alpha) { 459 super.setAlpha(alpha); 460 mPaint.setAlpha((int) (255 * alpha)); 461 invalidate(); 462 } 463 464 /** 465 * Create a window containing this view and show it. 466 * 467 * @param touchX the x coordinate the user touched in DragLayer coordinates 468 * @param touchY the y coordinate the user touched in DragLayer coordinates 469 */ show(int touchX, int touchY)470 public void show(int touchX, int touchY) { 471 mDragLayer.addView(this); 472 473 // Start the pick-up animation 474 DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0); 475 lp.width = mBitmap.getWidth(); 476 lp.height = mBitmap.getHeight(); 477 lp.customPosition = true; 478 setLayoutParams(lp); 479 move(touchX, touchY); 480 // Post the animation to skip other expensive work happening on the first frame 481 post(new Runnable() { 482 public void run() { 483 mAnim.start(); 484 } 485 }); 486 } 487 cancelAnimation()488 public void cancelAnimation() { 489 mAnimationCancelled = true; 490 if (mAnim != null && mAnim.isRunning()) { 491 mAnim.cancel(); 492 } 493 } 494 495 /** 496 * Move the window containing this view. 497 * 498 * @param touchX the x coordinate the user touched in DragLayer coordinates 499 * @param touchY the y coordinate the user touched in DragLayer coordinates 500 */ move(int touchX, int touchY)501 public void move(int touchX, int touchY) { 502 if (touchX > 0 && touchY > 0 && mLastTouchX > 0 && mLastTouchY > 0 503 && mScaledMaskPath != null) { 504 mTranslateX.animateToPos(mLastTouchX - touchX); 505 mTranslateY.animateToPos(mLastTouchY - touchY); 506 } 507 mLastTouchX = touchX; 508 mLastTouchY = touchY; 509 applyTranslation(); 510 } 511 animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration)512 public void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration) { 513 mTempLoc[0] = toTouchX - mRegistrationX; 514 mTempLoc[1] = toTouchY - mRegistrationY; 515 mDragLayer.animateViewIntoPosition(this, mTempLoc, 1f, mScaleOnDrop, mScaleOnDrop, 516 DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration); 517 } 518 animateShift(final int shiftX, final int shiftY)519 public void animateShift(final int shiftX, final int shiftY) { 520 if (mAnim.isStarted()) { 521 return; 522 } 523 mAnimatedShiftX = shiftX; 524 mAnimatedShiftY = shiftY; 525 applyTranslation(); 526 mAnim.addUpdateListener(new AnimatorUpdateListener() { 527 @Override 528 public void onAnimationUpdate(ValueAnimator animation) { 529 float fraction = 1 - animation.getAnimatedFraction(); 530 mAnimatedShiftX = (int) (fraction * shiftX); 531 mAnimatedShiftY = (int) (fraction * shiftY); 532 applyTranslation(); 533 } 534 }); 535 } 536 applyTranslation()537 private void applyTranslation() { 538 setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX); 539 setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY); 540 } 541 remove()542 public void remove() { 543 if (getParent() != null) { 544 mDragLayer.removeView(DragView.this); 545 } 546 } 547 getBlurSizeOutline()548 public int getBlurSizeOutline() { 549 return mBlurSizeOutline; 550 } 551 getInitialScale()552 public float getInitialScale() { 553 return mInitialScale; 554 } 555 556 private static class SpringFloatValue { 557 558 private static final FloatPropertyCompat<SpringFloatValue> VALUE = 559 new FloatPropertyCompat<SpringFloatValue>("value") { 560 @Override 561 public float getValue(SpringFloatValue object) { 562 return object.mValue; 563 } 564 565 @Override 566 public void setValue(SpringFloatValue object, float value) { 567 object.mValue = value; 568 object.mView.invalidate(); 569 } 570 }; 571 572 // Following three values are fine tuned with motion ux designer 573 private final static int STIFFNESS = 4000; 574 private final static float DAMPENING_RATIO = 1f; 575 private final static int PARALLAX_MAX_IN_DP = 8; 576 577 private final View mView; 578 private final SpringAnimation mSpring; 579 private final float mDelta; 580 581 private float mValue; 582 SpringFloatValue(View view, float range)583 public SpringFloatValue(View view, float range) { 584 mView = view; 585 mSpring = new SpringAnimation(this, VALUE, 0) 586 .setMinValue(-range).setMaxValue(range) 587 .setSpring(new SpringForce(0) 588 .setDampingRatio(DAMPENING_RATIO) 589 .setStiffness(STIFFNESS)); 590 mDelta = view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP; 591 } 592 animateToPos(float value)593 public void animateToPos(float value) { 594 mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta)); 595 } 596 } 597 } 598