1 /* 2 * Copyright (C) 2020 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.app.animation.Interpolators.LINEAR; 19 import static com.android.launcher3.Flags.enableAdditionalHomeAnimations; 20 import static com.android.launcher3.Utilities.boundToRange; 21 import static com.android.launcher3.Utilities.mapToRange; 22 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; 23 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; 24 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION; 25 26 import static java.lang.Math.max; 27 28 import android.animation.ValueAnimator; 29 import android.content.Context; 30 import android.graphics.Canvas; 31 import android.graphics.Color; 32 import android.graphics.Outline; 33 import android.graphics.Path; 34 import android.graphics.Rect; 35 import android.graphics.RectF; 36 import android.graphics.drawable.AdaptiveIconDrawable; 37 import android.graphics.drawable.ColorDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.util.AttributeSet; 40 import android.view.View; 41 import android.view.ViewGroup.MarginLayoutParams; 42 import android.view.ViewOutlineProvider; 43 44 import androidx.annotation.Nullable; 45 import androidx.core.util.Consumer; 46 47 import com.android.launcher3.DeviceProfile; 48 import com.android.launcher3.Flags; 49 import com.android.launcher3.R; 50 import com.android.launcher3.Utilities; 51 import com.android.launcher3.dragndrop.FolderAdaptiveIcon; 52 import com.android.launcher3.graphics.ThemeManager; 53 54 /** 55 * A view used to draw both layers of an {@link AdaptiveIconDrawable}. 56 * Supports springing just the foreground layer. 57 * Supports clipping the icon to/from its icon shape. 58 */ 59 public class ClipIconView extends View implements ClipPathView { 60 61 private static final Rect sTmpRect = new Rect(); 62 63 private final int mBlurSizeOutline; 64 private final boolean mIsRtl; 65 66 private @Nullable Drawable mForeground; 67 private @Nullable Drawable mBackground; 68 69 private boolean mIsAdaptiveIcon = false; 70 71 private ValueAnimator mRevealAnimator; 72 private float mIconScale; 73 74 private final Rect mStartRevealRect = new Rect(); 75 private final Rect mEndRevealRect = new Rect(); 76 private Path mClipPath; 77 private float mTaskCornerRadius; 78 79 private final Rect mOutline = new Rect(); 80 private final Rect mFinalDrawableBounds = new Rect(); 81 82 @Nullable private TaskViewArtist mTaskViewArtist; 83 ClipIconView(Context context)84 public ClipIconView(Context context) { 85 this(context, null); 86 } 87 ClipIconView(Context context, AttributeSet attrs)88 public ClipIconView(Context context, AttributeSet attrs) { 89 this(context, attrs, 0); 90 } 91 ClipIconView(Context context, AttributeSet attrs, int defStyleAttr)92 public ClipIconView(Context context, AttributeSet attrs, int defStyleAttr) { 93 super(context, attrs, defStyleAttr); 94 mBlurSizeOutline = getResources().getDimensionPixelSize( 95 R.dimen.blur_size_medium_outline); 96 mIsRtl = Utilities.isRtl(getResources()); 97 } 98 99 /** 100 * Sets a {@link TaskViewArtist} that will draw a {@link com.android.quickstep.views.TaskView} 101 * within the clip bounds of this view. 102 */ setTaskViewArtist(TaskViewArtist taskViewArtist)103 public void setTaskViewArtist(TaskViewArtist taskViewArtist) { 104 if (!enableAdditionalHomeAnimations()) { 105 return; 106 } 107 mTaskViewArtist = taskViewArtist; 108 invalidate(); 109 } 110 111 /** 112 * Update the icon UI to match the provided parameters during an animation frame 113 */ update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening, View container, DeviceProfile dp)114 public void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, 115 boolean isOpening, View container, DeviceProfile dp) { 116 update(rect, progress, shapeProgressStart, cornerRadius, isOpening, container, dp, 255); 117 } 118 119 /** 120 * Update the icon UI to match the provided parameters during an animation frame, optionally 121 * varying the alpha of the {@link TaskViewArtist} 122 */ update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening, View container, DeviceProfile dp, int taskViewDrawAlpha)123 public void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, 124 boolean isOpening, View container, DeviceProfile dp, int taskViewDrawAlpha) { 125 MarginLayoutParams lp = (MarginLayoutParams) container.getLayoutParams(); 126 127 float dX = mIsRtl 128 ? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width) 129 : rect.left - lp.getMarginStart(); 130 float dY = rect.top - lp.topMargin; 131 container.setTranslationX(dX); 132 container.setTranslationY(dY); 133 134 float minSize = Math.min(lp.width, lp.height); 135 float scaleX = rect.width() / minSize; 136 float scaleY = rect.height() / minSize; 137 float scale = Math.max(1f, Math.min(scaleX, scaleY)); 138 if (mTaskViewArtist != null) { 139 mTaskViewArtist.taskViewDrawWidth = lp.width; 140 mTaskViewArtist.taskViewDrawHeight = lp.height; 141 mTaskViewArtist.taskViewDrawAlpha = taskViewDrawAlpha; 142 mTaskViewArtist.taskViewDrawScale = (mTaskViewArtist.drawForPortraitLayout 143 ? Math.min(lp.height, lp.width) : Math.max(lp.height, lp.width)) 144 / mTaskViewArtist.taskViewMinSize; 145 } 146 147 if (Float.isNaN(scale) || Float.isInfinite(scale)) { 148 // Views are no longer laid out, do not update. 149 return; 150 } 151 152 update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale, minSize, dp); 153 154 container.setPivotX(0); 155 container.setPivotY(0); 156 container.setScaleX(scale); 157 container.setScaleY(scale); 158 159 container.invalidate(); 160 } 161 update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening, float scale, float minSize, DeviceProfile dp)162 private void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius, 163 boolean isOpening, float scale, float minSize, DeviceProfile dp) { 164 // shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION 165 float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f; 166 167 float shapeRevealProgress = boundToRange(mapToRange(max(shapeProgressStart, progress), 168 shapeProgressStart, 1f, 0, toMax, LINEAR), 0, 1); 169 170 if (dp.isLandscape) { 171 mOutline.right = (int) (rect.width() / scale); 172 } else { 173 mOutline.bottom = (int) (rect.height() / scale); 174 } 175 176 mTaskCornerRadius = cornerRadius / scale; 177 if (mIsAdaptiveIcon) { 178 final ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext()); 179 mIconScale = themeManager.getIconState().getIconScale(); 180 if ((!isOpening || Flags.enableLauncherIconShapes()) 181 && progress >= shapeProgressStart) { 182 if (mRevealAnimator == null) { 183 mRevealAnimator = themeManager.getIconShape() 184 .createRevealAnimator(this, mStartRevealRect, 185 mOutline, mTaskCornerRadius, !isOpening); 186 mRevealAnimator.addListener(forEndCallback(() -> mRevealAnimator = null)); 187 mRevealAnimator.start(); 188 // We pause here so we can set the current fraction ourselves. 189 mRevealAnimator.pause(); 190 } 191 mRevealAnimator.setCurrentFraction(shapeRevealProgress); 192 } 193 194 float drawableScale = (dp.isLandscape ? mOutline.width() : mOutline.height()) 195 / minSize; 196 setBackgroundDrawableBounds(drawableScale, dp.isLandscape); 197 198 // Center align foreground 199 int height = mFinalDrawableBounds.height(); 200 int width = mFinalDrawableBounds.width(); 201 int diffY = dp.isLandscape ? 0 202 : (int) (((height * drawableScale) - height) / 2); 203 int diffX = dp.isLandscape ? (int) (((width * drawableScale) - width) / 2) 204 : 0; 205 sTmpRect.set(mFinalDrawableBounds); 206 sTmpRect.offset(diffX, diffY); 207 mForeground.setBounds(sTmpRect); 208 } 209 invalidate(); 210 invalidateOutline(); 211 } 212 setBackgroundDrawableBounds(float scale, boolean isLandscape)213 private void setBackgroundDrawableBounds(float scale, boolean isLandscape) { 214 sTmpRect.set(mFinalDrawableBounds); 215 Utilities.scaleRectAboutCenter(sTmpRect, scale); 216 // Since the drawable is at the top of the view, we need to offset to keep it centered. 217 if (isLandscape) { 218 sTmpRect.offsetTo((int) (mFinalDrawableBounds.left * scale), sTmpRect.top); 219 } else { 220 sTmpRect.offsetTo(sTmpRect.left, (int) (mFinalDrawableBounds.top * scale)); 221 } 222 mBackground.setBounds(sTmpRect); 223 } 224 endReveal()225 protected void endReveal() { 226 if (mRevealAnimator != null) { 227 mRevealAnimator.end(); 228 } 229 } 230 231 /** 232 * Sets the icon for this view as part of initial setup 233 */ setIcon(@ullable Drawable drawable, int iconOffset, MarginLayoutParams lp, boolean isOpening, DeviceProfile dp)234 public void setIcon(@Nullable Drawable drawable, int iconOffset, MarginLayoutParams lp, 235 boolean isOpening, DeviceProfile dp) { 236 mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable; 237 if (mIsAdaptiveIcon) { 238 boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon; 239 240 AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable; 241 Drawable background = adaptiveIcon.getBackground(); 242 if (background == null) { 243 background = new ColorDrawable(Color.TRANSPARENT); 244 } 245 mBackground = background; 246 Drawable foreground = adaptiveIcon.getForeground(); 247 if (foreground == null) { 248 foreground = new ColorDrawable(Color.TRANSPARENT); 249 } 250 mForeground = foreground; 251 252 final int originalHeight = lp.height; 253 final int originalWidth = lp.width; 254 255 int blurMargin = mBlurSizeOutline / 2; 256 mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight); 257 258 if (!isFolderIcon) { 259 mFinalDrawableBounds.inset(iconOffset - blurMargin, iconOffset - blurMargin); 260 } 261 mForeground.setBounds(mFinalDrawableBounds); 262 mBackground.setBounds(mFinalDrawableBounds); 263 264 mStartRevealRect.set(0, 0, originalWidth, originalHeight); 265 266 if (!isFolderIcon) { 267 Utilities.scaleRectAboutCenter(mStartRevealRect, ICON_VISIBLE_AREA_FACTOR); 268 } 269 270 if (dp.isLandscape) { 271 lp.width = (int) Math.max(lp.width, lp.height * dp.aspectRatio); 272 } else { 273 lp.height = (int) Math.max(lp.height, lp.width * dp.aspectRatio); 274 } 275 276 int left = mIsRtl 277 ? dp.widthPx - lp.getMarginStart() - lp.width 278 : lp.leftMargin; 279 layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height); 280 281 float scale = Math.max((float) lp.height / originalHeight, 282 (float) lp.width / originalWidth); 283 float bgDrawableStartScale; 284 if (isOpening) { 285 bgDrawableStartScale = 1f; 286 mOutline.set(0, 0, originalWidth, originalHeight); 287 } else { 288 bgDrawableStartScale = scale; 289 mOutline.set(0, 0, lp.width, lp.height); 290 } 291 setBackgroundDrawableBounds(bgDrawableStartScale, dp.isLandscape); 292 mEndRevealRect.set(0, 0, lp.width, lp.height); 293 setOutlineProvider(new ViewOutlineProvider() { 294 @Override 295 public void getOutline(View view, Outline outline) { 296 outline.setRoundRect(mOutline, mTaskCornerRadius); 297 } 298 }); 299 setClipToOutline(true); 300 } else { 301 setBackground(drawable); 302 setClipToOutline(false); 303 } 304 305 invalidate(); 306 invalidateOutline(); 307 } 308 309 @Override setClipPath(Path clipPath)310 public void setClipPath(Path clipPath) { 311 mClipPath = clipPath; 312 invalidate(); 313 } 314 315 @Override draw(Canvas canvas)316 public void draw(Canvas canvas) { 317 int count1 = canvas.save(); 318 if (mClipPath != null) { 319 canvas.clipPath(mClipPath); 320 } 321 int count2 = canvas.save(); 322 float iconCenterX = 323 (mFinalDrawableBounds.right - mFinalDrawableBounds.left) / 2f * mIconScale; 324 float iconCenterY = 325 (mFinalDrawableBounds.bottom - mFinalDrawableBounds.top) / 2f * mIconScale; 326 canvas.scale(mIconScale, mIconScale, iconCenterX, iconCenterY); 327 if (mBackground != null) { 328 mBackground.draw(canvas); 329 } 330 if (mForeground != null) { 331 mForeground.draw(canvas); 332 } 333 canvas.restoreToCount(count2); 334 super.draw(canvas); 335 if (mTaskViewArtist != null) { 336 canvas.saveLayerAlpha( 337 0, 338 0, 339 mTaskViewArtist.taskViewDrawWidth, 340 mTaskViewArtist.taskViewDrawHeight, 341 mTaskViewArtist.taskViewDrawAlpha); 342 float drawScale = mTaskViewArtist.taskViewDrawScale; 343 canvas.translate(drawScale * mTaskViewArtist.taskViewTranslationX, 344 drawScale * mTaskViewArtist.taskViewTranslationY); 345 canvas.scale(drawScale, drawScale); 346 mTaskViewArtist.taskViewDrawCallback.accept(canvas); 347 } 348 canvas.restoreToCount(count1); 349 } 350 recycle()351 void recycle() { 352 setBackground(null); 353 mIsAdaptiveIcon = false; 354 mForeground = null; 355 mBackground = null; 356 mClipPath = null; 357 mFinalDrawableBounds.setEmpty(); 358 if (mRevealAnimator != null) { 359 mRevealAnimator.cancel(); 360 } 361 mRevealAnimator = null; 362 mTaskCornerRadius = 0; 363 mOutline.setEmpty(); 364 mTaskViewArtist = null; 365 } 366 367 /** 368 * Utility class to help draw a {@link com.android.quickstep.views.TaskView} within 369 * a {@link ClipIconView} bounds. 370 */ 371 public static class TaskViewArtist { 372 373 public final Consumer<Canvas> taskViewDrawCallback; 374 public final float taskViewTranslationX; 375 public final float taskViewTranslationY; 376 public final float taskViewMinSize; 377 public final boolean drawForPortraitLayout; 378 379 public int taskViewDrawAlpha; 380 public float taskViewDrawScale; 381 public int taskViewDrawWidth; 382 public int taskViewDrawHeight; 383 TaskViewArtist( Consumer<Canvas> taskViewDrawCallback, float taskViewTranslationX, float taskViewTranslationY, float taskViewMinSize, boolean drawForPortraitLayout)384 public TaskViewArtist( 385 Consumer<Canvas> taskViewDrawCallback, 386 float taskViewTranslationX, 387 float taskViewTranslationY, 388 float taskViewMinSize, 389 boolean drawForPortraitLayout) { 390 this.taskViewDrawCallback = taskViewDrawCallback; 391 this.taskViewTranslationX = taskViewTranslationX; 392 this.taskViewTranslationY = taskViewTranslationY; 393 this.taskViewMinSize = taskViewMinSize; 394 this.drawForPortraitLayout = drawForPortraitLayout; 395 } 396 } 397 } 398