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