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