blob: 27b906bcbb0c4daf8cde4b7c8e27c02ef53202e6 [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 Goyal905262c2019-05-03 16:50:43 -070019import static com.android.launcher3.graphics.IconShape.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;
Jon Miranda69b35e02019-03-04 20:16:18 -080036import android.graphics.Rect;
Jon Mirandaa0233f72017-06-22 18:34:45 -070037import android.graphics.Region;
38import android.graphics.Shader;
Jon Mirandaa0233f72017-06-22 18:34:45 -070039import android.util.Property;
40import android.view.View;
41
42import com.android.launcher3.CellLayout;
43import com.android.launcher3.DeviceProfile;
Tony05d98c22018-07-23 08:01:15 -070044import com.android.launcher3.R;
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 */
Samuel Fufa1e2d0042019-11-18 17:12:46 -080051public class PreviewBackground extends CellLayout.DelegatedCellDrawing {
Jon Mirandaa0233f72017-06-22 18:34:45 -070052
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;
Jon Mirandaa0233f72017-06-22 18:34:45 -070079
80 // When the PreviewBackground is drawn under an icon (for creating a folder) the border
81 // should not occlude the icon
82 public boolean isClipping = true;
83
84 // Drawing / animation configurations
Jon Miranda72b5fd12017-06-25 18:06:23 -070085 private static final float ACCEPT_SCALE_FACTOR = 1.20f;
Jon Mirandaa0233f72017-06-22 18:34:45 -070086 private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
87
88 // Expressed on a scale from 0 to 255.
89 private static final int BG_OPACITY = 160;
90 private static final int MAX_BG_OPACITY = 225;
91 private static final int SHADOW_OPACITY = 40;
92
93 private ValueAnimator mScaleAnimator;
94 private ObjectAnimator mStrokeAlphaAnimator;
95 private ObjectAnimator mShadowAnimator;
96
97 private static final Property<PreviewBackground, Integer> STROKE_ALPHA =
98 new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") {
99 @Override
100 public Integer get(PreviewBackground previewBackground) {
101 return previewBackground.mStrokeAlpha;
102 }
103
104 @Override
105 public void set(PreviewBackground previewBackground, Integer alpha) {
106 previewBackground.mStrokeAlpha = alpha;
107 previewBackground.invalidate();
108 }
109 };
110
111 private static final Property<PreviewBackground, Integer> SHADOW_ALPHA =
112 new Property<PreviewBackground, Integer>(Integer.class, "shadowAlpha") {
113 @Override
114 public Integer get(PreviewBackground previewBackground) {
115 return previewBackground.mShadowAlpha;
116 }
117
118 @Override
119 public void set(PreviewBackground previewBackground, Integer alpha) {
120 previewBackground.mShadowAlpha = alpha;
121 previewBackground.invalidate();
122 }
123 };
124
Samuel Fufa1e2d0042019-11-18 17:12:46 -0800125 /**
126 * Draws folder background under cell layout
127 */
128 @Override
129 public void drawUnderItem(Canvas canvas) {
130 drawBackground(canvas);
131 if (!isClipping) {
132 drawBackgroundStroke(canvas);
133 }
134 }
135
136 /**
137 * Draws folder background on cell layout
138 */
139 @Override
140 public void drawOverItem(Canvas canvas) {
141 if (isClipping) {
142 drawBackgroundStroke(canvas);
143 }
144 }
145
Sunny Goyalab770a12018-11-14 15:17:26 -0800146 public void setup(Context context, ActivityContext activity, View invalidateDelegate,
Jon Miranda591e3602018-03-28 11:37:00 -0700147 int availableSpaceX, int topPadding) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700148 mInvalidateDelegate = invalidateDelegate;
Sunny Goyalbcadb7f2019-02-05 14:45:31 -0800149
150 TypedArray ta = context.getTheme().obtainStyledAttributes(R.styleable.FolderIconPreview);
151 mDotColor = ta.getColor(R.styleable.FolderIconPreview_folderDotColor, 0);
152 mStrokeColor = ta.getColor(R.styleable.FolderIconPreview_folderIconBorderColor, 0);
Hyunyoung Songe751d902019-05-13 23:23:05 -0700153 mBgColor = ta.getColor(R.styleable.FolderIconPreview_folderFillColor, 0);
Sunny Goyalbcadb7f2019-02-05 14:45:31 -0800154 ta.recycle();
Jon Mirandaa0233f72017-06-22 18:34:45 -0700155
Sunny Goyalc4d32012020-04-03 17:10:11 -0700156 DeviceProfile grid = activity.getDeviceProfile();
Jon Miranda591e3602018-03-28 11:37:00 -0700157 previewSize = grid.folderIconSizePx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700158
Jon Miranda591e3602018-03-28 11:37:00 -0700159 basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
160 basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700161
162 // Stroke width is 1dp
Sunny Goyalab770a12018-11-14 15:17:26 -0800163 mStrokeWidth = context.getResources().getDisplayMetrics().density;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700164
165 float radius = getScaledRadius();
166 float shadowRadius = radius + mStrokeWidth;
167 int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
168 mShadowShader = new RadialGradient(0, 0, 1,
169 new int[] {shadowColor, Color.TRANSPARENT},
170 new float[] {radius / shadowRadius, 1},
171 Shader.TileMode.CLAMP);
172
173 invalidate();
174 }
175
Jon Miranda69b35e02019-03-04 20:16:18 -0800176 void getBounds(Rect outBounds) {
177 int top = basePreviewOffsetY;
178 int left = basePreviewOffsetX;
179 int right = left + previewSize;
180 int bottom = top + previewSize;
181 outBounds.set(left, top, right, bottom);
182 }
183
Jon Mirandaa0233f72017-06-22 18:34:45 -0700184 int getRadius() {
185 return previewSize / 2;
186 }
187
188 int getScaledRadius() {
189 return (int) (mScale * getRadius());
190 }
191
192 int getOffsetX() {
193 return basePreviewOffsetX - (getScaledRadius() - getRadius());
194 }
195
196 int getOffsetY() {
197 return basePreviewOffsetY - (getScaledRadius() - getRadius());
198 }
199
200 /**
201 * Returns the progress of the scale animation, where 0 means the scale is at 1f
202 * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
203 */
204 float getScaleProgress() {
205 return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
206 }
207
208 void invalidate() {
209 if (mInvalidateDelegate != null) {
210 mInvalidateDelegate.invalidate();
211 }
212
213 if (mDrawingDelegate != null) {
214 mDrawingDelegate.invalidate();
215 }
216 }
217
218 void setInvalidateDelegate(View invalidateDelegate) {
219 mInvalidateDelegate = invalidateDelegate;
220 invalidate();
221 }
222
Sunny Goyale29897f2017-07-20 10:09:42 -0700223 public int getBgColor() {
224 int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
Sunny Goyal066ace12018-11-05 11:08:31 -0800225 return setColorAlphaBound(mBgColor, alpha);
Sunny Goyale29897f2017-07-20 10:09:42 -0700226 }
227
Tony Wickhamf34bee82018-12-03 18:11:39 -0800228 public int getDotColor() {
229 return mDotColor;
Sunny Goyal179249d2017-12-19 16:49:24 -0800230 }
231
Jon Mirandaa0233f72017-06-22 18:34:45 -0700232 public void drawBackground(Canvas canvas) {
233 mPaint.setStyle(Paint.Style.FILL);
Sunny Goyale29897f2017-07-20 10:09:42 -0700234 mPaint.setColor(getBgColor());
Jon Mirandaa0233f72017-06-22 18:34:45 -0700235
Sunny Goyald0ae4922018-10-11 10:37:53 -0700236 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
Sunny Goyale29897f2017-07-20 10:09:42 -0700237 drawShadow(canvas);
238 }
239
240 public void drawShadow(Canvas canvas) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700241 if (mShadowShader == null) {
242 return;
243 }
Sunny Goyale29897f2017-07-20 10:09:42 -0700244
Jon Mirandaa0233f72017-06-22 18:34:45 -0700245 float radius = getScaledRadius();
246 float shadowRadius = radius + mStrokeWidth;
Sunny Goyale29897f2017-07-20 10:09:42 -0700247 mPaint.setStyle(Paint.Style.FILL);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700248 mPaint.setColor(Color.BLACK);
249 int offsetX = getOffsetX();
250 int offsetY = getOffsetY();
251 final int saveCount;
252 if (canvas.isHardwareAccelerated()) {
253 saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500254 offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, null);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700255
256 } else {
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500257 saveCount = canvas.save();
Sunny Goyale29897f2017-07-20 10:09:42 -0700258 canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700259 }
260
261 mShaderMatrix.setScale(shadowRadius, shadowRadius);
262 mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
263 mShadowShader.setLocalMatrix(mShaderMatrix);
264 mPaint.setAlpha(mShadowAlpha);
265 mPaint.setShader(mShadowShader);
266 canvas.drawPaint(mPaint);
267 mPaint.setAlpha(255);
268 mPaint.setShader(null);
269 if (canvas.isHardwareAccelerated()) {
270 mPaint.setXfermode(mShadowPorterDuffXfermode);
Sunny Goyald0ae4922018-10-11 10:37:53 -0700271 getShape().drawShape(canvas, offsetX, offsetY, radius, mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700272 mPaint.setXfermode(null);
273 }
274
275 canvas.restoreToCount(saveCount);
276 }
277
278 public void fadeInBackgroundShadow() {
279 if (mShadowAnimator != null) {
280 mShadowAnimator.cancel();
281 }
282 mShadowAnimator = ObjectAnimator
283 .ofInt(this, SHADOW_ALPHA, 0, 255)
284 .setDuration(100);
285 mShadowAnimator.addListener(new AnimatorListenerAdapter() {
286 @Override
287 public void onAnimationEnd(Animator animation) {
288 mShadowAnimator = null;
289 }
290 });
291 mShadowAnimator.start();
292 }
293
294 public void animateBackgroundStroke() {
295 if (mStrokeAlphaAnimator != null) {
296 mStrokeAlphaAnimator.cancel();
297 }
298 mStrokeAlphaAnimator = ObjectAnimator
299 .ofInt(this, STROKE_ALPHA, MAX_BG_OPACITY / 2, MAX_BG_OPACITY)
300 .setDuration(100);
301 mStrokeAlphaAnimator.addListener(new AnimatorListenerAdapter() {
302 @Override
303 public void onAnimationEnd(Animator animation) {
304 mStrokeAlphaAnimator = null;
305 }
306 });
307 mStrokeAlphaAnimator.start();
308 }
309
310 public void drawBackgroundStroke(Canvas canvas) {
Sunny Goyalbcadb7f2019-02-05 14:45:31 -0800311 mPaint.setColor(setColorAlphaBound(mStrokeColor, mStrokeAlpha));
Jon Mirandaa0233f72017-06-22 18:34:45 -0700312 mPaint.setStyle(Paint.Style.STROKE);
313 mPaint.setStrokeWidth(mStrokeWidth);
Sunny Goyald0ae4922018-10-11 10:37:53 -0700314
315 float inset = 1f;
316 getShape().drawShape(canvas,
317 getOffsetX() + inset, getOffsetY() + inset, getScaledRadius() - inset, mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700318 }
319
320 public void drawLeaveBehind(Canvas canvas) {
321 float originalScale = mScale;
322 mScale = 0.5f;
323
324 mPaint.setStyle(Paint.Style.FILL);
325 mPaint.setColor(Color.argb(160, 245, 245, 245));
Sunny Goyald0ae4922018-10-11 10:37:53 -0700326 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700327
328 mScale = originalScale;
329 }
330
Sunny Goyale29897f2017-07-20 10:09:42 -0700331 public Path getClipPath() {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700332 mPath.reset();
Sunny Goyal905262c2019-05-03 16:50:43 -0700333 getShape().addToPath(mPath, getOffsetX(), getOffsetY(), getScaledRadius());
Sunny Goyale29897f2017-07-20 10:09:42 -0700334 return mPath;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700335 }
336
Jon Mirandaa0233f72017-06-22 18:34:45 -0700337 private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
338 if (mDrawingDelegate != delegate) {
Samuel Fufa1e2d0042019-11-18 17:12:46 -0800339 delegate.addDelegatedCellDrawing(this);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700340 }
341
342 mDrawingDelegate = delegate;
Samuel Fufa1e2d0042019-11-18 17:12:46 -0800343 mDelegateCellX = cellX;
344 mDelegateCellY = cellY;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700345
346 invalidate();
347 }
348
349 private void clearDrawingDelegate() {
350 if (mDrawingDelegate != null) {
Samuel Fufa1e2d0042019-11-18 17:12:46 -0800351 mDrawingDelegate.removeDelegatedCellDrawing(this);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700352 }
353
354 mDrawingDelegate = null;
Sunny Goyal5247f5b2017-07-04 12:07:46 -0700355 isClipping = true;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700356 invalidate();
357 }
358
359 boolean drawingDelegated() {
360 return mDrawingDelegate != null;
361 }
362
363 private void animateScale(float finalScale, float finalMultiplier,
364 final Runnable onStart, final Runnable onEnd) {
365 final float scale0 = mScale;
366 final float scale1 = finalScale;
367
368 final float bgMultiplier0 = mColorMultiplier;
369 final float bgMultiplier1 = finalMultiplier;
370
371 if (mScaleAnimator != null) {
372 mScaleAnimator.cancel();
373 }
374
Sunny Goyal849c6a22018-08-08 16:33:46 -0700375 mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700376
377 mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
378 @Override
379 public void onAnimationUpdate(ValueAnimator animation) {
380 float prog = animation.getAnimatedFraction();
381 mScale = prog * scale1 + (1 - prog) * scale0;
382 mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
383 invalidate();
384 }
385 });
386 mScaleAnimator.addListener(new AnimatorListenerAdapter() {
387 @Override
388 public void onAnimationStart(Animator animation) {
389 if (onStart != null) {
390 onStart.run();
391 }
392 }
393
394 @Override
395 public void onAnimationEnd(Animator animation) {
396 if (onEnd != null) {
397 onEnd.run();
398 }
399 mScaleAnimator = null;
400 }
401 });
402
403 mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
404 mScaleAnimator.start();
405 }
406
Sunny Goyal849c6a22018-08-08 16:33:46 -0700407 public void animateToAccept(CellLayout cl, int cellX, int cellY) {
408 animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER,
409 () -> delegateDrawing(cl, cellX, cellY), null);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700410 }
411
412 public void animateToRest() {
413 // This can be called multiple times -- we need to make sure the drawing delegate
414 // is saved and restored at the beginning of the animation, since cancelling the
415 // existing animation can clear the delgate.
Sunny Goyal849c6a22018-08-08 16:33:46 -0700416 CellLayout cl = mDrawingDelegate;
Samuel Fufa1e2d0042019-11-18 17:12:46 -0800417 int cellX = mDelegateCellX;
418 int cellY = mDelegateCellY;
Sunny Goyal849c6a22018-08-08 16:33:46 -0700419 animateScale(1f, 1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700420 }
421
422 public int getBackgroundAlpha() {
423 return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
424 }
425
426 public float getStrokeWidth() {
427 return mStrokeWidth;
428 }
429}