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