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