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