1 /* 2 * Copyright (C) 2017 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 17 package com.android.launcher3.folder; 18 19 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE; 20 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; 21 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.animation.ObjectAnimator; 26 import android.animation.ValueAnimator; 27 import android.content.Context; 28 import android.content.res.TypedArray; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.Matrix; 32 import android.graphics.Paint; 33 import android.graphics.Path; 34 import android.graphics.PorterDuff; 35 import android.graphics.PorterDuffXfermode; 36 import android.graphics.RadialGradient; 37 import android.graphics.Rect; 38 import android.graphics.Region; 39 import android.graphics.Shader; 40 import android.util.Property; 41 import android.view.View; 42 import android.view.animation.Interpolator; 43 44 import androidx.annotation.VisibleForTesting; 45 46 import com.android.launcher3.CellLayout; 47 import com.android.launcher3.DeviceProfile; 48 import com.android.launcher3.R; 49 import com.android.launcher3.celllayout.DelegatedCellDrawing; 50 import com.android.launcher3.graphics.ShapeDelegate; 51 import com.android.launcher3.graphics.ThemeManager; 52 import com.android.launcher3.util.Themes; 53 import com.android.launcher3.views.ActivityContext; 54 55 /** 56 * This object represents a FolderIcon preview background. It stores drawing / measurement 57 * information, handles drawing, and animation (accept state <--> rest state). 58 */ 59 public class PreviewBackground extends DelegatedCellDrawing { 60 61 private static final boolean DRAW_SHADOW = false; 62 private static final boolean DRAW_STROKE = false; 63 64 @VisibleForTesting protected static final int CONSUMPTION_ANIMATION_DURATION = 100; 65 66 @VisibleForTesting protected static final float HOVER_SCALE = 1.1f; 67 @VisibleForTesting protected static final int HOVER_ANIMATION_DURATION = 300; 68 69 private final Context mContext; 70 private final PorterDuffXfermode mShadowPorterDuffXfermode 71 = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); 72 private RadialGradient mShadowShader = null; 73 74 private final Matrix mShaderMatrix = new Matrix(); 75 private final Path mPath = new Path(); 76 77 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 78 79 float mScale = 1f; 80 private int mBgColor; 81 private int mStrokeColor; 82 private int mDotColor; 83 private float mStrokeWidth; 84 private int mStrokeAlpha = MAX_BG_OPACITY; 85 private int mShadowAlpha = 255; 86 private View mInvalidateDelegate; 87 88 int previewSize; 89 int basePreviewOffsetX; 90 int basePreviewOffsetY; 91 92 private CellLayout mDrawingDelegate; 93 94 // When the PreviewBackground is drawn under an icon (for creating a folder) the border 95 // should not occlude the icon 96 public boolean isClipping = true; 97 98 // Drawing / animation configurations 99 @VisibleForTesting protected static final float ACCEPT_SCALE_FACTOR = 1.20f; 100 101 // Expressed on a scale from 0 to 255. 102 private static final int BG_OPACITY = 255; 103 private static final int MAX_BG_OPACITY = 255; 104 private static final int SHADOW_OPACITY = 40; 105 106 @VisibleForTesting protected ValueAnimator mScaleAnimator; 107 private ObjectAnimator mStrokeAlphaAnimator; 108 private ObjectAnimator mShadowAnimator; 109 110 @VisibleForTesting protected boolean mIsAccepting; 111 @VisibleForTesting protected boolean mIsHovered; 112 @VisibleForTesting protected boolean mIsHoveredOrAnimating; 113 114 private static final Property<PreviewBackground, Integer> STROKE_ALPHA = 115 new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") { 116 @Override 117 public Integer get(PreviewBackground previewBackground) { 118 return previewBackground.mStrokeAlpha; 119 } 120 121 @Override 122 public void set(PreviewBackground previewBackground, Integer alpha) { 123 previewBackground.mStrokeAlpha = alpha; 124 previewBackground.invalidate(); 125 } 126 }; 127 128 private static final Property<PreviewBackground, Integer> SHADOW_ALPHA = 129 new Property<PreviewBackground, Integer>(Integer.class, "shadowAlpha") { 130 @Override 131 public Integer get(PreviewBackground previewBackground) { 132 return previewBackground.mShadowAlpha; 133 } 134 135 @Override 136 public void set(PreviewBackground previewBackground, Integer alpha) { 137 previewBackground.mShadowAlpha = alpha; 138 previewBackground.invalidate(); 139 } 140 }; 141 PreviewBackground(Context context)142 public PreviewBackground(Context context) { 143 mContext = context; 144 } 145 146 /** 147 * Draws folder background under cell layout 148 */ 149 @Override drawUnderItem(Canvas canvas)150 public void drawUnderItem(Canvas canvas) { 151 drawBackground(canvas); 152 if (!isClipping) { 153 drawBackgroundStroke(canvas); 154 } 155 } 156 157 /** 158 * Draws folder background on cell layout 159 */ 160 @Override drawOverItem(Canvas canvas)161 public void drawOverItem(Canvas canvas) { 162 if (isClipping) { 163 drawBackgroundStroke(canvas); 164 } 165 } 166 setup(Context context, ActivityContext activity, View invalidateDelegate, int availableSpaceX, int topPadding)167 public void setup(Context context, ActivityContext activity, View invalidateDelegate, 168 int availableSpaceX, int topPadding) { 169 mInvalidateDelegate = invalidateDelegate; 170 171 TypedArray ta = context.getTheme().obtainStyledAttributes(R.styleable.FolderIconPreview); 172 mDotColor = Themes.getAttrColor(context, R.attr.notificationDotColor); 173 mStrokeColor = ta.getColor(R.styleable.FolderIconPreview_folderIconBorderColor, 0); 174 mBgColor = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0); 175 ta.recycle(); 176 177 DeviceProfile grid = activity.getDeviceProfile(); 178 previewSize = grid.folderIconSizePx; 179 180 basePreviewOffsetX = (availableSpaceX - previewSize) / 2; 181 basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx; 182 183 // Stroke width is 1dp 184 mStrokeWidth = context.getResources().getDisplayMetrics().density; 185 186 if (DRAW_SHADOW) { 187 float radius = getScaledRadius(); 188 float shadowRadius = radius + mStrokeWidth; 189 int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0); 190 mShadowShader = new RadialGradient(0, 0, 1, 191 new int[]{shadowColor, Color.TRANSPARENT}, 192 new float[]{radius / shadowRadius, 1}, 193 Shader.TileMode.CLAMP); 194 } 195 196 invalidate(); 197 } 198 getBounds(Rect outBounds)199 void getBounds(Rect outBounds) { 200 int top = basePreviewOffsetY; 201 int left = basePreviewOffsetX; 202 int right = left + previewSize; 203 int bottom = top + previewSize; 204 outBounds.set(left, top, right, bottom); 205 } 206 getRadius()207 public int getRadius() { 208 return previewSize / 2; 209 } 210 getScaledRadius()211 int getScaledRadius() { 212 return (int) (mScale * getRadius()); 213 } 214 getOffsetX()215 int getOffsetX() { 216 return basePreviewOffsetX - (getScaledRadius() - getRadius()); 217 } 218 getOffsetY()219 int getOffsetY() { 220 return basePreviewOffsetY - (getScaledRadius() - getRadius()); 221 } 222 223 /** 224 * Returns the progress of the scale animation to accept state, where 0 means the scale is at 225 * 1f and 1 means the scale is at ACCEPT_SCALE_FACTOR. Returns 0 when scaled due to hover. 226 */ getAcceptScaleProgress()227 float getAcceptScaleProgress() { 228 return mIsHoveredOrAnimating ? 0 : (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f); 229 } 230 invalidate()231 void invalidate() { 232 if (mInvalidateDelegate != null) { 233 mInvalidateDelegate.invalidate(); 234 } 235 236 if (mDrawingDelegate != null) { 237 mDrawingDelegate.invalidate(); 238 } 239 } 240 setInvalidateDelegate(View invalidateDelegate)241 void setInvalidateDelegate(View invalidateDelegate) { 242 mInvalidateDelegate = invalidateDelegate; 243 invalidate(); 244 } 245 getBgColor()246 public int getBgColor() { 247 return mBgColor; 248 } 249 getDotColor()250 public int getDotColor() { 251 return mDotColor; 252 } 253 drawBackground(Canvas canvas)254 public void drawBackground(Canvas canvas) { 255 mPaint.setStyle(Paint.Style.FILL); 256 mPaint.setColor(getBgColor()); 257 258 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint); 259 drawShadow(canvas); 260 } 261 getShape()262 private ShapeDelegate getShape() { 263 return ThemeManager.INSTANCE.get(mContext).getFolderShape(); 264 } 265 drawShadow(Canvas canvas)266 public void drawShadow(Canvas canvas) { 267 if (!DRAW_SHADOW) { 268 return; 269 } 270 if (mShadowShader == null) { 271 return; 272 } 273 274 float radius = getScaledRadius(); 275 float shadowRadius = radius + mStrokeWidth; 276 mPaint.setStyle(Paint.Style.FILL); 277 mPaint.setColor(Color.BLACK); 278 int offsetX = getOffsetX(); 279 int offsetY = getOffsetY(); 280 final int saveCount; 281 if (canvas.isHardwareAccelerated()) { 282 saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY, 283 offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, null); 284 285 } else { 286 saveCount = canvas.save(); 287 canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE); 288 } 289 290 mShaderMatrix.setScale(shadowRadius, shadowRadius); 291 mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY); 292 mShadowShader.setLocalMatrix(mShaderMatrix); 293 mPaint.setAlpha(mShadowAlpha); 294 mPaint.setShader(mShadowShader); 295 canvas.drawPaint(mPaint); 296 mPaint.setAlpha(255); 297 mPaint.setShader(null); 298 if (canvas.isHardwareAccelerated()) { 299 mPaint.setXfermode(mShadowPorterDuffXfermode); 300 getShape().drawShape(canvas, offsetX, offsetY, radius, mPaint); 301 mPaint.setXfermode(null); 302 } 303 304 canvas.restoreToCount(saveCount); 305 } 306 fadeInBackgroundShadow()307 public void fadeInBackgroundShadow() { 308 if (!DRAW_SHADOW) { 309 return; 310 } 311 if (mShadowAnimator != null) { 312 mShadowAnimator.cancel(); 313 } 314 mShadowAnimator = ObjectAnimator 315 .ofInt(this, SHADOW_ALPHA, 0, 255) 316 .setDuration(100); 317 mShadowAnimator.addListener(new AnimatorListenerAdapter() { 318 @Override 319 public void onAnimationEnd(Animator animation) { 320 mShadowAnimator = null; 321 } 322 }); 323 mShadowAnimator.start(); 324 } 325 animateBackgroundStroke()326 public void animateBackgroundStroke() { 327 if (!DRAW_STROKE) { 328 return; 329 } 330 331 if (mStrokeAlphaAnimator != null) { 332 mStrokeAlphaAnimator.cancel(); 333 } 334 mStrokeAlphaAnimator = ObjectAnimator 335 .ofInt(this, STROKE_ALPHA, MAX_BG_OPACITY / 2, MAX_BG_OPACITY) 336 .setDuration(100); 337 mStrokeAlphaAnimator.addListener(new AnimatorListenerAdapter() { 338 @Override 339 public void onAnimationEnd(Animator animation) { 340 mStrokeAlphaAnimator = null; 341 } 342 }); 343 mStrokeAlphaAnimator.start(); 344 } 345 drawBackgroundStroke(Canvas canvas)346 public void drawBackgroundStroke(Canvas canvas) { 347 if (!DRAW_STROKE) { 348 return; 349 } 350 mPaint.setColor(setColorAlphaBound(mStrokeColor, mStrokeAlpha)); 351 mPaint.setStyle(Paint.Style.STROKE); 352 mPaint.setStrokeWidth(mStrokeWidth); 353 354 float inset = 1f; 355 getShape().drawShape(canvas, 356 getOffsetX() + inset, getOffsetY() + inset, getScaledRadius() - inset, mPaint); 357 } 358 359 /** 360 * Draws the leave-behind circle on the given canvas and in the given color. 361 */ drawLeaveBehind(Canvas canvas, int color)362 public void drawLeaveBehind(Canvas canvas, int color) { 363 float originalScale = mScale; 364 mScale = 0.5f; 365 366 mPaint.setStyle(Paint.Style.FILL); 367 mPaint.setColor(color); 368 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint); 369 370 mScale = originalScale; 371 } 372 getClipPath()373 public Path getClipPath() { 374 mPath.reset(); 375 float radius = getScaledRadius() * ClippedFolderIconLayoutRule.getIconOverlapFactor(); 376 // Find the difference in radius so that the clip path remains centered. 377 float radiusDifference = radius - getRadius(); 378 float offsetX = basePreviewOffsetX - radiusDifference; 379 float offsetY = basePreviewOffsetY - radiusDifference; 380 getShape().addToPath(mPath, offsetX, offsetY, radius); 381 return mPath; 382 } 383 delegateDrawing(CellLayout delegate, int cellX, int cellY)384 private void delegateDrawing(CellLayout delegate, int cellX, int cellY) { 385 if (mDrawingDelegate != delegate) { 386 delegate.addDelegatedCellDrawing(this); 387 } 388 389 mDrawingDelegate = delegate; 390 mDelegateCellX = cellX; 391 mDelegateCellY = cellY; 392 393 invalidate(); 394 } 395 clearDrawingDelegate()396 private void clearDrawingDelegate() { 397 if (mDrawingDelegate != null) { 398 mDrawingDelegate.removeDelegatedCellDrawing(this); 399 } 400 401 mDrawingDelegate = null; 402 isClipping = false; 403 invalidate(); 404 } 405 drawingDelegated()406 boolean drawingDelegated() { 407 return mDrawingDelegate != null; 408 } 409 animateScale(boolean isAccepting, boolean isHovered)410 protected void animateScale(boolean isAccepting, boolean isHovered) { 411 if (mScaleAnimator != null) { 412 mScaleAnimator.cancel(); 413 } 414 415 final float startScale = mScale; 416 final float endScale = isAccepting ? ACCEPT_SCALE_FACTOR : (isHovered ? HOVER_SCALE : 1f); 417 Interpolator interpolator = 418 isAccepting != mIsAccepting ? ACCELERATE_DECELERATE : EMPHASIZED_DECELERATE; 419 int duration = isAccepting != mIsAccepting ? CONSUMPTION_ANIMATION_DURATION 420 : HOVER_ANIMATION_DURATION; 421 mIsAccepting = isAccepting; 422 mIsHovered = isHovered; 423 if (startScale == endScale) { 424 if (!mIsAccepting) { 425 clearDrawingDelegate(); 426 } 427 mIsHoveredOrAnimating = mIsHovered; 428 return; 429 } 430 431 432 mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f); 433 mScaleAnimator.addUpdateListener(animation -> { 434 float prog = animation.getAnimatedFraction(); 435 mScale = prog * endScale + (1 - prog) * startScale; 436 invalidate(); 437 }); 438 mScaleAnimator.addListener(new AnimatorListenerAdapter() { 439 @Override 440 public void onAnimationStart(Animator animation) { 441 if (mIsHovered) { 442 mIsHoveredOrAnimating = true; 443 } 444 } 445 446 @Override 447 public void onAnimationEnd(Animator animation) { 448 if (!mIsAccepting) { 449 clearDrawingDelegate(); 450 } 451 mIsHoveredOrAnimating = mIsHovered; 452 mScaleAnimator = null; 453 } 454 }); 455 mScaleAnimator.setInterpolator(interpolator); 456 mScaleAnimator.setDuration(duration); 457 mScaleAnimator.start(); 458 } 459 animateToAccept(CellLayout cl, int cellX, int cellY)460 public void animateToAccept(CellLayout cl, int cellX, int cellY) { 461 delegateDrawing(cl, cellX, cellY); 462 animateScale(/* isAccepting= */ true, mIsHovered); 463 } 464 animateToRest()465 public void animateToRest() { 466 animateScale(/* isAccepting= */ false, mIsHovered); 467 } 468 getStrokeWidth()469 public float getStrokeWidth() { 470 return mStrokeWidth; 471 } 472 setHovered(boolean hovered)473 protected void setHovered(boolean hovered) { 474 animateScale(mIsAccepting, /* isHovered= */ hovered); 475 } 476 } 477