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