blob: 069ec4ba1157440d5b4d0cbe1c1b670f6464c39e [file] [log] [blame]
Jon Mirandaa0233f72017-06-22 18:34:45 -07001/*
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
17package com.android.launcher3.folder;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Matrix;
26import android.graphics.Paint;
27import android.graphics.Path;
28import android.graphics.PorterDuff;
29import android.graphics.PorterDuffXfermode;
30import android.graphics.RadialGradient;
31import android.graphics.Region;
32import android.graphics.Shader;
33import android.support.v4.graphics.ColorUtils;
34import android.util.Property;
35import android.view.View;
36
37import com.android.launcher3.CellLayout;
38import com.android.launcher3.DeviceProfile;
39import com.android.launcher3.Launcher;
40import com.android.launcher3.LauncherAnimUtils;
41import 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 */
47public 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
Jon Miranda72b5fd12017-06-25 18:06:23 -070091 private static final float ACCEPT_SCALE_FACTOR = 1.20f;
Jon Mirandaa0233f72017-06-22 18:34:45 -070092 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,
Jon Miranda591e3602018-03-28 11:37:00 -0700132 int availableSpaceX, int topPadding) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700133 mInvalidateDelegate = invalidateDelegate;
134 mBgColor = Themes.getAttrColor(launcher, android.R.attr.colorPrimary);
135
136 DeviceProfile grid = launcher.getDeviceProfile();
Jon Miranda591e3602018-03-28 11:37:00 -0700137 previewSize = grid.folderIconSizePx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700138
Jon Miranda591e3602018-03-28 11:37:00 -0700139 basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
140 basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700141
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
Sunny Goyale29897f2017-07-20 10:09:42 -0700195 public int getBgColor() {
196 int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
197 return ColorUtils.setAlphaComponent(mBgColor, alpha);
198 }
199
Sunny Goyal179249d2017-12-19 16:49:24 -0800200 public int getBadgeColor() {
201 return mBgColor;
202 }
203
Jon Mirandaa0233f72017-06-22 18:34:45 -0700204 public void drawBackground(Canvas canvas) {
205 mPaint.setStyle(Paint.Style.FILL);
Sunny Goyale29897f2017-07-20 10:09:42 -0700206 mPaint.setColor(getBgColor());
Jon Mirandaa0233f72017-06-22 18:34:45 -0700207
208 drawCircle(canvas, 0 /* deltaRadius */);
209
Sunny Goyale29897f2017-07-20 10:09:42 -0700210 drawShadow(canvas);
211 }
212
213 public void drawShadow(Canvas canvas) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700214 if (mShadowShader == null) {
215 return;
216 }
Sunny Goyale29897f2017-07-20 10:09:42 -0700217
Jon Mirandaa0233f72017-06-22 18:34:45 -0700218 float radius = getScaledRadius();
219 float shadowRadius = radius + mStrokeWidth;
Sunny Goyale29897f2017-07-20 10:09:42 -0700220 mPaint.setStyle(Paint.Style.FILL);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700221 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,
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500227 offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, null);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700228
229 } else {
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500230 saveCount = canvas.save();
Sunny Goyale29897f2017-07-20 10:09:42 -0700231 canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700232 }
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
Sunny Goyale29897f2017-07-20 10:09:42 -0700307 public Path getClipPath() {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700308 mPath.reset();
309 float r = getScaledRadius();
310 mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW);
Sunny Goyale29897f2017-07-20 10:09:42 -0700311 return mPath;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700312 }
313
314 // It is the callers responsibility to save and restore the canvas layers.
315 void clipCanvasHardware(Canvas canvas) {
316 mPaint.setColor(Color.BLACK);
Sunny Goyal5247f5b2017-07-04 12:07:46 -0700317 mPaint.setStyle(Paint.Style.FILL);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700318 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;
Sunny Goyal5247f5b2017-07-04 12:07:46 -0700348 isClipping = true;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700349 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}