• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
setup(Launcher launcher, View invalidateDelegate, int availableSpace, int topPadding)131     public void setup(Launcher launcher, View invalidateDelegate,
132                       int availableSpace, int topPadding) {
133         mInvalidateDelegate = invalidateDelegate;
134         mBgColor = Themes.getAttrColor(launcher, android.R.attr.colorPrimary);
135 
136         DeviceProfile grid = launcher.getDeviceProfile();
137         final int previewSize = grid.folderIconSizePx;
138         final int previewPadding = grid.folderIconPreviewPadding;
139 
140         this.previewSize = (previewSize - 2 * previewPadding);
141 
142         basePreviewOffsetX = (availableSpace - this.previewSize) / 2;
143         basePreviewOffsetY = previewPadding + grid.folderBackgroundOffset + topPadding;
144 
145         // Stroke width is 1dp
146         mStrokeWidth = launcher.getResources().getDisplayMetrics().density;
147 
148         float radius = getScaledRadius();
149         float shadowRadius = radius + mStrokeWidth;
150         int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
151         mShadowShader = new RadialGradient(0, 0, 1,
152                 new int[] {shadowColor, Color.TRANSPARENT},
153                 new float[] {radius / shadowRadius, 1},
154                 Shader.TileMode.CLAMP);
155 
156         invalidate();
157     }
158 
getRadius()159     int getRadius() {
160         return previewSize / 2;
161     }
162 
getScaledRadius()163     int getScaledRadius() {
164         return (int) (mScale * getRadius());
165     }
166 
getOffsetX()167     int getOffsetX() {
168         return basePreviewOffsetX - (getScaledRadius() - getRadius());
169     }
170 
getOffsetY()171     int getOffsetY() {
172         return basePreviewOffsetY - (getScaledRadius() - getRadius());
173     }
174 
175     /**
176      * Returns the progress of the scale animation, where 0 means the scale is at 1f
177      * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
178      */
getScaleProgress()179     float getScaleProgress() {
180         return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
181     }
182 
invalidate()183     void invalidate() {
184         if (mInvalidateDelegate != null) {
185             mInvalidateDelegate.invalidate();
186         }
187 
188         if (mDrawingDelegate != null) {
189             mDrawingDelegate.invalidate();
190         }
191     }
192 
setInvalidateDelegate(View invalidateDelegate)193     void setInvalidateDelegate(View invalidateDelegate) {
194         mInvalidateDelegate = invalidateDelegate;
195         invalidate();
196     }
197 
getBgColor()198     public int getBgColor() {
199         int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
200         return ColorUtils.setAlphaComponent(mBgColor, alpha);
201     }
202 
drawBackground(Canvas canvas)203     public void drawBackground(Canvas canvas) {
204         mPaint.setStyle(Paint.Style.FILL);
205         mPaint.setColor(getBgColor());
206 
207         drawCircle(canvas, 0 /* deltaRadius */);
208 
209         drawShadow(canvas);
210     }
211 
drawShadow(Canvas canvas)212     public void drawShadow(Canvas canvas) {
213         if (mShadowShader == null) {
214             return;
215         }
216 
217         float radius = getScaledRadius();
218         float shadowRadius = radius + mStrokeWidth;
219         mPaint.setStyle(Paint.Style.FILL);
220         mPaint.setColor(Color.BLACK);
221         int offsetX = getOffsetX();
222         int offsetY = getOffsetY();
223         final int saveCount;
224         if (canvas.isHardwareAccelerated()) {
225             saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
226                     offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius,
227                     null, Canvas.CLIP_TO_LAYER_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
228 
229         } else {
230             saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
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 
fadeInBackgroundShadow()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 
animateBackgroundStroke()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 
drawBackgroundStroke(Canvas canvas)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 
drawLeaveBehind(Canvas canvas)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 
drawCircle(Canvas canvas,float deltaRadius)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 
getClipPath()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.
clipCanvasHardware(Canvas canvas)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 
delegateDrawing(CellLayout delegate, int cellX, int cellY)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 
clearDrawingDelegate()342     private void clearDrawingDelegate() {
343         if (mDrawingDelegate != null) {
344             mDrawingDelegate.removeFolderBackground(this);
345         }
346 
347         mDrawingDelegate = null;
348         isClipping = true;
349         invalidate();
350     }
351 
drawingDelegated()352     boolean drawingDelegated() {
353         return mDrawingDelegate != null;
354     }
355 
animateScale(float finalScale, float finalMultiplier, final Runnable onStart, final Runnable onEnd)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 
animateToAccept(final CellLayout cl, final int cellX, final int cellY)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 
animateToRest()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 
getBackgroundAlpha()433     public int getBackgroundAlpha() {
434         return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
435     }
436 
getStrokeWidth()437     public float getStrokeWidth() {
438         return mStrokeWidth;
439     }
440 }
441