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