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