blob: 8443953bd99f7a35af20cfad61d1997bfa2dbad7 [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;
20
Jon Mirandaa0233f72017-06-22 18:34:45 -070021import android.animation.Animator;
22import android.animation.AnimatorListenerAdapter;
23import android.animation.ObjectAnimator;
24import android.animation.ValueAnimator;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Matrix;
28import android.graphics.Paint;
29import android.graphics.Path;
30import android.graphics.PorterDuff;
31import android.graphics.PorterDuffXfermode;
32import android.graphics.RadialGradient;
33import android.graphics.Region;
34import android.graphics.Shader;
Jon Mirandaa0233f72017-06-22 18:34:45 -070035import android.util.Property;
36import android.view.View;
37
38import com.android.launcher3.CellLayout;
39import com.android.launcher3.DeviceProfile;
40import com.android.launcher3.Launcher;
Tony05d98c22018-07-23 08:01:15 -070041import com.android.launcher3.R;
Jon Mirandaa0233f72017-06-22 18:34:45 -070042import com.android.launcher3.util.Themes;
43
Sunny Goyald2303072018-08-14 15:21:45 -070044import androidx.core.graphics.ColorUtils;
45
Jon Mirandaa0233f72017-06-22 18:34:45 -070046/**
47 * This object represents a FolderIcon preview background. It stores drawing / measurement
48 * information, handles drawing, and animation (accept state <--> rest state).
49 */
50public class PreviewBackground {
51
52 private static final int CONSUMPTION_ANIMATION_DURATION = 100;
53
Jon Mirandaa0233f72017-06-22 18:34:45 -070054 private final PorterDuffXfermode mShadowPorterDuffXfermode
55 = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
56 private RadialGradient mShadowShader = null;
57
58 private final Matrix mShaderMatrix = new Matrix();
59 private final Path mPath = new Path();
60
61 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
62
63 float mScale = 1f;
64 private float mColorMultiplier = 1f;
65 private int mBgColor;
Tony05d98c22018-07-23 08:01:15 -070066 private int mBadgeColor;
Jon Mirandaa0233f72017-06-22 18:34:45 -070067 private float mStrokeWidth;
68 private int mStrokeAlpha = MAX_BG_OPACITY;
69 private int mShadowAlpha = 255;
70 private View mInvalidateDelegate;
71
72 int previewSize;
73 int basePreviewOffsetX;
74 int basePreviewOffsetY;
75
76 private CellLayout mDrawingDelegate;
77 public int delegateCellX;
78 public int delegateCellY;
79
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
125 public void setup(Launcher launcher, View invalidateDelegate,
Jon Miranda591e3602018-03-28 11:37:00 -0700126 int availableSpaceX, int topPadding) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700127 mInvalidateDelegate = invalidateDelegate;
128 mBgColor = Themes.getAttrColor(launcher, android.R.attr.colorPrimary);
Tony05d98c22018-07-23 08:01:15 -0700129 mBadgeColor = Themes.getAttrColor(launcher, R.attr.folderBadgeColor);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700130
131 DeviceProfile grid = launcher.getDeviceProfile();
Jon Miranda591e3602018-03-28 11:37:00 -0700132 previewSize = grid.folderIconSizePx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700133
Jon Miranda591e3602018-03-28 11:37:00 -0700134 basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
135 basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700136
137 // Stroke width is 1dp
138 mStrokeWidth = launcher.getResources().getDisplayMetrics().density;
139
140 float radius = getScaledRadius();
141 float shadowRadius = radius + mStrokeWidth;
142 int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
143 mShadowShader = new RadialGradient(0, 0, 1,
144 new int[] {shadowColor, Color.TRANSPARENT},
145 new float[] {radius / shadowRadius, 1},
146 Shader.TileMode.CLAMP);
147
148 invalidate();
149 }
150
151 int getRadius() {
152 return previewSize / 2;
153 }
154
155 int getScaledRadius() {
156 return (int) (mScale * getRadius());
157 }
158
159 int getOffsetX() {
160 return basePreviewOffsetX - (getScaledRadius() - getRadius());
161 }
162
163 int getOffsetY() {
164 return basePreviewOffsetY - (getScaledRadius() - getRadius());
165 }
166
167 /**
168 * Returns the progress of the scale animation, where 0 means the scale is at 1f
169 * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
170 */
171 float getScaleProgress() {
172 return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
173 }
174
175 void invalidate() {
176 if (mInvalidateDelegate != null) {
177 mInvalidateDelegate.invalidate();
178 }
179
180 if (mDrawingDelegate != null) {
181 mDrawingDelegate.invalidate();
182 }
183 }
184
185 void setInvalidateDelegate(View invalidateDelegate) {
186 mInvalidateDelegate = invalidateDelegate;
187 invalidate();
188 }
189
Sunny Goyale29897f2017-07-20 10:09:42 -0700190 public int getBgColor() {
191 int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
192 return ColorUtils.setAlphaComponent(mBgColor, alpha);
193 }
194
Sunny Goyal179249d2017-12-19 16:49:24 -0800195 public int getBadgeColor() {
Tony05d98c22018-07-23 08:01:15 -0700196 return mBadgeColor;
Sunny Goyal179249d2017-12-19 16:49:24 -0800197 }
198
Jon Mirandaa0233f72017-06-22 18:34:45 -0700199 public void drawBackground(Canvas canvas) {
200 mPaint.setStyle(Paint.Style.FILL);
Sunny Goyale29897f2017-07-20 10:09:42 -0700201 mPaint.setColor(getBgColor());
Jon Mirandaa0233f72017-06-22 18:34:45 -0700202
Sunny Goyald0ae4922018-10-11 10:37:53 -0700203 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
Sunny Goyale29897f2017-07-20 10:09:42 -0700204 drawShadow(canvas);
205 }
206
207 public void drawShadow(Canvas canvas) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700208 if (mShadowShader == null) {
209 return;
210 }
Sunny Goyale29897f2017-07-20 10:09:42 -0700211
Jon Mirandaa0233f72017-06-22 18:34:45 -0700212 float radius = getScaledRadius();
213 float shadowRadius = radius + mStrokeWidth;
Sunny Goyale29897f2017-07-20 10:09:42 -0700214 mPaint.setStyle(Paint.Style.FILL);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700215 mPaint.setColor(Color.BLACK);
216 int offsetX = getOffsetX();
217 int offsetY = getOffsetY();
218 final int saveCount;
219 if (canvas.isHardwareAccelerated()) {
220 saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500221 offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, null);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700222
223 } else {
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500224 saveCount = canvas.save();
Sunny Goyale29897f2017-07-20 10:09:42 -0700225 canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700226 }
227
228 mShaderMatrix.setScale(shadowRadius, shadowRadius);
229 mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
230 mShadowShader.setLocalMatrix(mShaderMatrix);
231 mPaint.setAlpha(mShadowAlpha);
232 mPaint.setShader(mShadowShader);
233 canvas.drawPaint(mPaint);
234 mPaint.setAlpha(255);
235 mPaint.setShader(null);
236 if (canvas.isHardwareAccelerated()) {
237 mPaint.setXfermode(mShadowPorterDuffXfermode);
Sunny Goyald0ae4922018-10-11 10:37:53 -0700238 getShape().drawShape(canvas, offsetX, offsetY, radius, mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700239 mPaint.setXfermode(null);
240 }
241
242 canvas.restoreToCount(saveCount);
243 }
244
245 public void fadeInBackgroundShadow() {
246 if (mShadowAnimator != null) {
247 mShadowAnimator.cancel();
248 }
249 mShadowAnimator = ObjectAnimator
250 .ofInt(this, SHADOW_ALPHA, 0, 255)
251 .setDuration(100);
252 mShadowAnimator.addListener(new AnimatorListenerAdapter() {
253 @Override
254 public void onAnimationEnd(Animator animation) {
255 mShadowAnimator = null;
256 }
257 });
258 mShadowAnimator.start();
259 }
260
261 public void animateBackgroundStroke() {
262 if (mStrokeAlphaAnimator != null) {
263 mStrokeAlphaAnimator.cancel();
264 }
265 mStrokeAlphaAnimator = ObjectAnimator
266 .ofInt(this, STROKE_ALPHA, MAX_BG_OPACITY / 2, MAX_BG_OPACITY)
267 .setDuration(100);
268 mStrokeAlphaAnimator.addListener(new AnimatorListenerAdapter() {
269 @Override
270 public void onAnimationEnd(Animator animation) {
271 mStrokeAlphaAnimator = null;
272 }
273 });
274 mStrokeAlphaAnimator.start();
275 }
276
277 public void drawBackgroundStroke(Canvas canvas) {
278 mPaint.setColor(ColorUtils.setAlphaComponent(mBgColor, mStrokeAlpha));
279 mPaint.setStyle(Paint.Style.STROKE);
280 mPaint.setStrokeWidth(mStrokeWidth);
Sunny Goyald0ae4922018-10-11 10:37:53 -0700281
282 float inset = 1f;
283 getShape().drawShape(canvas,
284 getOffsetX() + inset, getOffsetY() + inset, getScaledRadius() - inset, mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700285 }
286
287 public void drawLeaveBehind(Canvas canvas) {
288 float originalScale = mScale;
289 mScale = 0.5f;
290
291 mPaint.setStyle(Paint.Style.FILL);
292 mPaint.setColor(Color.argb(160, 245, 245, 245));
Sunny Goyald0ae4922018-10-11 10:37:53 -0700293 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700294
295 mScale = originalScale;
296 }
297
Sunny Goyale29897f2017-07-20 10:09:42 -0700298 public Path getClipPath() {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700299 mPath.reset();
Sunny Goyald0ae4922018-10-11 10:37:53 -0700300 getShape().addShape(mPath, getOffsetX(), getOffsetY(), getScaledRadius());
Sunny Goyale29897f2017-07-20 10:09:42 -0700301 return mPath;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700302 }
303
Jon Mirandaa0233f72017-06-22 18:34:45 -0700304 private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
305 if (mDrawingDelegate != delegate) {
306 delegate.addFolderBackground(this);
307 }
308
309 mDrawingDelegate = delegate;
310 delegateCellX = cellX;
311 delegateCellY = cellY;
312
313 invalidate();
314 }
315
316 private void clearDrawingDelegate() {
317 if (mDrawingDelegate != null) {
318 mDrawingDelegate.removeFolderBackground(this);
319 }
320
321 mDrawingDelegate = null;
Sunny Goyal5247f5b2017-07-04 12:07:46 -0700322 isClipping = true;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700323 invalidate();
324 }
325
326 boolean drawingDelegated() {
327 return mDrawingDelegate != null;
328 }
329
330 private void animateScale(float finalScale, float finalMultiplier,
331 final Runnable onStart, final Runnable onEnd) {
332 final float scale0 = mScale;
333 final float scale1 = finalScale;
334
335 final float bgMultiplier0 = mColorMultiplier;
336 final float bgMultiplier1 = finalMultiplier;
337
338 if (mScaleAnimator != null) {
339 mScaleAnimator.cancel();
340 }
341
Sunny Goyal849c6a22018-08-08 16:33:46 -0700342 mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700343
344 mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
345 @Override
346 public void onAnimationUpdate(ValueAnimator animation) {
347 float prog = animation.getAnimatedFraction();
348 mScale = prog * scale1 + (1 - prog) * scale0;
349 mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
350 invalidate();
351 }
352 });
353 mScaleAnimator.addListener(new AnimatorListenerAdapter() {
354 @Override
355 public void onAnimationStart(Animator animation) {
356 if (onStart != null) {
357 onStart.run();
358 }
359 }
360
361 @Override
362 public void onAnimationEnd(Animator animation) {
363 if (onEnd != null) {
364 onEnd.run();
365 }
366 mScaleAnimator = null;
367 }
368 });
369
370 mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
371 mScaleAnimator.start();
372 }
373
Sunny Goyal849c6a22018-08-08 16:33:46 -0700374 public void animateToAccept(CellLayout cl, int cellX, int cellY) {
375 animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER,
376 () -> delegateDrawing(cl, cellX, cellY), null);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700377 }
378
379 public void animateToRest() {
380 // This can be called multiple times -- we need to make sure the drawing delegate
381 // is saved and restored at the beginning of the animation, since cancelling the
382 // existing animation can clear the delgate.
Sunny Goyal849c6a22018-08-08 16:33:46 -0700383 CellLayout cl = mDrawingDelegate;
384 int cellX = delegateCellX;
385 int cellY = delegateCellY;
386 animateScale(1f, 1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700387 }
388
389 public int getBackgroundAlpha() {
390 return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
391 }
392
393 public float getStrokeWidth() {
394 return mStrokeWidth;
395 }
396}