1 /*
2  * Copyright (C) 2018 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 package com.android.launcher3.graphics;
17 
18 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.FloatArrayEvaluator;
23 import android.animation.ValueAnimator;
24 import android.animation.ValueAnimator.AnimatorUpdateListener;
25 import android.content.Context;
26 import android.content.res.TypedArray;
27 import android.content.res.XmlResourceParser;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.Paint;
31 import android.graphics.Path;
32 import android.graphics.Rect;
33 import android.graphics.Region;
34 import android.graphics.Region.Op;
35 import android.graphics.drawable.AdaptiveIconDrawable;
36 import android.graphics.drawable.ColorDrawable;
37 import android.util.AttributeSet;
38 import android.util.Xml;
39 import android.view.View;
40 import android.view.ViewOutlineProvider;
41 
42 import com.android.launcher3.R;
43 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
44 import com.android.launcher3.icons.GraphicsUtils;
45 import com.android.launcher3.icons.IconNormalizer;
46 import com.android.launcher3.util.MainThreadInitializedObject;
47 import com.android.launcher3.util.SafeCloseable;
48 import com.android.launcher3.views.ClipPathView;
49 
50 import org.xmlpull.v1.XmlPullParser;
51 import org.xmlpull.v1.XmlPullParserException;
52 
53 import java.io.IOException;
54 import java.util.ArrayList;
55 import java.util.List;
56 
57 /**
58  * Abstract representation of the shape of an icon shape
59  */
60 public final class IconShape implements SafeCloseable {
61 
62     public static final MainThreadInitializedObject<IconShape> INSTANCE =
63             new MainThreadInitializedObject<>(IconShape::new);
64 
65 
66     private ShapeDelegate mDelegate = new Circle();
67     private float mNormalizationScale = ICON_VISIBLE_AREA_FACTOR;
68 
IconShape(Context context)69     private IconShape(Context context) {
70         pickBestShape(context);
71     }
72 
getShape()73     public ShapeDelegate getShape() {
74         return mDelegate;
75     }
76 
getNormalizationScale()77     public float getNormalizationScale() {
78         return mNormalizationScale;
79     }
80 
81     @Override
close()82     public void close() { }
83 
84     /**
85      * Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
86      */
pickBestShape(Context context)87     public void pickBestShape(Context context) {
88         // Pick any large size
89         final int size = 200;
90 
91         Region full = new Region(0, 0, size, size);
92         Region iconR = new Region();
93         AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
94                 new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
95         drawable.setBounds(0, 0, size, size);
96         iconR.setPath(drawable.getIconMask(), full);
97 
98         Path shapePath = new Path();
99         Region shapeR = new Region();
100 
101         // Find the shape with minimum area of divergent region.
102         int minArea = Integer.MAX_VALUE;
103         ShapeDelegate closestShape = null;
104         for (ShapeDelegate shape : getAllShapes(context)) {
105             shapePath.reset();
106             shape.addToPath(shapePath, 0, 0, size / 2f);
107             shapeR.setPath(shapePath, full);
108             shapeR.op(iconR, Op.XOR);
109 
110             int area = GraphicsUtils.getArea(shapeR);
111             if (area < minArea) {
112                 minArea = area;
113                 closestShape = shape;
114             }
115         }
116 
117         if (closestShape != null) {
118             mDelegate = closestShape;
119         }
120 
121         // Initialize shape properties
122         mNormalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, size, null);
123     }
124 
125 
126 
127     public interface ShapeDelegate {
128 
enableShapeDetection()129         default boolean enableShapeDetection() {
130             return false;
131         }
132 
drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint paint)133         void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint paint);
134 
addToPath(Path path, float offsetX, float offsetY, float radius)135         void addToPath(Path path, float offsetX, float offsetY, float radius);
136 
createRevealAnimator(T target, Rect startRect, Rect endRect, float endRadius, boolean isReversed)137         <T extends View & ClipPathView> ValueAnimator createRevealAnimator(T target,
138                 Rect startRect, Rect endRect, float endRadius, boolean isReversed);
139     }
140 
141     /**
142      * Abstract shape where the reveal animation is a derivative of a round rect animation
143      */
144     private static abstract class SimpleRectShape implements ShapeDelegate {
145 
146         @Override
createRevealAnimator(T target, Rect startRect, Rect endRect, float endRadius, boolean isReversed)147         public final <T extends View & ClipPathView> ValueAnimator createRevealAnimator(T target,
148                 Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
149             return new RoundedRectRevealOutlineProvider(
150                     getStartRadius(startRect), endRadius, startRect, endRect) {
151                 @Override
152                 public boolean shouldRemoveElevationDuringAnimation() {
153                     return true;
154                 }
155             }.createRevealAnimator(target, isReversed);
156         }
157 
getStartRadius(Rect startRect)158         protected abstract float getStartRadius(Rect startRect);
159     }
160 
161     /**
162      * Abstract shape which draws using {@link Path}
163      */
164     private static abstract class PathShape implements ShapeDelegate {
165 
166         private final Path mTmpPath = new Path();
167 
168         @Override
169         public final void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,
170                 Paint paint) {
171             mTmpPath.reset();
172             addToPath(mTmpPath, offsetX, offsetY, radius);
173             canvas.drawPath(mTmpPath, paint);
174         }
175 
176         protected abstract AnimatorUpdateListener newUpdateListener(
177                 Rect startRect, Rect endRect, float endRadius, Path outPath);
178 
179         @Override
180         public final <T extends View & ClipPathView> ValueAnimator createRevealAnimator(T target,
181                 Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
182             Path path = new Path();
183             AnimatorUpdateListener listener =
184                     newUpdateListener(startRect, endRect, endRadius, path);
185 
186             ValueAnimator va =
187                     isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
188             va.addListener(new AnimatorListenerAdapter() {
189                 private ViewOutlineProvider mOldOutlineProvider;
190 
191                 public void onAnimationStart(Animator animation) {
192                     mOldOutlineProvider = target.getOutlineProvider();
193                     target.setOutlineProvider(null);
194 
195                     target.setTranslationZ(-target.getElevation());
196                 }
197 
198                 public void onAnimationEnd(Animator animation) {
199                     target.setTranslationZ(0);
200                     target.setClipPath(null);
201                     target.setOutlineProvider(mOldOutlineProvider);
202                 }
203             });
204 
205             va.addUpdateListener((anim) -> {
206                 path.reset();
207                 listener.onAnimationUpdate(anim);
208                 target.setClipPath(path);
209             });
210 
211             return va;
212         }
213     }
214 
215     public static final class Circle extends PathShape {
216 
217         private final float[] mTempRadii = new float[8];
218 
219         protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
220                 float endRadius, Path outPath) {
221             float r1 = getStartRadius(startRect);
222 
223             float[] startValues = new float[] {
224                     startRect.left, startRect.top, startRect.right, startRect.bottom, r1, r1};
225             float[] endValues = new float[] {
226                     endRect.left, endRect.top, endRect.right, endRect.bottom, endRadius, endRadius};
227 
228             FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[6]);
229 
230             return (anim) -> {
231                 float progress = (Float) anim.getAnimatedValue();
232                 float[] values = evaluator.evaluate(progress, startValues, endValues);
233                 outPath.addRoundRect(
234                         values[0], values[1], values[2], values[3],
235                         getRadiiArray(values[4], values[5]), Path.Direction.CW);
236             };
237         }
238 
239         private float[] getRadiiArray(float r1, float r2) {
240             mTempRadii[0] = mTempRadii [1] = mTempRadii[2] = mTempRadii[3] =
241                     mTempRadii[6] = mTempRadii[7] = r1;
242             mTempRadii[4] = mTempRadii[5] = r2;
243             return mTempRadii;
244         }
245 
246 
247         @Override
248         public void addToPath(Path path, float offsetX, float offsetY, float radius) {
249             path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW);
250         }
251 
252         protected float getStartRadius(Rect startRect) {
253             return startRect.width() / 2f;
254         }
255 
256         @Override
257         public boolean enableShapeDetection() {
258             return true;
259         }
260     }
261 
262     private static class RoundedSquare extends SimpleRectShape {
263 
264         /**
265          * Ratio of corner radius to half size.
266          */
267         private final float mRadiusRatio;
268 
269         public RoundedSquare(float radiusRatio) {
270             mRadiusRatio = radiusRatio;
271         }
272 
273         @Override
274         public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
275             float cx = radius + offsetX;
276             float cy = radius + offsetY;
277             float cr = radius * mRadiusRatio;
278             canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, p);
279         }
280 
281         @Override
282         public void addToPath(Path path, float offsetX, float offsetY, float radius) {
283             float cx = radius + offsetX;
284             float cy = radius + offsetY;
285             float cr = radius * mRadiusRatio;
286             path.addRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr,
287                     Path.Direction.CW);
288         }
289 
290         @Override
291         protected float getStartRadius(Rect startRect) {
292             return (startRect.width() / 2f) * mRadiusRatio;
293         }
294     }
295 
296     private static class TearDrop extends PathShape {
297 
298         /**
299          * Radio of short radius to large radius, based on the shape options defined in the config.
300          */
301         private final float mRadiusRatio;
302         private final float[] mTempRadii = new float[8];
303 
304         public TearDrop(float radiusRatio) {
305             mRadiusRatio = radiusRatio;
306         }
307 
308         @Override
309         public void addToPath(Path p, float offsetX, float offsetY, float r1) {
310             float r2 = r1 * mRadiusRatio;
311             float cx = r1 + offsetX;
312             float cy = r1 + offsetY;
313 
314             p.addRoundRect(cx - r1, cy - r1, cx + r1, cy + r1, getRadiiArray(r1, r2),
315                     Path.Direction.CW);
316         }
317 
318         private float[] getRadiiArray(float r1, float r2) {
319             mTempRadii[0] = mTempRadii [1] = mTempRadii[2] = mTempRadii[3] =
320                     mTempRadii[6] = mTempRadii[7] = r1;
321             mTempRadii[4] = mTempRadii[5] = r2;
322             return mTempRadii;
323         }
324 
325         @Override
326         protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
327                 float endRadius, Path outPath) {
328             float r1 = startRect.width() / 2f;
329             float r2 = r1 * mRadiusRatio;
330 
331             float[] startValues = new float[] {
332                     startRect.left, startRect.top, startRect.right, startRect.bottom, r1, r2};
333             float[] endValues = new float[] {
334                     endRect.left, endRect.top, endRect.right, endRect.bottom, endRadius, endRadius};
335 
336             FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[6]);
337 
338             return (anim) -> {
339                 float progress = (Float) anim.getAnimatedValue();
340                 float[] values = evaluator.evaluate(progress, startValues, endValues);
341                 outPath.addRoundRect(
342                         values[0], values[1], values[2], values[3],
343                         getRadiiArray(values[4], values[5]), Path.Direction.CW);
344             };
345         }
346     }
347 
348     private static class Squircle extends PathShape {
349 
350         /**
351          * Radio of radius to circle radius, based on the shape options defined in the config.
352          */
353         private final float mRadiusRatio;
354 
355         public Squircle(float radiusRatio) {
356             mRadiusRatio = radiusRatio;
357         }
358 
359         @Override
360         public void addToPath(Path p, float offsetX, float offsetY, float r) {
361             float cx = r + offsetX;
362             float cy = r + offsetY;
363             float control = r - r * mRadiusRatio;
364 
365             p.moveTo(cx, cy - r);
366             addLeftCurve(cx, cy, r, control, p);
367             addRightCurve(cx, cy, r, control, p);
368             addLeftCurve(cx, cy, -r, -control, p);
369             addRightCurve(cx, cy, -r, -control, p);
370             p.close();
371         }
372 
373         private void addLeftCurve(float cx, float cy, float r, float control, Path path) {
374             path.cubicTo(
375                     cx - control, cy - r,
376                     cx - r, cy - control,
377                     cx - r, cy);
378         }
379 
380         private void addRightCurve(float cx, float cy, float r, float control, Path path) {
381             path.cubicTo(
382                     cx - r, cy + control,
383                     cx - control, cy + r,
384                     cx, cy + r);
385         }
386 
387         @Override
388         protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
389                 float endR, Path outPath) {
390 
391             float startCX = startRect.exactCenterX();
392             float startCY = startRect.exactCenterY();
393             float startR = startRect.width() / 2f;
394             float startControl = startR - startR * mRadiusRatio;
395             float startHShift = 0;
396             float startVShift = 0;
397 
398             float endCX = endRect.exactCenterX();
399             float endCY = endRect.exactCenterY();
400             // Approximate corner circle using bezier curves
401             // http://spencermortensen.com/articles/bezier-circle/
402             float endControl = endR * 0.551915024494f;
403             float endHShift = endRect.width() / 2f - endR;
404             float endVShift = endRect.height() / 2f - endR;
405 
406             return (anim) -> {
407                 float progress = (Float) anim.getAnimatedValue();
408 
409                 float cx = (1 - progress) * startCX + progress * endCX;
410                 float cy = (1 - progress) * startCY + progress * endCY;
411                 float r = (1 - progress) * startR + progress * endR;
412                 float control = (1 - progress) * startControl + progress * endControl;
413                 float hShift = (1 - progress) * startHShift + progress * endHShift;
414                 float vShift = (1 - progress) * startVShift + progress * endVShift;
415 
416                 outPath.moveTo(cx, cy - vShift - r);
417                 outPath.rLineTo(-hShift, 0);
418 
419                 addLeftCurve(cx - hShift, cy - vShift, r, control, outPath);
420                 outPath.rLineTo(0, vShift + vShift);
421 
422                 addRightCurve(cx - hShift, cy + vShift, r, control, outPath);
423                 outPath.rLineTo(hShift + hShift, 0);
424 
425                 addLeftCurve(cx + hShift, cy + vShift, -r, -control, outPath);
426                 outPath.rLineTo(0, -vShift - vShift);
427 
428                 addRightCurve(cx + hShift, cy - vShift, -r, -control, outPath);
429                 outPath.close();
430             };
431         }
432     }
433 
434     private static ShapeDelegate getShapeDefinition(String type, float radius) {
435         switch (type) {
436             case "Circle":
437                 return new Circle();
438             case "RoundedSquare":
439                 return new RoundedSquare(radius);
440             case "TearDrop":
441                 return new TearDrop(radius);
442             case "Squircle":
443                 return new Squircle(radius);
444             default:
445                 throw new IllegalArgumentException("Invalid shape type: " + type);
446         }
447     }
448 
449     private static List<ShapeDelegate> getAllShapes(Context context) {
450         ArrayList<ShapeDelegate> result = new ArrayList<>();
451         try (XmlResourceParser parser = context.getResources().getXml(R.xml.folder_shapes)) {
452 
453             // Find the root tag
454             int type;
455             while ((type = parser.next()) != XmlPullParser.END_TAG
456                     && type != XmlPullParser.END_DOCUMENT
457                     && !"shapes".equals(parser.getName()));
458 
459             final int depth = parser.getDepth();
460             int[] radiusAttr = new int[] {R.attr.folderIconRadius};
461 
462             while (((type = parser.next()) != XmlPullParser.END_TAG ||
463                     parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
464 
465                 if (type == XmlPullParser.START_TAG) {
466                     AttributeSet attrs = Xml.asAttributeSet(parser);
467                     TypedArray a = context.obtainStyledAttributes(attrs, radiusAttr);
468                     ShapeDelegate shape = getShapeDefinition(parser.getName(), a.getFloat(0, 1));
469                     a.recycle();
470 
471                     result.add(shape);
472                 }
473             }
474         } catch (IOException | XmlPullParserException e) {
475             throw new RuntimeException(e);
476         }
477         return result;
478     }
479 
480 }
481