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