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