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