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