blob: fd4774f98277cd0d6e46414c5d843bc8ceb907a7 [file] [log] [blame]
Jon Mirandaa0233f72017-06-22 18:34:45 -07001/*
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
Sunny Goyald0ae4922018-10-11 10:37:53 -070019import static com.android.launcher3.folder.FolderShape.getShape;
Sunny Goyal066ace12018-11-05 11:08:31 -080020import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
Sunny Goyald0ae4922018-10-11 10:37:53 -070021
Jon Mirandaa0233f72017-06-22 18:34:45 -070022import android.animation.Animator;
23import android.animation.AnimatorListenerAdapter;
24import android.animation.ObjectAnimator;
25import android.animation.ValueAnimator;
Sunny Goyalab770a12018-11-14 15:17:26 -080026import android.content.Context;
Sunny Goyalbcadb7f2019-02-05 14:45:31 -080027import android.content.res.TypedArray;
Jon Mirandaa0233f72017-06-22 18:34:45 -070028import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Matrix;
31import android.graphics.Paint;
32import android.graphics.Path;
33import android.graphics.PorterDuff;
34import android.graphics.PorterDuffXfermode;
35import android.graphics.RadialGradient;
36import android.graphics.Region;
37import android.graphics.Shader;
Jon Mirandaa0233f72017-06-22 18:34:45 -070038import android.util.Property;
39import android.view.View;
40
41import com.android.launcher3.CellLayout;
42import com.android.launcher3.DeviceProfile;
Tony05d98c22018-07-23 08:01:15 -070043import com.android.launcher3.R;
Jon Mirandaa0233f72017-06-22 18:34:45 -070044import com.android.launcher3.util.Themes;
Sunny Goyalab770a12018-11-14 15:17:26 -080045import com.android.launcher3.views.ActivityContext;
Jon Mirandaa0233f72017-06-22 18:34:45 -070046
47/**
48 * This object represents a FolderIcon preview background. It stores drawing / measurement
49 * information, handles drawing, and animation (accept state <--> rest state).
50 */
51public class PreviewBackground {
52
53 private static final int CONSUMPTION_ANIMATION_DURATION = 100;
54
Jon Mirandaa0233f72017-06-22 18:34:45 -070055 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;
Sunny Goyalbcadb7f2019-02-05 14:45:31 -080067 private int mStrokeColor;
Tony Wickhamf34bee82018-12-03 18:11:39 -080068 private int mDotColor;
Jon Mirandaa0233f72017-06-22 18:34:45 -070069 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
Jon Miranda72b5fd12017-06-25 18:06:23 -070087 private static final float ACCEPT_SCALE_FACTOR = 1.20f;
Jon Mirandaa0233f72017-06-22 18:34:45 -070088 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
Sunny Goyalab770a12018-11-14 15:17:26 -0800127 public void setup(Context context, ActivityContext activity, View invalidateDelegate,
Jon Miranda591e3602018-03-28 11:37:00 -0700128 int availableSpaceX, int topPadding) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700129 mInvalidateDelegate = invalidateDelegate;
Sunny Goyalbcadb7f2019-02-05 14:45:31 -0800130
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_android_colorPrimary, 0);
135 ta.recycle();
Jon Mirandaa0233f72017-06-22 18:34:45 -0700136
Sunny Goyalab770a12018-11-14 15:17:26 -0800137 DeviceProfile grid = activity.getDeviceProfile();
Jon Miranda591e3602018-03-28 11:37:00 -0700138 previewSize = grid.folderIconSizePx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700139
Jon Miranda591e3602018-03-28 11:37:00 -0700140 basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
141 basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700142
143 // Stroke width is 1dp
Sunny Goyalab770a12018-11-14 15:17:26 -0800144 mStrokeWidth = context.getResources().getDisplayMetrics().density;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700145
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
157 int getRadius() {
158 return previewSize / 2;
159 }
160
161 int getScaledRadius() {
162 return (int) (mScale * getRadius());
163 }
164
165 int getOffsetX() {
166 return basePreviewOffsetX - (getScaledRadius() - getRadius());
167 }
168
169 int getOffsetY() {
170 return basePreviewOffsetY - (getScaledRadius() - getRadius());
171 }
172
173 /**
174 * Returns the progress of the scale animation, where 0 means the scale is at 1f
175 * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
176 */
177 float getScaleProgress() {
178 return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
179 }
180
181 void invalidate() {
182 if (mInvalidateDelegate != null) {
183 mInvalidateDelegate.invalidate();
184 }
185
186 if (mDrawingDelegate != null) {
187 mDrawingDelegate.invalidate();
188 }
189 }
190
191 void setInvalidateDelegate(View invalidateDelegate) {
192 mInvalidateDelegate = invalidateDelegate;
193 invalidate();
194 }
195
Sunny Goyale29897f2017-07-20 10:09:42 -0700196 public int getBgColor() {
197 int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
Sunny Goyal066ace12018-11-05 11:08:31 -0800198 return setColorAlphaBound(mBgColor, alpha);
Sunny Goyale29897f2017-07-20 10:09:42 -0700199 }
200
Tony Wickhamf34bee82018-12-03 18:11:39 -0800201 public int getDotColor() {
202 return mDotColor;
Sunny Goyal179249d2017-12-19 16:49:24 -0800203 }
204
Jon Mirandaa0233f72017-06-22 18:34:45 -0700205 public void drawBackground(Canvas canvas) {
206 mPaint.setStyle(Paint.Style.FILL);
Sunny Goyale29897f2017-07-20 10:09:42 -0700207 mPaint.setColor(getBgColor());
Jon Mirandaa0233f72017-06-22 18:34:45 -0700208
Sunny Goyald0ae4922018-10-11 10:37:53 -0700209 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
Sunny Goyale29897f2017-07-20 10:09:42 -0700210 drawShadow(canvas);
211 }
212
213 public void drawShadow(Canvas canvas) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700214 if (mShadowShader == null) {
215 return;
216 }
Sunny Goyale29897f2017-07-20 10:09:42 -0700217
Jon Mirandaa0233f72017-06-22 18:34:45 -0700218 float radius = getScaledRadius();
219 float shadowRadius = radius + mStrokeWidth;
Sunny Goyale29897f2017-07-20 10:09:42 -0700220 mPaint.setStyle(Paint.Style.FILL);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700221 mPaint.setColor(Color.BLACK);
222 int offsetX = getOffsetX();
223 int offsetY = getOffsetY();
224 final int saveCount;
225 if (canvas.isHardwareAccelerated()) {
226 saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500227 offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, null);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700228
229 } else {
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500230 saveCount = canvas.save();
Sunny Goyale29897f2017-07-20 10:09:42 -0700231 canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700232 }
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);
Sunny Goyald0ae4922018-10-11 10:37:53 -0700244 getShape().drawShape(canvas, offsetX, offsetY, radius, mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700245 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) {
Sunny Goyalbcadb7f2019-02-05 14:45:31 -0800284 mPaint.setColor(setColorAlphaBound(mStrokeColor, mStrokeAlpha));
Jon Mirandaa0233f72017-06-22 18:34:45 -0700285 mPaint.setStyle(Paint.Style.STROKE);
286 mPaint.setStrokeWidth(mStrokeWidth);
Sunny Goyald0ae4922018-10-11 10:37:53 -0700287
288 float inset = 1f;
289 getShape().drawShape(canvas,
290 getOffsetX() + inset, getOffsetY() + inset, getScaledRadius() - inset, mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700291 }
292
293 public void drawLeaveBehind(Canvas canvas) {
294 float originalScale = mScale;
295 mScale = 0.5f;
296
297 mPaint.setStyle(Paint.Style.FILL);
298 mPaint.setColor(Color.argb(160, 245, 245, 245));
Sunny Goyald0ae4922018-10-11 10:37:53 -0700299 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700300
301 mScale = originalScale;
302 }
303
Sunny Goyale29897f2017-07-20 10:09:42 -0700304 public Path getClipPath() {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700305 mPath.reset();
Sunny Goyald0ae4922018-10-11 10:37:53 -0700306 getShape().addShape(mPath, getOffsetX(), getOffsetY(), getScaledRadius());
Sunny Goyale29897f2017-07-20 10:09:42 -0700307 return mPath;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700308 }
309
Jon Mirandaa0233f72017-06-22 18:34:45 -0700310 private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
311 if (mDrawingDelegate != delegate) {
312 delegate.addFolderBackground(this);
313 }
314
315 mDrawingDelegate = delegate;
316 delegateCellX = cellX;
317 delegateCellY = cellY;
318
319 invalidate();
320 }
321
322 private void clearDrawingDelegate() {
323 if (mDrawingDelegate != null) {
324 mDrawingDelegate.removeFolderBackground(this);
325 }
326
327 mDrawingDelegate = null;
Sunny Goyal5247f5b2017-07-04 12:07:46 -0700328 isClipping = true;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700329 invalidate();
330 }
331
332 boolean drawingDelegated() {
333 return mDrawingDelegate != null;
334 }
335
336 private void animateScale(float finalScale, float finalMultiplier,
337 final Runnable onStart, final Runnable onEnd) {
338 final float scale0 = mScale;
339 final float scale1 = finalScale;
340
341 final float bgMultiplier0 = mColorMultiplier;
342 final float bgMultiplier1 = finalMultiplier;
343
344 if (mScaleAnimator != null) {
345 mScaleAnimator.cancel();
346 }
347
Sunny Goyal849c6a22018-08-08 16:33:46 -0700348 mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700349
350 mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
351 @Override
352 public void onAnimationUpdate(ValueAnimator animation) {
353 float prog = animation.getAnimatedFraction();
354 mScale = prog * scale1 + (1 - prog) * scale0;
355 mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
356 invalidate();
357 }
358 });
359 mScaleAnimator.addListener(new AnimatorListenerAdapter() {
360 @Override
361 public void onAnimationStart(Animator animation) {
362 if (onStart != null) {
363 onStart.run();
364 }
365 }
366
367 @Override
368 public void onAnimationEnd(Animator animation) {
369 if (onEnd != null) {
370 onEnd.run();
371 }
372 mScaleAnimator = null;
373 }
374 });
375
376 mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
377 mScaleAnimator.start();
378 }
379
Sunny Goyal849c6a22018-08-08 16:33:46 -0700380 public void animateToAccept(CellLayout cl, int cellX, int cellY) {
381 animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER,
382 () -> delegateDrawing(cl, cellX, cellY), null);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700383 }
384
385 public void animateToRest() {
386 // This can be called multiple times -- we need to make sure the drawing delegate
387 // is saved and restored at the beginning of the animation, since cancelling the
388 // existing animation can clear the delgate.
Sunny Goyal849c6a22018-08-08 16:33:46 -0700389 CellLayout cl = mDrawingDelegate;
390 int cellX = delegateCellX;
391 int cellY = delegateCellY;
392 animateScale(1f, 1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700393 }
394
395 public int getBackgroundAlpha() {
396 return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
397 }
398
399 public float getStrokeWidth() {
400 return mStrokeWidth;
401 }
402}