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