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