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