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
17package com.android.launcher3.folder;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.Matrix;
26import android.graphics.Paint;
27import android.graphics.Path;
28import android.graphics.PorterDuff;
29import android.graphics.PorterDuffXfermode;
30import android.graphics.RadialGradient;
31import android.graphics.Region;
32import android.graphics.Shader;
33import android.support.v4.graphics.ColorUtils;
34import android.util.Property;
35import android.view.View;
36
37import com.android.launcher3.CellLayout;
38import com.android.launcher3.DeviceProfile;
39import com.android.launcher3.Launcher;
40import com.android.launcher3.LauncherAnimUtils;
41import com.android.launcher3.util.Themes;
42
43/**
44 * This object represents a FolderIcon preview background. It stores drawing / measurement
45 * information, handles drawing, and animation (accept state <--> rest state).
46 */
47public class PreviewBackground {
48
49    private static final int CONSUMPTION_ANIMATION_DURATION = 100;
50
51    private final PorterDuffXfermode mClipPorterDuffXfermode
52            = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
53    // Create a RadialGradient such that it draws a black circle and then extends with
54    // transparent. To achieve this, we keep the gradient to black for the range [0, 1) and
55    // just at the edge quickly change it to transparent.
56    private final RadialGradient mClipShader = new RadialGradient(0, 0, 1,
57            new int[] {Color.BLACK, Color.BLACK, Color.TRANSPARENT },
58            new float[] {0, 0.999f, 1},
59            Shader.TileMode.CLAMP);
60
61    private final PorterDuffXfermode mShadowPorterDuffXfermode
62            = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
63    private RadialGradient mShadowShader = null;
64
65    private final Matrix mShaderMatrix = new Matrix();
66    private final Path mPath = new Path();
67
68    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
69
70    float mScale = 1f;
71    private float mColorMultiplier = 1f;
72    private int mBgColor;
73    private float mStrokeWidth;
74    private int mStrokeAlpha = MAX_BG_OPACITY;
75    private int mShadowAlpha = 255;
76    private View mInvalidateDelegate;
77
78    int previewSize;
79    int basePreviewOffsetX;
80    int basePreviewOffsetY;
81
82    private CellLayout mDrawingDelegate;
83    public int delegateCellX;
84    public int delegateCellY;
85
86    // When the PreviewBackground is drawn under an icon (for creating a folder) the border
87    // should not occlude the icon
88    public boolean isClipping = true;
89
90    // Drawing / animation configurations
91    private static final float ACCEPT_SCALE_FACTOR = 1.20f;
92    private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
93
94    // Expressed on a scale from 0 to 255.
95    private static final int BG_OPACITY = 160;
96    private static final int MAX_BG_OPACITY = 225;
97    private static final int SHADOW_OPACITY = 40;
98
99    private ValueAnimator mScaleAnimator;
100    private ObjectAnimator mStrokeAlphaAnimator;
101    private ObjectAnimator mShadowAnimator;
102
103    private static final Property<PreviewBackground, Integer> STROKE_ALPHA =
104            new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") {
105                @Override
106                public Integer get(PreviewBackground previewBackground) {
107                    return previewBackground.mStrokeAlpha;
108                }
109
110                @Override
111                public void set(PreviewBackground previewBackground, Integer alpha) {
112                    previewBackground.mStrokeAlpha = alpha;
113                    previewBackground.invalidate();
114                }
115            };
116
117    private static final Property<PreviewBackground, Integer> SHADOW_ALPHA =
118            new Property<PreviewBackground, Integer>(Integer.class, "shadowAlpha") {
119                @Override
120                public Integer get(PreviewBackground previewBackground) {
121                    return previewBackground.mShadowAlpha;
122                }
123
124                @Override
125                public void set(PreviewBackground previewBackground, Integer alpha) {
126                    previewBackground.mShadowAlpha = alpha;
127                    previewBackground.invalidate();
128                }
129            };
130
131    public void setup(Launcher launcher, View invalidateDelegate,
132                      int availableSpace, int topPadding) {
133        mInvalidateDelegate = invalidateDelegate;
134        mBgColor = Themes.getAttrColor(launcher, android.R.attr.colorPrimary);
135
136        DeviceProfile grid = launcher.getDeviceProfile();
137        final int previewSize = grid.folderIconSizePx;
138        final int previewPadding = grid.folderIconPreviewPadding;
139
140        this.previewSize = (previewSize - 2 * previewPadding);
141
142        basePreviewOffsetX = (availableSpace - this.previewSize) / 2;
143        basePreviewOffsetY = previewPadding + grid.folderBackgroundOffset + topPadding;
144
145        // Stroke width is 1dp
146        mStrokeWidth = launcher.getResources().getDisplayMetrics().density;
147
148        float radius = getScaledRadius();
149        float shadowRadius = radius + mStrokeWidth;
150        int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
151        mShadowShader = new RadialGradient(0, 0, 1,
152                new int[] {shadowColor, Color.TRANSPARENT},
153                new float[] {radius / shadowRadius, 1},
154                Shader.TileMode.CLAMP);
155
156        invalidate();
157    }
158
159    int getRadius() {
160        return previewSize / 2;
161    }
162
163    int getScaledRadius() {
164        return (int) (mScale * getRadius());
165    }
166
167    int getOffsetX() {
168        return basePreviewOffsetX - (getScaledRadius() - getRadius());
169    }
170
171    int getOffsetY() {
172        return basePreviewOffsetY - (getScaledRadius() - getRadius());
173    }
174
175    /**
176     * Returns the progress of the scale animation, where 0 means the scale is at 1f
177     * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
178     */
179    float getScaleProgress() {
180        return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
181    }
182
183    void invalidate() {
184        if (mInvalidateDelegate != null) {
185            mInvalidateDelegate.invalidate();
186        }
187
188        if (mDrawingDelegate != null) {
189            mDrawingDelegate.invalidate();
190        }
191    }
192
193    void setInvalidateDelegate(View invalidateDelegate) {
194        mInvalidateDelegate = invalidateDelegate;
195        invalidate();
196    }
197
198    public int getBgColor() {
199        int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
200        return ColorUtils.setAlphaComponent(mBgColor, alpha);
201    }
202
203    public void drawBackground(Canvas canvas) {
204        mPaint.setStyle(Paint.Style.FILL);
205        mPaint.setColor(getBgColor());
206
207        drawCircle(canvas, 0 /* deltaRadius */);
208
209        drawShadow(canvas);
210    }
211
212    public void drawShadow(Canvas canvas) {
213        if (mShadowShader == null) {
214            return;
215        }
216
217        float radius = getScaledRadius();
218        float shadowRadius = radius + mStrokeWidth;
219        mPaint.setStyle(Paint.Style.FILL);
220        mPaint.setColor(Color.BLACK);
221        int offsetX = getOffsetX();
222        int offsetY = getOffsetY();
223        final int saveCount;
224        if (canvas.isHardwareAccelerated()) {
225            saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
226                    offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius,
227                    null, Canvas.CLIP_TO_LAYER_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG);
228
229        } else {
230            saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
231            canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
232        }
233
234        mShaderMatrix.setScale(shadowRadius, shadowRadius);
235        mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
236        mShadowShader.setLocalMatrix(mShaderMatrix);
237        mPaint.setAlpha(mShadowAlpha);
238        mPaint.setShader(mShadowShader);
239        canvas.drawPaint(mPaint);
240        mPaint.setAlpha(255);
241        mPaint.setShader(null);
242        if (canvas.isHardwareAccelerated()) {
243            mPaint.setXfermode(mShadowPorterDuffXfermode);
244            canvas.drawCircle(radius + offsetX, radius + offsetY, radius, mPaint);
245            mPaint.setXfermode(null);
246        }
247
248        canvas.restoreToCount(saveCount);
249    }
250
251    public void fadeInBackgroundShadow() {
252        if (mShadowAnimator != null) {
253            mShadowAnimator.cancel();
254        }
255        mShadowAnimator = ObjectAnimator
256                .ofInt(this, SHADOW_ALPHA, 0, 255)
257                .setDuration(100);
258        mShadowAnimator.addListener(new AnimatorListenerAdapter() {
259            @Override
260            public void onAnimationEnd(Animator animation) {
261                mShadowAnimator = null;
262            }
263        });
264        mShadowAnimator.start();
265    }
266
267    public void animateBackgroundStroke() {
268        if (mStrokeAlphaAnimator != null) {
269            mStrokeAlphaAnimator.cancel();
270        }
271        mStrokeAlphaAnimator = ObjectAnimator
272                .ofInt(this, STROKE_ALPHA, MAX_BG_OPACITY / 2, MAX_BG_OPACITY)
273                .setDuration(100);
274        mStrokeAlphaAnimator.addListener(new AnimatorListenerAdapter() {
275            @Override
276            public void onAnimationEnd(Animator animation) {
277                mStrokeAlphaAnimator = null;
278            }
279        });
280        mStrokeAlphaAnimator.start();
281    }
282
283    public void drawBackgroundStroke(Canvas canvas) {
284        mPaint.setColor(ColorUtils.setAlphaComponent(mBgColor, mStrokeAlpha));
285        mPaint.setStyle(Paint.Style.STROKE);
286        mPaint.setStrokeWidth(mStrokeWidth);
287        drawCircle(canvas, 1 /* deltaRadius */);
288    }
289
290    public void drawLeaveBehind(Canvas canvas) {
291        float originalScale = mScale;
292        mScale = 0.5f;
293
294        mPaint.setStyle(Paint.Style.FILL);
295        mPaint.setColor(Color.argb(160, 245, 245, 245));
296        drawCircle(canvas, 0 /* deltaRadius */);
297
298        mScale = originalScale;
299    }
300
301    private void drawCircle(Canvas canvas,float deltaRadius) {
302        float radius = getScaledRadius();
303        canvas.drawCircle(radius + getOffsetX(), radius + getOffsetY(),
304                radius - deltaRadius, mPaint);
305    }
306
307    public Path getClipPath() {
308        mPath.reset();
309        float r = getScaledRadius();
310        mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW);
311        return mPath;
312    }
313
314    // It is the callers responsibility to save and restore the canvas layers.
315    void clipCanvasHardware(Canvas canvas) {
316        mPaint.setColor(Color.BLACK);
317        mPaint.setStyle(Paint.Style.FILL);
318        mPaint.setXfermode(mClipPorterDuffXfermode);
319
320        float radius = getScaledRadius();
321        mShaderMatrix.setScale(radius, radius);
322        mShaderMatrix.postTranslate(radius + getOffsetX(), radius + getOffsetY());
323        mClipShader.setLocalMatrix(mShaderMatrix);
324        mPaint.setShader(mClipShader);
325        canvas.drawPaint(mPaint);
326        mPaint.setXfermode(null);
327        mPaint.setShader(null);
328    }
329
330    private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
331        if (mDrawingDelegate != delegate) {
332            delegate.addFolderBackground(this);
333        }
334
335        mDrawingDelegate = delegate;
336        delegateCellX = cellX;
337        delegateCellY = cellY;
338
339        invalidate();
340    }
341
342    private void clearDrawingDelegate() {
343        if (mDrawingDelegate != null) {
344            mDrawingDelegate.removeFolderBackground(this);
345        }
346
347        mDrawingDelegate = null;
348        isClipping = true;
349        invalidate();
350    }
351
352    boolean drawingDelegated() {
353        return mDrawingDelegate != null;
354    }
355
356    private void animateScale(float finalScale, float finalMultiplier,
357                              final Runnable onStart, final Runnable onEnd) {
358        final float scale0 = mScale;
359        final float scale1 = finalScale;
360
361        final float bgMultiplier0 = mColorMultiplier;
362        final float bgMultiplier1 = finalMultiplier;
363
364        if (mScaleAnimator != null) {
365            mScaleAnimator.cancel();
366        }
367
368        mScaleAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f);
369
370        mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
371            @Override
372            public void onAnimationUpdate(ValueAnimator animation) {
373                float prog = animation.getAnimatedFraction();
374                mScale = prog * scale1 + (1 - prog) * scale0;
375                mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
376                invalidate();
377            }
378        });
379        mScaleAnimator.addListener(new AnimatorListenerAdapter() {
380            @Override
381            public void onAnimationStart(Animator animation) {
382                if (onStart != null) {
383                    onStart.run();
384                }
385            }
386
387            @Override
388            public void onAnimationEnd(Animator animation) {
389                if (onEnd != null) {
390                    onEnd.run();
391                }
392                mScaleAnimator = null;
393            }
394        });
395
396        mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
397        mScaleAnimator.start();
398    }
399
400    public void animateToAccept(final CellLayout cl, final int cellX, final int cellY) {
401        Runnable onStart = new Runnable() {
402            @Override
403            public void run() {
404                delegateDrawing(cl, cellX, cellY);
405            }
406        };
407        animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER, onStart, null);
408    }
409
410    public void animateToRest() {
411        // This can be called multiple times -- we need to make sure the drawing delegate
412        // is saved and restored at the beginning of the animation, since cancelling the
413        // existing animation can clear the delgate.
414        final CellLayout cl = mDrawingDelegate;
415        final int cellX = delegateCellX;
416        final int cellY = delegateCellY;
417
418        Runnable onStart = new Runnable() {
419            @Override
420            public void run() {
421                delegateDrawing(cl, cellX, cellY);
422            }
423        };
424        Runnable onEnd = new Runnable() {
425            @Override
426            public void run() {
427                clearDrawingDelegate();
428            }
429        };
430        animateScale(1f, 1f, onStart, onEnd);
431    }
432
433    public int getBackgroundAlpha() {
434        return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
435    }
436
437    public float getStrokeWidth() {
438        return mStrokeWidth;
439    }
440}
441