blob: ac908f4ad8725e527be10f84c77ca00dc71e803f [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;
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;
Jon Mirandaa0233f72017-06-22 18:34:45 -070045import com.android.launcher3.util.Themes;
Sunny Goyalab770a12018-11-14 15:17:26 -080046import com.android.launcher3.views.ActivityContext;
Jon Mirandaa0233f72017-06-22 18:34:45 -070047
48/**
49 * This object represents a FolderIcon preview background. It stores drawing / measurement
50 * information, handles drawing, and animation (accept state <--> rest state).
51 */
52public class PreviewBackground {
53
54 private static final int CONSUMPTION_ANIMATION_DURATION = 100;
55
Jon Mirandaa0233f72017-06-22 18:34:45 -070056 private final PorterDuffXfermode mShadowPorterDuffXfermode
57 = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
58 private RadialGradient mShadowShader = null;
59
60 private final Matrix mShaderMatrix = new Matrix();
61 private final Path mPath = new Path();
62
63 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
64
65 float mScale = 1f;
66 private float mColorMultiplier = 1f;
67 private int mBgColor;
Sunny Goyalbcadb7f2019-02-05 14:45:31 -080068 private int mStrokeColor;
Tony Wickhamf34bee82018-12-03 18:11:39 -080069 private int mDotColor;
Jon Mirandaa0233f72017-06-22 18:34:45 -070070 private float mStrokeWidth;
71 private int mStrokeAlpha = MAX_BG_OPACITY;
72 private int mShadowAlpha = 255;
73 private View mInvalidateDelegate;
74
75 int previewSize;
76 int basePreviewOffsetX;
77 int basePreviewOffsetY;
78
79 private CellLayout mDrawingDelegate;
80 public int delegateCellX;
81 public int delegateCellY;
82
83 // When the PreviewBackground is drawn under an icon (for creating a folder) the border
84 // should not occlude the icon
85 public boolean isClipping = true;
86
87 // Drawing / animation configurations
Jon Miranda72b5fd12017-06-25 18:06:23 -070088 private static final float ACCEPT_SCALE_FACTOR = 1.20f;
Jon Mirandaa0233f72017-06-22 18:34:45 -070089 private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f;
90
91 // Expressed on a scale from 0 to 255.
92 private static final int BG_OPACITY = 160;
93 private static final int MAX_BG_OPACITY = 225;
94 private static final int SHADOW_OPACITY = 40;
95
96 private ValueAnimator mScaleAnimator;
97 private ObjectAnimator mStrokeAlphaAnimator;
98 private ObjectAnimator mShadowAnimator;
99
100 private static final Property<PreviewBackground, Integer> STROKE_ALPHA =
101 new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") {
102 @Override
103 public Integer get(PreviewBackground previewBackground) {
104 return previewBackground.mStrokeAlpha;
105 }
106
107 @Override
108 public void set(PreviewBackground previewBackground, Integer alpha) {
109 previewBackground.mStrokeAlpha = alpha;
110 previewBackground.invalidate();
111 }
112 };
113
114 private static final Property<PreviewBackground, Integer> SHADOW_ALPHA =
115 new Property<PreviewBackground, Integer>(Integer.class, "shadowAlpha") {
116 @Override
117 public Integer get(PreviewBackground previewBackground) {
118 return previewBackground.mShadowAlpha;
119 }
120
121 @Override
122 public void set(PreviewBackground previewBackground, Integer alpha) {
123 previewBackground.mShadowAlpha = alpha;
124 previewBackground.invalidate();
125 }
126 };
127
Sunny Goyalab770a12018-11-14 15:17:26 -0800128 public void setup(Context context, ActivityContext activity, View invalidateDelegate,
Jon Miranda591e3602018-03-28 11:37:00 -0700129 int availableSpaceX, int topPadding) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700130 mInvalidateDelegate = invalidateDelegate;
Sunny Goyalbcadb7f2019-02-05 14:45:31 -0800131
132 TypedArray ta = context.getTheme().obtainStyledAttributes(R.styleable.FolderIconPreview);
133 mDotColor = ta.getColor(R.styleable.FolderIconPreview_folderDotColor, 0);
134 mStrokeColor = ta.getColor(R.styleable.FolderIconPreview_folderIconBorderColor, 0);
135 mBgColor = ta.getColor(R.styleable.FolderIconPreview_android_colorPrimary, 0);
136 ta.recycle();
Jon Mirandaa0233f72017-06-22 18:34:45 -0700137
Sunny Goyalab770a12018-11-14 15:17:26 -0800138 DeviceProfile grid = activity.getDeviceProfile();
Jon Miranda591e3602018-03-28 11:37:00 -0700139 previewSize = grid.folderIconSizePx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700140
Jon Miranda591e3602018-03-28 11:37:00 -0700141 basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
142 basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700143
144 // Stroke width is 1dp
Sunny Goyalab770a12018-11-14 15:17:26 -0800145 mStrokeWidth = context.getResources().getDisplayMetrics().density;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700146
147 float radius = getScaledRadius();
148 float shadowRadius = radius + mStrokeWidth;
149 int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0);
150 mShadowShader = new RadialGradient(0, 0, 1,
151 new int[] {shadowColor, Color.TRANSPARENT},
152 new float[] {radius / shadowRadius, 1},
153 Shader.TileMode.CLAMP);
154
155 invalidate();
156 }
157
Jon Miranda69b35e02019-03-04 20:16:18 -0800158 void getBounds(Rect outBounds) {
159 int top = basePreviewOffsetY;
160 int left = basePreviewOffsetX;
161 int right = left + previewSize;
162 int bottom = top + previewSize;
163 outBounds.set(left, top, right, bottom);
164 }
165
Jon Mirandaa0233f72017-06-22 18:34:45 -0700166 int getRadius() {
167 return previewSize / 2;
168 }
169
170 int getScaledRadius() {
171 return (int) (mScale * getRadius());
172 }
173
174 int getOffsetX() {
175 return basePreviewOffsetX - (getScaledRadius() - getRadius());
176 }
177
178 int getOffsetY() {
179 return basePreviewOffsetY - (getScaledRadius() - getRadius());
180 }
181
182 /**
183 * Returns the progress of the scale animation, where 0 means the scale is at 1f
184 * and 1 means the scale is at ACCEPT_SCALE_FACTOR.
185 */
186 float getScaleProgress() {
187 return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f);
188 }
189
190 void invalidate() {
191 if (mInvalidateDelegate != null) {
192 mInvalidateDelegate.invalidate();
193 }
194
195 if (mDrawingDelegate != null) {
196 mDrawingDelegate.invalidate();
197 }
198 }
199
200 void setInvalidateDelegate(View invalidateDelegate) {
201 mInvalidateDelegate = invalidateDelegate;
202 invalidate();
203 }
204
Sunny Goyale29897f2017-07-20 10:09:42 -0700205 public int getBgColor() {
206 int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
Sunny Goyal066ace12018-11-05 11:08:31 -0800207 return setColorAlphaBound(mBgColor, alpha);
Sunny Goyale29897f2017-07-20 10:09:42 -0700208 }
209
Tony Wickhamf34bee82018-12-03 18:11:39 -0800210 public int getDotColor() {
211 return mDotColor;
Sunny Goyal179249d2017-12-19 16:49:24 -0800212 }
213
Jon Mirandaa0233f72017-06-22 18:34:45 -0700214 public void drawBackground(Canvas canvas) {
215 mPaint.setStyle(Paint.Style.FILL);
Sunny Goyale29897f2017-07-20 10:09:42 -0700216 mPaint.setColor(getBgColor());
Jon Mirandaa0233f72017-06-22 18:34:45 -0700217
Sunny Goyald0ae4922018-10-11 10:37:53 -0700218 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
Sunny Goyale29897f2017-07-20 10:09:42 -0700219 drawShadow(canvas);
220 }
221
222 public void drawShadow(Canvas canvas) {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700223 if (mShadowShader == null) {
224 return;
225 }
Sunny Goyale29897f2017-07-20 10:09:42 -0700226
Jon Mirandaa0233f72017-06-22 18:34:45 -0700227 float radius = getScaledRadius();
228 float shadowRadius = radius + mStrokeWidth;
Sunny Goyale29897f2017-07-20 10:09:42 -0700229 mPaint.setStyle(Paint.Style.FILL);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700230 mPaint.setColor(Color.BLACK);
231 int offsetX = getOffsetX();
232 int offsetY = getOffsetY();
233 final int saveCount;
234 if (canvas.isHardwareAccelerated()) {
235 saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY,
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500236 offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, null);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700237
238 } else {
Derek Sollenberger26a1b452018-02-23 13:37:25 -0500239 saveCount = canvas.save();
Sunny Goyale29897f2017-07-20 10:09:42 -0700240 canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700241 }
242
243 mShaderMatrix.setScale(shadowRadius, shadowRadius);
244 mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY);
245 mShadowShader.setLocalMatrix(mShaderMatrix);
246 mPaint.setAlpha(mShadowAlpha);
247 mPaint.setShader(mShadowShader);
248 canvas.drawPaint(mPaint);
249 mPaint.setAlpha(255);
250 mPaint.setShader(null);
251 if (canvas.isHardwareAccelerated()) {
252 mPaint.setXfermode(mShadowPorterDuffXfermode);
Sunny Goyald0ae4922018-10-11 10:37:53 -0700253 getShape().drawShape(canvas, offsetX, offsetY, radius, mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700254 mPaint.setXfermode(null);
255 }
256
257 canvas.restoreToCount(saveCount);
258 }
259
260 public void fadeInBackgroundShadow() {
261 if (mShadowAnimator != null) {
262 mShadowAnimator.cancel();
263 }
264 mShadowAnimator = ObjectAnimator
265 .ofInt(this, SHADOW_ALPHA, 0, 255)
266 .setDuration(100);
267 mShadowAnimator.addListener(new AnimatorListenerAdapter() {
268 @Override
269 public void onAnimationEnd(Animator animation) {
270 mShadowAnimator = null;
271 }
272 });
273 mShadowAnimator.start();
274 }
275
276 public void animateBackgroundStroke() {
277 if (mStrokeAlphaAnimator != null) {
278 mStrokeAlphaAnimator.cancel();
279 }
280 mStrokeAlphaAnimator = ObjectAnimator
281 .ofInt(this, STROKE_ALPHA, MAX_BG_OPACITY / 2, MAX_BG_OPACITY)
282 .setDuration(100);
283 mStrokeAlphaAnimator.addListener(new AnimatorListenerAdapter() {
284 @Override
285 public void onAnimationEnd(Animator animation) {
286 mStrokeAlphaAnimator = null;
287 }
288 });
289 mStrokeAlphaAnimator.start();
290 }
291
292 public void drawBackgroundStroke(Canvas canvas) {
Sunny Goyalbcadb7f2019-02-05 14:45:31 -0800293 mPaint.setColor(setColorAlphaBound(mStrokeColor, mStrokeAlpha));
Jon Mirandaa0233f72017-06-22 18:34:45 -0700294 mPaint.setStyle(Paint.Style.STROKE);
295 mPaint.setStrokeWidth(mStrokeWidth);
Sunny Goyald0ae4922018-10-11 10:37:53 -0700296
297 float inset = 1f;
298 getShape().drawShape(canvas,
299 getOffsetX() + inset, getOffsetY() + inset, getScaledRadius() - inset, mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700300 }
301
302 public void drawLeaveBehind(Canvas canvas) {
303 float originalScale = mScale;
304 mScale = 0.5f;
305
306 mPaint.setStyle(Paint.Style.FILL);
307 mPaint.setColor(Color.argb(160, 245, 245, 245));
Sunny Goyald0ae4922018-10-11 10:37:53 -0700308 getShape().drawShape(canvas, getOffsetX(), getOffsetY(), getScaledRadius(), mPaint);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700309
310 mScale = originalScale;
311 }
312
Sunny Goyale29897f2017-07-20 10:09:42 -0700313 public Path getClipPath() {
Jon Mirandaa0233f72017-06-22 18:34:45 -0700314 mPath.reset();
Sunny Goyald0ae4922018-10-11 10:37:53 -0700315 getShape().addShape(mPath, getOffsetX(), getOffsetY(), getScaledRadius());
Sunny Goyale29897f2017-07-20 10:09:42 -0700316 return mPath;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700317 }
318
Jon Mirandaa0233f72017-06-22 18:34:45 -0700319 private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
320 if (mDrawingDelegate != delegate) {
321 delegate.addFolderBackground(this);
322 }
323
324 mDrawingDelegate = delegate;
325 delegateCellX = cellX;
326 delegateCellY = cellY;
327
328 invalidate();
329 }
330
331 private void clearDrawingDelegate() {
332 if (mDrawingDelegate != null) {
333 mDrawingDelegate.removeFolderBackground(this);
334 }
335
336 mDrawingDelegate = null;
Sunny Goyal5247f5b2017-07-04 12:07:46 -0700337 isClipping = true;
Jon Mirandaa0233f72017-06-22 18:34:45 -0700338 invalidate();
339 }
340
341 boolean drawingDelegated() {
342 return mDrawingDelegate != null;
343 }
344
345 private void animateScale(float finalScale, float finalMultiplier,
346 final Runnable onStart, final Runnable onEnd) {
347 final float scale0 = mScale;
348 final float scale1 = finalScale;
349
350 final float bgMultiplier0 = mColorMultiplier;
351 final float bgMultiplier1 = finalMultiplier;
352
353 if (mScaleAnimator != null) {
354 mScaleAnimator.cancel();
355 }
356
Sunny Goyal849c6a22018-08-08 16:33:46 -0700357 mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700358
359 mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
360 @Override
361 public void onAnimationUpdate(ValueAnimator animation) {
362 float prog = animation.getAnimatedFraction();
363 mScale = prog * scale1 + (1 - prog) * scale0;
364 mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0;
365 invalidate();
366 }
367 });
368 mScaleAnimator.addListener(new AnimatorListenerAdapter() {
369 @Override
370 public void onAnimationStart(Animator animation) {
371 if (onStart != null) {
372 onStart.run();
373 }
374 }
375
376 @Override
377 public void onAnimationEnd(Animator animation) {
378 if (onEnd != null) {
379 onEnd.run();
380 }
381 mScaleAnimator = null;
382 }
383 });
384
385 mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
386 mScaleAnimator.start();
387 }
388
Sunny Goyal849c6a22018-08-08 16:33:46 -0700389 public void animateToAccept(CellLayout cl, int cellX, int cellY) {
390 animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER,
391 () -> delegateDrawing(cl, cellX, cellY), null);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700392 }
393
394 public void animateToRest() {
395 // This can be called multiple times -- we need to make sure the drawing delegate
396 // is saved and restored at the beginning of the animation, since cancelling the
397 // existing animation can clear the delgate.
Sunny Goyal849c6a22018-08-08 16:33:46 -0700398 CellLayout cl = mDrawingDelegate;
399 int cellX = delegateCellX;
400 int cellY = delegateCellY;
401 animateScale(1f, 1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
Jon Mirandaa0233f72017-06-22 18:34:45 -0700402 }
403
404 public int getBackgroundAlpha() {
405 return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
406 }
407
408 public float getStrokeWidth() {
409 return mStrokeWidth;
410 }
411}