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