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