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