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