1 /* 2 * Copyright (C) 2019 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 package com.android.launcher3.views; 17 18 import static com.android.launcher3.LauncherAnimUtils.DRAWABLE_ALPHA; 19 import static com.android.launcher3.Utilities.getBadge; 20 import static com.android.launcher3.Utilities.getFullDrawable; 21 import static com.android.launcher3.Utilities.mapToRange; 22 import static com.android.launcher3.anim.Interpolators.LINEAR; 23 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM; 24 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK; 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.annotation.TargetApi; 33 import android.content.Context; 34 import android.graphics.Canvas; 35 import android.graphics.Color; 36 import android.graphics.Outline; 37 import android.graphics.Path; 38 import android.graphics.Rect; 39 import android.graphics.RectF; 40 import android.graphics.drawable.AdaptiveIconDrawable; 41 import android.graphics.drawable.ColorDrawable; 42 import android.graphics.drawable.Drawable; 43 import android.os.Build; 44 import android.os.CancellationSignal; 45 import android.util.AttributeSet; 46 import android.util.Log; 47 import android.view.View; 48 import android.view.ViewGroup; 49 import android.view.ViewOutlineProvider; 50 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 51 import android.widget.ImageView; 52 53 import androidx.annotation.Nullable; 54 import androidx.annotation.UiThread; 55 import androidx.annotation.WorkerThread; 56 import androidx.dynamicanimation.animation.FloatPropertyCompat; 57 import androidx.dynamicanimation.animation.SpringAnimation; 58 import androidx.dynamicanimation.animation.SpringForce; 59 60 import com.android.launcher3.BubbleTextView; 61 import com.android.launcher3.InsettableFrameLayout.LayoutParams; 62 import com.android.launcher3.ItemInfo; 63 import com.android.launcher3.Launcher; 64 import com.android.launcher3.R; 65 import com.android.launcher3.Utilities; 66 import com.android.launcher3.dragndrop.DragLayer; 67 import com.android.launcher3.dragndrop.FolderAdaptiveIcon; 68 import com.android.launcher3.folder.FolderIcon; 69 import com.android.launcher3.graphics.IconShape; 70 import com.android.launcher3.graphics.ShiftedBitmapDrawable; 71 import com.android.launcher3.icons.LauncherIcons; 72 import com.android.launcher3.popup.SystemShortcut; 73 import com.android.launcher3.shortcuts.DeepShortcutView; 74 75 /** 76 * A view that is created to look like another view with the purpose of creating fluid animations. 77 */ 78 @TargetApi(Build.VERSION_CODES.Q) 79 public class FloatingIconView extends View implements 80 Animator.AnimatorListener, ClipPathView, OnGlobalLayoutListener { 81 82 private static final String TAG = FloatingIconView.class.getSimpleName(); 83 84 // Manages loading the icon on a worker thread 85 private static @Nullable IconLoadResult sIconLoadResult; 86 87 public static final float SHAPE_PROGRESS_DURATION = 0.10f; 88 private static final int FADE_DURATION_MS = 200; 89 private static final Rect sTmpRect = new Rect(); 90 private static final RectF sTmpRectF = new RectF(); 91 private static final Object[] sTmpObjArray = new Object[1]; 92 93 // We spring the foreground drawable relative to the icon's movement in the DragLayer. 94 // We then use these two factor values to scale the movement of the fg within this view. 95 private static final int FG_TRANS_X_FACTOR = 60; 96 private static final int FG_TRANS_Y_FACTOR = 75; 97 98 private static final FloatPropertyCompat<FloatingIconView> mFgTransYProperty 99 = new FloatPropertyCompat<FloatingIconView>("FloatingViewFgTransY") { 100 @Override 101 public float getValue(FloatingIconView view) { 102 return view.mFgTransY; 103 } 104 105 @Override 106 public void setValue(FloatingIconView view, float transY) { 107 view.mFgTransY = transY; 108 view.invalidate(); 109 } 110 }; 111 112 private static final FloatPropertyCompat<FloatingIconView> mFgTransXProperty 113 = new FloatPropertyCompat<FloatingIconView>("FloatingViewFgTransX") { 114 @Override 115 public float getValue(FloatingIconView view) { 116 return view.mFgTransX; 117 } 118 119 @Override 120 public void setValue(FloatingIconView view, float transX) { 121 view.mFgTransX = transX; 122 view.invalidate(); 123 } 124 }; 125 126 private Runnable mEndRunnable; 127 private CancellationSignal mLoadIconSignal; 128 129 private final Launcher mLauncher; 130 private final int mBlurSizeOutline; 131 private final boolean mIsRtl; 132 133 private boolean mIsVerticalBarLayout = false; 134 private boolean mIsAdaptiveIcon = false; 135 private boolean mIsOpening; 136 137 private IconLoadResult mIconLoadResult; 138 139 private @Nullable Drawable mBadge; 140 private @Nullable Drawable mForeground; 141 private @Nullable Drawable mBackground; 142 private float mRotation; 143 private ValueAnimator mRevealAnimator; 144 private final Rect mStartRevealRect = new Rect(); 145 private final Rect mEndRevealRect = new Rect(); 146 private Path mClipPath; 147 private float mTaskCornerRadius; 148 149 private View mOriginalIcon; 150 private RectF mPositionOut; 151 private Runnable mOnTargetChangeRunnable; 152 153 private final Rect mOutline = new Rect(); 154 private final Rect mFinalDrawableBounds = new Rect(); 155 156 private AnimatorSet mFadeAnimatorSet; 157 private ListenerView mListenerView; 158 159 private final SpringAnimation mFgSpringY; 160 private float mFgTransY; 161 private final SpringAnimation mFgSpringX; 162 private float mFgTransX; 163 FloatingIconView(Context context)164 public FloatingIconView(Context context) { 165 this(context, null); 166 } 167 FloatingIconView(Context context, AttributeSet attrs)168 public FloatingIconView(Context context, AttributeSet attrs) { 169 this(context, attrs, 0); 170 } 171 FloatingIconView(Context context, AttributeSet attrs, int defStyleAttr)172 public FloatingIconView(Context context, AttributeSet attrs, int defStyleAttr) { 173 super(context, attrs, defStyleAttr); 174 mLauncher = Launcher.getLauncher(context); 175 mBlurSizeOutline = getResources().getDimensionPixelSize( 176 R.dimen.blur_size_medium_outline); 177 mIsRtl = Utilities.isRtl(getResources()); 178 mListenerView = new ListenerView(context, attrs); 179 180 mFgSpringX = new SpringAnimation(this, mFgTransXProperty) 181 .setSpring(new SpringForce() 182 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) 183 .setStiffness(SpringForce.STIFFNESS_LOW)); 184 mFgSpringY = new SpringAnimation(this, mFgTransYProperty) 185 .setSpring(new SpringForce() 186 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) 187 .setStiffness(SpringForce.STIFFNESS_LOW)); 188 } 189 190 @Override onAttachedToWindow()191 protected void onAttachedToWindow() { 192 super.onAttachedToWindow(); 193 if (!mIsOpening) { 194 getViewTreeObserver().addOnGlobalLayoutListener(this); 195 mLauncher.getRotationHelper().setCurrentTransitionRequest(REQUEST_LOCK); 196 } 197 } 198 199 @Override onDetachedFromWindow()200 protected void onDetachedFromWindow() { 201 getViewTreeObserver().removeOnGlobalLayoutListener(this); 202 super.onDetachedFromWindow(); 203 } 204 205 /** 206 * Positions this view to match the size and location of {@param rect}. 207 * @param alpha The alpha to set this view. 208 * @param progress A value from [0, 1] that represents the animation progress. 209 * @param shapeProgressStart The progress value at which to start the shape reveal. 210 * @param cornerRadius The corner radius of {@param rect}. 211 */ update(RectF rect, float alpha, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening)212 public void update(RectF rect, float alpha, float progress, float shapeProgressStart, 213 float cornerRadius, boolean isOpening) { 214 setAlpha(alpha); 215 216 LayoutParams lp = (LayoutParams) getLayoutParams(); 217 float dX = mIsRtl 218 ? rect.left 219 - (mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width) 220 : rect.left - lp.getMarginStart(); 221 float dY = rect.top - lp.topMargin; 222 setTranslationX(dX); 223 setTranslationY(dY); 224 225 float minSize = Math.min(lp.width, lp.height); 226 float scaleX = rect.width() / minSize; 227 float scaleY = rect.height() / minSize; 228 float scale = Math.max(1f, Math.min(scaleX, scaleY)); 229 230 setPivotX(0); 231 setPivotY(0); 232 setScaleX(scale); 233 setScaleY(scale); 234 235 // shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION 236 float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f; 237 float shapeRevealProgress = Utilities.boundToRange(mapToRange( 238 Math.max(shapeProgressStart, progress), shapeProgressStart, 1f, 0, toMax, 239 LINEAR), 0, 1); 240 241 if (mIsVerticalBarLayout) { 242 mOutline.right = (int) (rect.width() / scale); 243 } else { 244 mOutline.bottom = (int) (rect.height() / scale); 245 } 246 247 mTaskCornerRadius = cornerRadius / scale; 248 if (mIsAdaptiveIcon) { 249 if (!isOpening && progress >= shapeProgressStart) { 250 if (mRevealAnimator == null) { 251 mRevealAnimator = (ValueAnimator) IconShape.getShape().createRevealAnimator( 252 this, mStartRevealRect, mOutline, mTaskCornerRadius, !isOpening); 253 mRevealAnimator.addListener(new AnimatorListenerAdapter() { 254 @Override 255 public void onAnimationEnd(Animator animation) { 256 mRevealAnimator = null; 257 } 258 }); 259 mRevealAnimator.start(); 260 // We pause here so we can set the current fraction ourselves. 261 mRevealAnimator.pause(); 262 } 263 mRevealAnimator.setCurrentFraction(shapeRevealProgress); 264 } 265 266 float drawableScale = (mIsVerticalBarLayout ? mOutline.width() : mOutline.height()) 267 / minSize; 268 setBackgroundDrawableBounds(drawableScale); 269 if (isOpening) { 270 // Center align foreground 271 int height = mFinalDrawableBounds.height(); 272 int width = mFinalDrawableBounds.width(); 273 int diffY = mIsVerticalBarLayout ? 0 274 : (int) (((height * drawableScale) - height) / 2); 275 int diffX = mIsVerticalBarLayout ? (int) (((width * drawableScale) - width) / 2) 276 : 0; 277 sTmpRect.set(mFinalDrawableBounds); 278 sTmpRect.offset(diffX, diffY); 279 mForeground.setBounds(sTmpRect); 280 } else { 281 // Spring the foreground relative to the icon's movement within the DragLayer. 282 int diffX = (int) (dX / mLauncher.getDeviceProfile().availableWidthPx 283 * FG_TRANS_X_FACTOR); 284 int diffY = (int) (dY / mLauncher.getDeviceProfile().availableHeightPx 285 * FG_TRANS_Y_FACTOR); 286 287 mFgSpringX.animateToFinalPosition(diffX); 288 mFgSpringY.animateToFinalPosition(diffY); 289 } 290 } 291 invalidate(); 292 invalidateOutline(); 293 } 294 295 @Override onAnimationEnd(Animator animator)296 public void onAnimationEnd(Animator animator) { 297 if (mLoadIconSignal != null) { 298 mLoadIconSignal.cancel(); 299 } 300 if (mEndRunnable != null) { 301 mEndRunnable.run(); 302 } else { 303 // End runnable also ends the reveal animator, so we manually handle it here. 304 if (mRevealAnimator != null) { 305 mRevealAnimator.end(); 306 } 307 } 308 } 309 310 /** 311 * Sets the size and position of this view to match {@param v}. 312 * 313 * @param v The view to copy 314 * @param positionOut Rect that will hold the size and position of v. 315 */ matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut)316 private void matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut) { 317 float rotation = getLocationBoundsForView(launcher, v, isOpening, positionOut); 318 final LayoutParams lp = new LayoutParams( 319 Math.round(positionOut.width()), 320 Math.round(positionOut.height())); 321 updatePosition(rotation, positionOut, lp); 322 setLayoutParams(lp); 323 } 324 updatePosition(float rotation, RectF position, LayoutParams lp)325 private void updatePosition(float rotation, RectF position, LayoutParams lp) { 326 mRotation = rotation; 327 mPositionOut.set(position); 328 lp.ignoreInsets = true; 329 // Position the floating view exactly on top of the original 330 lp.topMargin = Math.round(position.top); 331 if (mIsRtl) { 332 lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - position.right)); 333 } else { 334 lp.setMarginStart(Math.round(position.left)); 335 } 336 // Set the properties here already to make sure they are available when running the first 337 // animation frame. 338 int left = mIsRtl 339 ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width 340 : lp.leftMargin; 341 layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height); 342 } 343 344 /** 345 * Gets the location bounds of a view and returns the overall rotation. 346 * - For DeepShortcutView, we return the bounds of the icon view. 347 * - For BubbleTextView, we return the icon bounds. 348 */ getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, RectF outRect)349 private static float getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, 350 RectF outRect) { 351 boolean ignoreTransform = !isOpening; 352 if (v instanceof DeepShortcutView) { 353 v = ((DeepShortcutView) v).getBubbleText(); 354 ignoreTransform = false; 355 } else if (v.getParent() instanceof DeepShortcutView) { 356 v = ((DeepShortcutView) v.getParent()).getIconView(); 357 ignoreTransform = false; 358 } 359 if (v == null) { 360 return 0; 361 } 362 363 Rect iconBounds = new Rect(); 364 if (v instanceof BubbleTextView) { 365 ((BubbleTextView) v).getIconBounds(iconBounds); 366 } else if (v instanceof FolderIcon) { 367 ((FolderIcon) v).getPreviewBounds(iconBounds); 368 } else { 369 iconBounds.set(0, 0, v.getWidth(), v.getHeight()); 370 } 371 372 float[] points = new float[] {iconBounds.left, iconBounds.top, iconBounds.right, 373 iconBounds.bottom}; 374 float[] rotation = new float[] {0}; 375 Utilities.getDescendantCoordRelativeToAncestor(v, launcher.getDragLayer(), points, 376 false, ignoreTransform, rotation); 377 outRect.set( 378 Math.min(points[0], points[2]), 379 Math.min(points[1], points[3]), 380 Math.max(points[0], points[2]), 381 Math.max(points[1], points[3])); 382 return rotation[0]; 383 } 384 385 /** 386 * Loads the icon and saves the results to {@link #sIconLoadResult}. 387 * Runs onIconLoaded callback (if any), which signifies that the FloatingIconView is 388 * ready to display the icon. Otherwise, the FloatingIconView will grab the results when its 389 * initialized. 390 * 391 * @param originalView The View that the FloatingIconView will replace. 392 * @param info ItemInfo of the originalView 393 * @param pos The position of the view. 394 */ 395 @WorkerThread 396 @SuppressWarnings("WrongThread") getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos, IconLoadResult iconLoadResult)397 private static void getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos, 398 IconLoadResult iconLoadResult) { 399 Drawable drawable = null; 400 Drawable badge = null; 401 boolean supportsAdaptiveIcons = ADAPTIVE_ICON_WINDOW_ANIM.get() 402 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; 403 Drawable btvIcon = originalView instanceof BubbleTextView 404 ? ((BubbleTextView) originalView).getIcon() : null; 405 if (info instanceof SystemShortcut) { 406 if (originalView instanceof ImageView) { 407 drawable = ((ImageView) originalView).getDrawable(); 408 } else if (originalView instanceof DeepShortcutView) { 409 drawable = ((DeepShortcutView) originalView).getIconView().getBackground(); 410 } else { 411 drawable = originalView.getBackground(); 412 } 413 } else { 414 boolean isFolderIcon = originalView instanceof FolderIcon; 415 int width = isFolderIcon ? originalView.getWidth() : (int) pos.width(); 416 int height = isFolderIcon ? originalView.getHeight() : (int) pos.height(); 417 if (supportsAdaptiveIcons) { 418 drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray); 419 if (drawable instanceof AdaptiveIconDrawable) { 420 badge = getBadge(l, info, sTmpObjArray[0]); 421 } else { 422 // The drawable we get back is not an adaptive icon, so we need to use the 423 // BubbleTextView icon that is already legacy treated. 424 drawable = btvIcon; 425 } 426 } else { 427 if (originalView instanceof BubbleTextView) { 428 // Similar to DragView, we simply use the BubbleTextView icon here. 429 drawable = btvIcon; 430 } else { 431 drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray); 432 } 433 } 434 } 435 436 drawable = drawable == null ? null : drawable.getConstantState().newDrawable(); 437 int iconOffset = getOffsetForIconBounds(l, drawable, pos); 438 synchronized (iconLoadResult) { 439 iconLoadResult.drawable = drawable; 440 iconLoadResult.badge = badge; 441 iconLoadResult.iconOffset = iconOffset; 442 if (iconLoadResult.onIconLoaded != null) { 443 l.getMainExecutor().execute(iconLoadResult.onIconLoaded); 444 iconLoadResult.onIconLoaded = null; 445 } 446 iconLoadResult.isIconLoaded = true; 447 } 448 } 449 450 /** 451 * Sets the drawables of the {@param originalView} onto this view. 452 * 453 * @param originalView The View that the FloatingIconView will replace. 454 * @param drawable The drawable of the original view. 455 * @param badge The badge of the original view. 456 * @param iconOffset The amount of offset needed to match this view with the original view. 457 */ 458 @UiThread setIcon(View originalView, @Nullable Drawable drawable, @Nullable Drawable badge, int iconOffset)459 private void setIcon(View originalView, @Nullable Drawable drawable, @Nullable Drawable badge, 460 int iconOffset) { 461 mBadge = badge; 462 463 mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable; 464 if (mIsAdaptiveIcon) { 465 boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon; 466 467 AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable; 468 Drawable background = adaptiveIcon.getBackground(); 469 if (background == null) { 470 background = new ColorDrawable(Color.TRANSPARENT); 471 } 472 mBackground = background; 473 Drawable foreground = adaptiveIcon.getForeground(); 474 if (foreground == null) { 475 foreground = new ColorDrawable(Color.TRANSPARENT); 476 } 477 mForeground = foreground; 478 479 final LayoutParams lp = (LayoutParams) getLayoutParams(); 480 final int originalHeight = lp.height; 481 final int originalWidth = lp.width; 482 483 int blurMargin = mBlurSizeOutline / 2; 484 mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight); 485 486 if (!isFolderIcon) { 487 mFinalDrawableBounds.inset(iconOffset - blurMargin, iconOffset - blurMargin); 488 } 489 mForeground.setBounds(mFinalDrawableBounds); 490 mBackground.setBounds(mFinalDrawableBounds); 491 492 mStartRevealRect.set(0, 0, originalWidth, originalHeight); 493 494 if (mBadge != null) { 495 mBadge.setBounds(mStartRevealRect); 496 if (!mIsOpening && !isFolderIcon) { 497 DRAWABLE_ALPHA.set(mBadge, 0); 498 } 499 } 500 501 if (isFolderIcon) { 502 ((FolderIcon) originalView).getPreviewBounds(sTmpRect); 503 float bgStroke = ((FolderIcon) originalView).getBackgroundStrokeWidth(); 504 if (mForeground instanceof ShiftedBitmapDrawable) { 505 ShiftedBitmapDrawable sbd = (ShiftedBitmapDrawable) mForeground; 506 sbd.setShiftX(sbd.getShiftX() - sTmpRect.left - bgStroke); 507 sbd.setShiftY(sbd.getShiftY() - sTmpRect.top - bgStroke); 508 } 509 if (mBadge instanceof ShiftedBitmapDrawable) { 510 ShiftedBitmapDrawable sbd = (ShiftedBitmapDrawable) mBadge; 511 sbd.setShiftX(sbd.getShiftX() - sTmpRect.left - bgStroke); 512 sbd.setShiftY(sbd.getShiftY() - sTmpRect.top - bgStroke); 513 } 514 } else { 515 Utilities.scaleRectAboutCenter(mStartRevealRect, 516 IconShape.getNormalizationScale()); 517 } 518 519 float aspectRatio = mLauncher.getDeviceProfile().aspectRatio; 520 if (mIsVerticalBarLayout) { 521 lp.width = (int) Math.max(lp.width, lp.height * aspectRatio); 522 } else { 523 lp.height = (int) Math.max(lp.height, lp.width * aspectRatio); 524 } 525 526 int left = mIsRtl 527 ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width 528 : lp.leftMargin; 529 layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height); 530 531 float scale = Math.max((float) lp.height / originalHeight, 532 (float) lp.width / originalWidth); 533 float bgDrawableStartScale; 534 if (mIsOpening) { 535 bgDrawableStartScale = 1f; 536 mOutline.set(0, 0, originalWidth, originalHeight); 537 } else { 538 bgDrawableStartScale = scale; 539 mOutline.set(0, 0, lp.width, lp.height); 540 } 541 setBackgroundDrawableBounds(bgDrawableStartScale); 542 mEndRevealRect.set(0, 0, lp.width, lp.height); 543 setOutlineProvider(new ViewOutlineProvider() { 544 @Override 545 public void getOutline(View view, Outline outline) { 546 outline.setRoundRect(mOutline, mTaskCornerRadius); 547 } 548 }); 549 setClipToOutline(true); 550 } else { 551 setBackground(drawable); 552 setClipToOutline(false); 553 } 554 555 invalidate(); 556 invalidateOutline(); 557 } 558 559 /** 560 * Checks if the icon result is loaded. If true, we set the icon immediately. Else, we add a 561 * callback to set the icon once the icon result is loaded. 562 */ checkIconResult(View originalView)563 private void checkIconResult(View originalView) { 564 CancellationSignal cancellationSignal = new CancellationSignal(); 565 566 if (mIconLoadResult == null) { 567 Log.w(TAG, "No icon load result found in checkIconResult"); 568 return; 569 } 570 571 synchronized (mIconLoadResult) { 572 if (mIconLoadResult.isIconLoaded) { 573 setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge, 574 mIconLoadResult.iconOffset); 575 hideOriginalView(originalView); 576 } else { 577 mIconLoadResult.onIconLoaded = () -> { 578 if (cancellationSignal.isCanceled()) { 579 return; 580 } 581 582 setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge, 583 mIconLoadResult.iconOffset); 584 setVisibility(VISIBLE); 585 hideOriginalView(originalView); 586 }; 587 mLoadIconSignal = cancellationSignal; 588 } 589 } 590 } 591 hideOriginalView(View originalView)592 private void hideOriginalView(View originalView) { 593 if (originalView instanceof IconLabelDotView) { 594 ((IconLabelDotView) originalView).setIconVisible(false); 595 ((IconLabelDotView) originalView).setForceHideDot(true); 596 } else { 597 originalView.setVisibility(INVISIBLE); 598 } 599 } 600 setBackgroundDrawableBounds(float scale)601 private void setBackgroundDrawableBounds(float scale) { 602 sTmpRect.set(mFinalDrawableBounds); 603 Utilities.scaleRectAboutCenter(sTmpRect, scale); 604 // Since the drawable is at the top of the view, we need to offset to keep it centered. 605 if (mIsVerticalBarLayout) { 606 sTmpRect.offsetTo((int) (mFinalDrawableBounds.left * scale), sTmpRect.top); 607 } else { 608 sTmpRect.offsetTo(sTmpRect.left, (int) (mFinalDrawableBounds.top * scale)); 609 } 610 mBackground.setBounds(sTmpRect); 611 } 612 613 @WorkerThread 614 @SuppressWarnings("WrongThread") getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position)615 private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) { 616 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || 617 !(drawable instanceof AdaptiveIconDrawable)) { 618 return 0; 619 } 620 int blurSizeOutline = 621 l.getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline); 622 623 Rect bounds = new Rect(0, 0, (int) position.width() + blurSizeOutline, 624 (int) position.height() + blurSizeOutline); 625 bounds.inset(blurSizeOutline / 2, blurSizeOutline / 2); 626 627 try (LauncherIcons li = LauncherIcons.obtain(l)) { 628 Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(drawable, null, 629 null, null)); 630 } 631 632 bounds.inset( 633 (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()), 634 (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction()) 635 ); 636 637 return bounds.left; 638 } 639 640 @Override setClipPath(Path clipPath)641 public void setClipPath(Path clipPath) { 642 mClipPath = clipPath; 643 invalidate(); 644 } 645 646 @Override draw(Canvas canvas)647 public void draw(Canvas canvas) { 648 int count = canvas.save(); 649 canvas.rotate(mRotation, 650 mFinalDrawableBounds.exactCenterX(), mFinalDrawableBounds.exactCenterY()); 651 if (mClipPath != null) { 652 canvas.clipPath(mClipPath); 653 } 654 super.draw(canvas); 655 if (mBackground != null) { 656 mBackground.draw(canvas); 657 } 658 if (mForeground != null) { 659 int count2 = canvas.save(); 660 canvas.translate(mFgTransX, mFgTransY); 661 mForeground.draw(canvas); 662 canvas.restoreToCount(count2); 663 } 664 if (mBadge != null) { 665 mBadge.draw(canvas); 666 } 667 canvas.restoreToCount(count); 668 } 669 fastFinish()670 public void fastFinish() { 671 if (mLoadIconSignal != null) { 672 mLoadIconSignal.cancel(); 673 } 674 if (mEndRunnable != null) { 675 mEndRunnable.run(); 676 mEndRunnable = null; 677 } 678 if (mFadeAnimatorSet != null) { 679 mFadeAnimatorSet.end(); 680 mFadeAnimatorSet = null; 681 } 682 } 683 684 @Override onAnimationStart(Animator animator)685 public void onAnimationStart(Animator animator) { 686 if (mIconLoadResult != null && mIconLoadResult.isIconLoaded) { 687 setVisibility(View.VISIBLE); 688 } 689 if (!mIsOpening) { 690 // When closing an app, we want the item on the workspace to be invisible immediately 691 hideOriginalView(mOriginalIcon); 692 } 693 } 694 695 @Override onAnimationCancel(Animator animator)696 public void onAnimationCancel(Animator animator) {} 697 698 @Override onAnimationRepeat(Animator animator)699 public void onAnimationRepeat(Animator animator) {} 700 701 @Override onGlobalLayout()702 public void onGlobalLayout() { 703 if (mOriginalIcon.isAttachedToWindow() && mPositionOut != null) { 704 float rotation = getLocationBoundsForView(mLauncher, mOriginalIcon, mIsOpening, 705 sTmpRectF); 706 if (rotation != mRotation || !sTmpRectF.equals(mPositionOut)) { 707 updatePosition(rotation, sTmpRectF, (LayoutParams) getLayoutParams()); 708 if (mOnTargetChangeRunnable != null) { 709 mOnTargetChangeRunnable.run(); 710 } 711 } 712 } 713 } 714 setOnTargetChangeListener(Runnable onTargetChangeListener)715 public void setOnTargetChangeListener(Runnable onTargetChangeListener) { 716 mOnTargetChangeRunnable = onTargetChangeListener; 717 } 718 719 /** 720 * Loads the icon drawable on a worker thread to reduce latency between swapping views. 721 */ 722 @UiThread fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening)723 public static IconLoadResult fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening) { 724 IconLoadResult result = new IconLoadResult(info); 725 MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> { 726 RectF position = new RectF(); 727 getLocationBoundsForView(l, v, isOpening, position); 728 getIconResult(l, v, info, position, result); 729 }); 730 731 sIconLoadResult = result; 732 return result; 733 } 734 735 /** 736 * Creates a floating icon view for {@param originalView}. 737 * @param originalView The view to copy 738 * @param hideOriginal If true, it will hide {@param originalView} while this view is visible. 739 * Else, we will not draw anything in this view. 740 * @param positionOut Rect that will hold the size and position of v. 741 * @param isOpening True if this view replaces the icon for app open animation. 742 */ getFloatingIconView(Launcher launcher, View originalView, boolean hideOriginal, RectF positionOut, boolean isOpening)743 public static FloatingIconView getFloatingIconView(Launcher launcher, View originalView, 744 boolean hideOriginal, RectF positionOut, boolean isOpening) { 745 final DragLayer dragLayer = launcher.getDragLayer(); 746 ViewGroup parent = (ViewGroup) dragLayer.getParent(); 747 FloatingIconView view = launcher.getViewCache().getView(R.layout.floating_icon_view, 748 launcher, parent); 749 view.recycle(); 750 751 // Get the drawable on the background thread 752 boolean shouldLoadIcon = originalView.getTag() instanceof ItemInfo && hideOriginal; 753 if (shouldLoadIcon) { 754 if (sIconLoadResult != null && sIconLoadResult.itemInfo == originalView.getTag()) { 755 view.mIconLoadResult = sIconLoadResult; 756 } else { 757 view.mIconLoadResult = fetchIcon(launcher, originalView, 758 (ItemInfo) originalView.getTag(), isOpening); 759 } 760 } 761 sIconLoadResult = null; 762 763 view.mIsVerticalBarLayout = launcher.getDeviceProfile().isVerticalBarLayout(); 764 view.mIsOpening = isOpening; 765 view.mOriginalIcon = originalView; 766 view.mPositionOut = positionOut; 767 768 // Match the position of the original view. 769 view.matchPositionOf(launcher, originalView, isOpening, positionOut); 770 771 // We need to add it to the overlay, but keep it invisible until animation starts.. 772 view.setVisibility(INVISIBLE); 773 parent.addView(view); 774 dragLayer.addView(view.mListenerView); 775 view.mListenerView.setListener(view::fastFinish); 776 777 view.mEndRunnable = () -> { 778 view.mEndRunnable = null; 779 780 if (hideOriginal) { 781 if (isOpening) { 782 if (originalView instanceof BubbleTextView) { 783 ((BubbleTextView) originalView).setIconVisible(true); 784 ((BubbleTextView) originalView).setForceHideDot(false); 785 } else { 786 originalView.setVisibility(VISIBLE); 787 } 788 view.finish(dragLayer); 789 } else { 790 view.mFadeAnimatorSet = view.createFadeAnimation(originalView, dragLayer); 791 view.mFadeAnimatorSet.start(); 792 } 793 } else { 794 view.finish(dragLayer); 795 } 796 }; 797 798 // Must be called after matchPositionOf so that we know what size to load. 799 // Must be called after the fastFinish listener and end runnable is created so that 800 // the icon is not left in a hidden state. 801 if (shouldLoadIcon) { 802 view.checkIconResult(originalView); 803 } 804 805 return view; 806 } 807 createFadeAnimation(View originalView, DragLayer dragLayer)808 private AnimatorSet createFadeAnimation(View originalView, DragLayer dragLayer) { 809 AnimatorSet fade = new AnimatorSet(); 810 fade.setDuration(FADE_DURATION_MS); 811 fade.addListener(new AnimatorListenerAdapter() { 812 @Override 813 public void onAnimationStart(Animator animation) { 814 originalView.setVisibility(VISIBLE); 815 } 816 817 @Override 818 public void onAnimationEnd(Animator animation) { 819 finish(dragLayer); 820 } 821 }); 822 823 if (mBadge != null) { 824 ObjectAnimator badgeFade = ObjectAnimator.ofInt(mBadge, DRAWABLE_ALPHA, 255); 825 badgeFade.addUpdateListener(valueAnimator -> invalidate()); 826 fade.play(badgeFade); 827 } 828 829 if (originalView instanceof IconLabelDotView) { 830 IconLabelDotView view = (IconLabelDotView) originalView; 831 fade.addListener(new AnimatorListenerAdapter() { 832 @Override 833 public void onAnimationEnd(Animator animation) { 834 view.setIconVisible(true); 835 view.setForceHideDot(false); 836 } 837 }); 838 } 839 840 if (originalView instanceof BubbleTextView) { 841 BubbleTextView btv = (BubbleTextView) originalView; 842 fade.addListener(new AnimatorListenerAdapter() { 843 @Override 844 public void onAnimationStart(Animator animation) { 845 btv.setIconVisible(true); 846 btv.setForceHideDot(true); 847 } 848 }); 849 fade.play(ObjectAnimator.ofInt(btv.getIcon(), DRAWABLE_ALPHA, 0, 255)); 850 } else if (!(originalView instanceof FolderIcon)) { 851 fade.play(ObjectAnimator.ofFloat(originalView, ALPHA, 0f, 1f)); 852 } 853 854 return fade; 855 } 856 finish(DragLayer dragLayer)857 private void finish(DragLayer dragLayer) { 858 ((ViewGroup) dragLayer.getParent()).removeView(this); 859 dragLayer.removeView(mListenerView); 860 recycle(); 861 mLauncher.getViewCache().recycleView(R.layout.floating_icon_view, this); 862 } 863 recycle()864 private void recycle() { 865 setTranslationX(0); 866 setTranslationY(0); 867 setScaleX(1); 868 setScaleY(1); 869 setAlpha(1); 870 setBackground(null); 871 if (mLoadIconSignal != null) { 872 mLoadIconSignal.cancel(); 873 } 874 mLoadIconSignal = null; 875 mEndRunnable = null; 876 mIsAdaptiveIcon = false; 877 mForeground = null; 878 mBackground = null; 879 mClipPath = null; 880 mFinalDrawableBounds.setEmpty(); 881 if (mRevealAnimator != null) { 882 mRevealAnimator.cancel(); 883 } 884 mRevealAnimator = null; 885 if (mFadeAnimatorSet != null) { 886 mFadeAnimatorSet.cancel(); 887 } 888 mPositionOut = null; 889 mFadeAnimatorSet = null; 890 mListenerView.setListener(null); 891 mOriginalIcon = null; 892 mOnTargetChangeRunnable = null; 893 mTaskCornerRadius = 0; 894 mOutline.setEmpty(); 895 mFgTransY = 0; 896 mFgSpringX.cancel(); 897 mFgTransX = 0; 898 mFgSpringY.cancel(); 899 mBadge = null; 900 sTmpObjArray[0] = null; 901 mIconLoadResult = null; 902 } 903 904 private static class IconLoadResult { 905 final ItemInfo itemInfo; 906 Drawable drawable; 907 Drawable badge; 908 int iconOffset; 909 Runnable onIconLoaded; 910 boolean isIconLoaded; 911 IconLoadResult(ItemInfo itemInfo)912 public IconLoadResult(ItemInfo itemInfo) { 913 this.itemInfo = itemInfo; 914 } 915 } 916 } 917