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.app.animation.Interpolators.ACCELERATE_DECELERATE;
20 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
21 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
22 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
23 
24 import android.animation.Animator;
25 import android.animation.AnimatorListenerAdapter;
26 import android.animation.ObjectAnimator;
27 import android.animation.ValueAnimator;
28 import android.content.Context;
29 import android.content.res.TypedArray;
30 import android.graphics.Canvas;
31 import android.graphics.Color;
32 import android.graphics.Matrix;
33 import android.graphics.Paint;
34 import android.graphics.Path;
35 import android.graphics.PorterDuff;
36 import android.graphics.PorterDuffXfermode;
37 import android.graphics.RadialGradient;
38 import android.graphics.Rect;
39 import android.graphics.Region;
40 import android.graphics.Shader;
41 import android.util.Property;
42 import android.view.View;
43 import android.view.animation.Interpolator;
44 
45 import androidx.annotation.VisibleForTesting;
46 
47 import com.android.launcher3.CellLayout;
48 import com.android.launcher3.DeviceProfile;
49 import com.android.launcher3.R;
50 import com.android.launcher3.celllayout.DelegatedCellDrawing;
51 import com.android.launcher3.graphics.IconShape;
52 import com.android.launcher3.graphics.IconShape.ShapeDelegate;
53 import com.android.launcher3.util.Themes;
54 import com.android.launcher3.views.ActivityContext;
55 
56 /**
57  * This object represents a FolderIcon preview background. It stores drawing / measurement
58  * information, handles drawing, and animation (accept state <--> rest state).
59  */
60 public class PreviewBackground extends DelegatedCellDrawing {
61 
62     private static final boolean DRAW_SHADOW = false;
63     private static final boolean DRAW_STROKE = false;
64 
65     @VisibleForTesting protected static final int CONSUMPTION_ANIMATION_DURATION = 100;
66 
67     @VisibleForTesting protected static final float HOVER_SCALE = 1.1f;
68     @VisibleForTesting protected static final int HOVER_ANIMATION_DURATION = 300;
69 
70     private final Context mContext;
71     private final PorterDuffXfermode mShadowPorterDuffXfermode
72             = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
73     private RadialGradient mShadowShader = null;
74 
75     private final Matrix mShaderMatrix = new Matrix();
76     private final Path mPath = new Path();
77 
78     private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
79 
80     float mScale = 1f;
81     private int mBgColor;
82     private int mStrokeColor;
83     private int mDotColor;
84     private float mStrokeWidth;
85     private int mStrokeAlpha = MAX_BG_OPACITY;
86     private int mShadowAlpha = 255;
87     private View mInvalidateDelegate;
88 
89     int previewSize;
90     int basePreviewOffsetX;
91     int basePreviewOffsetY;
92 
93     private CellLayout mDrawingDelegate;
94 
95     // When the PreviewBackground is drawn under an icon (for creating a folder) the border
96     // should not occlude the icon
97     public boolean isClipping = true;
98 
99     // Drawing / animation configurations
100     @VisibleForTesting protected static final float ACCEPT_SCALE_FACTOR = 1.20f;
101 
102     // Expressed on a scale from 0 to 255.
103     private static final int BG_OPACITY = 255;
104     private static final int MAX_BG_OPACITY = 255;
105     private static final int SHADOW_OPACITY = 40;
106 
107     @VisibleForTesting protected ValueAnimator mScaleAnimator;
108     private ObjectAnimator mStrokeAlphaAnimator;
109     private ObjectAnimator mShadowAnimator;
110 
111     @VisibleForTesting protected boolean mIsAccepting;
112     @VisibleForTesting protected boolean mIsHovered;
113     @VisibleForTesting protected boolean mIsHoveredOrAnimating;
114 
115     private static final Property<PreviewBackground, Integer> STROKE_ALPHA =
116             new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") {
117                 @Override
118                 public Integer get(PreviewBackground previewBackground) {
119                     return previewBackground.mStrokeAlpha;
120                 }
121 
122                 @Override
123                 public void set(PreviewBackground previewBackground, Integer alpha) {
124                     previewBackground.mStrokeAlpha = alpha;
125                     previewBackground.invalidate();
126                 }
127             };
128 
129     private static final Property<PreviewBackground, Integer> SHADOW_ALPHA =
130             new Property<PreviewBackground, Integer>(Integer.class, "shadowAlpha") {
131                 @Override
132                 public Integer get(PreviewBackground previewBackground) {
133                     return previewBackground.mShadowAlpha;
134                 }
135 
136                 @Override
137                 public void set(PreviewBackground previewBackground, Integer alpha) {
138                     previewBackground.mShadowAlpha = alpha;
139                     previewBackground.invalidate();
140                 }
141             };
142 
PreviewBackground(Context context)143     public PreviewBackground(Context context) {
144         mContext = context;
145     }
146 
147     /**
148      * Draws folder background under cell layout
149      */
150     @Override
drawUnderItem(Canvas canvas)151     public void drawUnderItem(Canvas canvas) {
152         drawBackground(canvas);
153         if (!isClipping) {
154             drawBackgroundStroke(canvas);
155         }
156     }
157 
158     /**
159      * Draws folder background on cell layout
160      */
161     @Override
drawOverItem(Canvas canvas)162     public void drawOverItem(Canvas canvas) {
163         if (isClipping) {
164             drawBackgroundStroke(canvas);
165         }
166     }
167 
setup(Context context, ActivityContext activity, View invalidateDelegate, int availableSpaceX, int topPadding)168     public void setup(Context context, ActivityContext activity, View invalidateDelegate,
169                       int availableSpaceX, int topPadding) {
170         mInvalidateDelegate = invalidateDelegate;
171 
172         TypedArray ta = context.getTheme().obtainStyledAttributes(R.styleable.FolderIconPreview);
173         mDotColor = Themes.getAttrColor(context, R.attr.notificationDotColor);
174         mStrokeColor = ta.getColor(R.styleable.FolderIconPreview_folderIconBorderColor, 0);
175         mBgColor = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0);
176         ta.recycle();
177 
178         DeviceProfile grid = activity.getDeviceProfile();
179         previewSize = grid.folderIconSizePx;
180 
181         basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
182         basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
183 
184         // Stroke width is 1dp
185         mStrokeWidth = context.getResources().getDisplayMetrics().density;
186 
187         if (DRAW_SHADOW) {
188             float radius = getScaledRadius();
189             float shadowRadius = radius + mStrokeWidth;
190             int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
191             mShadowShader = new RadialGradient(0, 0, 1,
192                     new int[]{shadowColor, Color.TRANSPARENT},
193                     new float[]{radius / shadowRadius, 1},
194                     Shader.TileMode.CLAMP);
195         }
196 
197         invalidate();
198     }
199 
getBounds(Rect outBounds)200     void getBounds(Rect outBounds) {
201         int top = basePreviewOffsetY;
202         int left = basePreviewOffsetX;
203         int right = left + previewSize;
204         int bottom = top + previewSize;
205         outBounds.set(left, top, right, bottom);
206     }
207 
getRadius()208     public int getRadius() {
209         return previewSize / 2;
210     }
211 
getScaledRadius()212     int getScaledRadius() {
213         return (int) (mScale * getRadius());
214     }
215 
getOffsetX()216     int getOffsetX() {
217         return basePreviewOffsetX - (getScaledRadius() - getRadius());
218     }
219 
getOffsetY()220     int getOffsetY() {
221         return basePreviewOffsetY - (getScaledRadius() - getRadius());
222     }
223 
224     /**
225      * Returns the progress of the scale animation to accept state, where 0 means the scale is at
226      * 1f and 1 means the scale is at ACCEPT_SCALE_FACTOR. Returns 0 when scaled due to hover.
227      */
getAcceptScaleProgress()228     float getAcceptScaleProgress() {
229         return mIsHoveredOrAnimating ? 0 : (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
230     }
231 
invalidate()232     void invalidate() {
233         if (mInvalidateDelegate != null) {
234             mInvalidateDelegate.invalidate();
235         }
236 
237         if (mDrawingDelegate != null) {
238             mDrawingDelegate.invalidate();
239         }
240     }
241 
setInvalidateDelegate(View invalidateDelegate)242     void setInvalidateDelegate(View invalidateDelegate) {
243         mInvalidateDelegate = invalidateDelegate;
244         invalidate();
245     }
246 
getBgColor()247     public int getBgColor() {
248         return mBgColor;
249     }
250 
getDotColor()251     public int getDotColor() {
252         return mDotColor;
253     }
254 
drawBackground(Canvas canvas)255     public void drawBackground(Canvas canvas) {
256         mPaint.setStyle(Paint.Style.FILL);
257         mPaint.setColor(getBgColor());
258 
259         getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
260         drawShadow(canvas);
261     }
262 
getShape()263     private ShapeDelegate getShape() {
264         return IconShape.INSTANCE.get(mContext).getShape();
265     }
266 
drawShadow(Canvas canvas)267     public void drawShadow(Canvas canvas) {
268         if (!DRAW_SHADOW) {
269             return;
270         }
271         if (mShadowShader == null) {
272             return;
273         }
274 
275         float radius = getScaledRadius();
276         float shadowRadius = radius + mStrokeWidth;
277         mPaint.setStyle(Paint.Style.FILL);
278         mPaint.setColor(Color.BLACK);
279         int offsetX = getOffsetX();
280         int offsetY = getOffsetY();
281         final int saveCount;
282         if (canvas.isHardwareAccelerated()) {
283             saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
284                     offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, null);
285 
286         } else {
287             saveCount = canvas.save();
288             canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
289         }
290 
291         mShaderMatrix.setScale(shadowRadius, shadowRadius);
292         mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
293         mShadowShader.setLocalMatrix(mShaderMatrix);
294         mPaint.setAlpha(mShadowAlpha);
295         mPaint.setShader(mShadowShader);
296         canvas.drawPaint(mPaint);
297         mPaint.setAlpha(255);
298         mPaint.setShader(null);
299         if (canvas.isHardwareAccelerated()) {
300             mPaint.setXfermode(mShadowPorterDuffXfermode);
301             getShape().drawShape(canvas, offsetX, offsetY, radius, mPaint);
302             mPaint.setXfermode(null);
303         }
304 
305         canvas.restoreToCount(saveCount);
306     }
307 
fadeInBackgroundShadow()308     public void fadeInBackgroundShadow() {
309         if (!DRAW_SHADOW) {
310             return;
311         }
312         if (mShadowAnimator != null) {
313             mShadowAnimator.cancel();
314         }
315         mShadowAnimator = ObjectAnimator
316                 .ofInt(this, SHADOW_ALPHA, 0, 255)
317                 .setDuration(100);
318         mShadowAnimator.addListener(new AnimatorListenerAdapter() {
319             @Override
320             public void onAnimationEnd(Animator animation) {
321                 mShadowAnimator = null;
322             }
323         });
324         mShadowAnimator.start();
325     }
326 
animateBackgroundStroke()327     public void animateBackgroundStroke() {
328         if (!DRAW_STROKE) {
329             return;
330         }
331 
332         if (mStrokeAlphaAnimator != null) {
333             mStrokeAlphaAnimator.cancel();
334         }
335         mStrokeAlphaAnimator = ObjectAnimator
336                 .ofInt(this, STROKE_ALPHA, MAX_BG_OPACITY / 2, MAX_BG_OPACITY)
337                 .setDuration(100);
338         mStrokeAlphaAnimator.addListener(new AnimatorListenerAdapter() {
339             @Override
340             public void onAnimationEnd(Animator animation) {
341                 mStrokeAlphaAnimator = null;
342             }
343         });
344         mStrokeAlphaAnimator.start();
345     }
346 
drawBackgroundStroke(Canvas canvas)347     public void drawBackgroundStroke(Canvas canvas) {
348         if (!DRAW_STROKE) {
349             return;
350         }
351         mPaint.setColor(setColorAlphaBound(mStrokeColor, mStrokeAlpha));
352         mPaint.setStyle(Paint.Style.STROKE);
353         mPaint.setStrokeWidth(mStrokeWidth);
354 
355         float inset = 1f;
356         getShape().drawShape(canvas,
357                 getOffsetX() + inset, getOffsetY() + inset, getScaledRadius() - inset, mPaint);
358     }
359 
360     /**
361      * Draws the leave-behind circle on the given canvas and in the given color.
362      */
drawLeaveBehind(Canvas canvas, int color)363     public void drawLeaveBehind(Canvas canvas, int color) {
364         float originalScale = mScale;
365         mScale = 0.5f;
366 
367         mPaint.setStyle(Paint.Style.FILL);
368         mPaint.setColor(color);
369         getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
370 
371         mScale = originalScale;
372     }
373 
getClipPath()374     public Path getClipPath() {
375         mPath.reset();
376         float radius = getScaledRadius() * ICON_OVERLAP_FACTOR;
377         // Find the difference in radius so that the clip path remains centered.
378         float radiusDifference = radius - getRadius();
379         float offsetX = basePreviewOffsetX - radiusDifference;
380         float offsetY = basePreviewOffsetY - radiusDifference;
381         getShape().addToPath(mPath, offsetX, offsetY, radius);
382         return mPath;
383     }
384 
delegateDrawing(CellLayout delegate, int cellX, int cellY)385     private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
386         if (mDrawingDelegate != delegate) {
387             delegate.addDelegatedCellDrawing(this);
388         }
389 
390         mDrawingDelegate = delegate;
391         mDelegateCellX = cellX;
392         mDelegateCellY = cellY;
393 
394         invalidate();
395     }
396 
clearDrawingDelegate()397     private void clearDrawingDelegate() {
398         if (mDrawingDelegate != null) {
399             mDrawingDelegate.removeDelegatedCellDrawing(this);
400         }
401 
402         mDrawingDelegate = null;
403         isClipping = false;
404         invalidate();
405     }
406 
drawingDelegated()407     boolean drawingDelegated() {
408         return mDrawingDelegate != null;
409     }
410 
animateScale(boolean isAccepting, boolean isHovered)411     protected void animateScale(boolean isAccepting, boolean isHovered) {
412         if (mScaleAnimator != null) {
413             mScaleAnimator.cancel();
414         }
415 
416         final float startScale = mScale;
417         final float endScale = isAccepting ? ACCEPT_SCALE_FACTOR : (isHovered ? HOVER_SCALE : 1f);
418         Interpolator interpolator =
419                 isAccepting != mIsAccepting ? ACCELERATE_DECELERATE : EMPHASIZED_DECELERATE;
420         int duration = isAccepting != mIsAccepting ? CONSUMPTION_ANIMATION_DURATION
421                 : HOVER_ANIMATION_DURATION;
422         mIsAccepting = isAccepting;
423         mIsHovered = isHovered;
424         if (startScale == endScale) {
425             if (!mIsAccepting) {
426                 clearDrawingDelegate();
427             }
428             mIsHoveredOrAnimating = mIsHovered;
429             return;
430         }
431 
432 
433         mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f);
434         mScaleAnimator.addUpdateListener(animation -> {
435             float prog = animation.getAnimatedFraction();
436             mScale = prog * endScale + (1 - prog) * startScale;
437             invalidate();
438         });
439         mScaleAnimator.addListener(new AnimatorListenerAdapter() {
440             @Override
441             public void onAnimationStart(Animator animation) {
442                 if (mIsHovered) {
443                     mIsHoveredOrAnimating = true;
444                 }
445             }
446 
447             @Override
448             public void onAnimationEnd(Animator animation) {
449                 if (!mIsAccepting) {
450                     clearDrawingDelegate();
451                 }
452                 mIsHoveredOrAnimating = mIsHovered;
453                 mScaleAnimator = null;
454             }
455         });
456         mScaleAnimator.setInterpolator(interpolator);
457         mScaleAnimator.setDuration(duration);
458         mScaleAnimator.start();
459     }
460 
animateToAccept(CellLayout cl, int cellX, int cellY)461     public void animateToAccept(CellLayout cl, int cellX, int cellY) {
462         delegateDrawing(cl, cellX, cellY);
463         animateScale(/* isAccepting= */ true, mIsHovered);
464     }
465 
animateToRest()466     public void animateToRest() {
467         animateScale(/* isAccepting= */ false, mIsHovered);
468     }
469 
getStrokeWidth()470     public float getStrokeWidth() {
471         return mStrokeWidth;
472     }
473 
setHovered(boolean hovered)474     protected void setHovered(boolean hovered) {
475         animateScale(mIsAccepting, /* isHovered= */ hovered);
476     }
477 }
478