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