/*
|
* Copyright (C) 2018 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
package com.android.launcher3.graphics;
|
|
import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
|
|
import android.animation.Animator;
|
import android.animation.AnimatorListenerAdapter;
|
import android.animation.FloatArrayEvaluator;
|
import android.animation.ValueAnimator;
|
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
import android.annotation.TargetApi;
|
import android.content.Context;
|
import android.content.res.TypedArray;
|
import android.content.res.XmlResourceParser;
|
import android.graphics.Canvas;
|
import android.graphics.Color;
|
import android.graphics.Paint;
|
import android.graphics.Path;
|
import android.graphics.Rect;
|
import android.graphics.Region;
|
import android.graphics.Region.Op;
|
import android.graphics.drawable.AdaptiveIconDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
import android.os.Build;
|
import android.util.AttributeSet;
|
import android.util.SparseArray;
|
import android.util.TypedValue;
|
import android.util.Xml;
|
import android.view.View;
|
import android.view.ViewOutlineProvider;
|
|
import com.android.launcher3.R;
|
import com.android.launcher3.Utilities;
|
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
|
import com.android.launcher3.icons.GraphicsUtils;
|
import com.android.launcher3.icons.IconNormalizer;
|
import com.android.launcher3.util.IntArray;
|
import com.android.launcher3.util.Themes;
|
import com.android.launcher3.views.ClipPathView;
|
|
import org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParserException;
|
|
import java.io.IOException;
|
import java.util.ArrayList;
|
import java.util.List;
|
|
import androidx.annotation.Nullable;
|
|
/**
|
* Abstract representation of the shape of an icon shape
|
*/
|
public abstract class IconShape {
|
|
private static IconShape sInstance = new Circle();
|
private static Path sShapePath;
|
private static float sNormalizationScale = ICON_VISIBLE_AREA_FACTOR;
|
|
public static final int DEFAULT_PATH_SIZE = 100;
|
|
public static IconShape getShape() {
|
return sInstance;
|
}
|
|
public static Path getShapePath() {
|
if (sShapePath == null) {
|
Path p = new Path();
|
getShape().addToPath(p, 0, 0, DEFAULT_PATH_SIZE * 0.5f);
|
sShapePath = p;
|
}
|
return sShapePath;
|
}
|
|
public static float getNormalizationScale() {
|
return sNormalizationScale;
|
}
|
|
private SparseArray<TypedValue> mAttrs;
|
|
public boolean enableShapeDetection(){
|
return false;
|
};
|
|
public abstract void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,
|
Paint paint);
|
|
public abstract void addToPath(Path path, float offsetX, float offsetY, float radius);
|
|
public abstract <T extends View & ClipPathView> Animator createRevealAnimator(T target,
|
Rect startRect, Rect endRect, float endRadius, boolean isReversed);
|
|
@Nullable
|
public TypedValue getAttrValue(int attr) {
|
return mAttrs == null ? null : mAttrs.get(attr);
|
}
|
|
/**
|
* Abstract shape where the reveal animation is a derivative of a round rect animation
|
*/
|
private static abstract class SimpleRectShape extends IconShape {
|
|
@Override
|
public final <T extends View & ClipPathView> Animator createRevealAnimator(T target,
|
Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
|
return new RoundedRectRevealOutlineProvider(
|
getStartRadius(startRect), endRadius, startRect, endRect) {
|
@Override
|
public boolean shouldRemoveElevationDuringAnimation() {
|
return true;
|
}
|
}.createRevealAnimator(target, isReversed);
|
}
|
|
protected abstract float getStartRadius(Rect startRect);
|
}
|
|
/**
|
* Abstract shape which draws using {@link Path}
|
*/
|
private static abstract class PathShape extends IconShape {
|
|
private final Path mTmpPath = new Path();
|
|
@Override
|
public final void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,
|
Paint paint) {
|
mTmpPath.reset();
|
addToPath(mTmpPath, offsetX, offsetY, radius);
|
canvas.drawPath(mTmpPath, paint);
|
}
|
|
protected abstract AnimatorUpdateListener newUpdateListener(
|
Rect startRect, Rect endRect, float endRadius, Path outPath);
|
|
@Override
|
public final <T extends View & ClipPathView> Animator createRevealAnimator(T target,
|
Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
|
Path path = new Path();
|
AnimatorUpdateListener listener =
|
newUpdateListener(startRect, endRect, endRadius, path);
|
|
ValueAnimator va =
|
isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
|
va.addListener(new AnimatorListenerAdapter() {
|
private ViewOutlineProvider mOldOutlineProvider;
|
|
public void onAnimationStart(Animator animation) {
|
mOldOutlineProvider = target.getOutlineProvider();
|
target.setOutlineProvider(null);
|
|
target.setTranslationZ(-target.getElevation());
|
}
|
|
public void onAnimationEnd(Animator animation) {
|
target.setTranslationZ(0);
|
target.setClipPath(null);
|
target.setOutlineProvider(mOldOutlineProvider);
|
}
|
});
|
|
va.addUpdateListener((anim) -> {
|
path.reset();
|
listener.onAnimationUpdate(anim);
|
target.setClipPath(path);
|
});
|
|
return va;
|
}
|
}
|
|
public static final class Circle extends SimpleRectShape {
|
|
@Override
|
public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
|
canvas.drawCircle(radius + offsetX, radius + offsetY, radius, p);
|
}
|
|
@Override
|
public void addToPath(Path path, float offsetX, float offsetY, float radius) {
|
path.addCircle(radius + offsetX, radius + offsetY, radius, Path.Direction.CW);
|
}
|
|
@Override
|
protected float getStartRadius(Rect startRect) {
|
return startRect.width() / 2f;
|
}
|
|
@Override
|
public boolean enableShapeDetection() {
|
return true;
|
}
|
}
|
|
public static class RoundedSquare extends SimpleRectShape {
|
|
/**
|
* Ratio of corner radius to half size.
|
*/
|
private final float mRadiusRatio;
|
|
public RoundedSquare(float radiusRatio) {
|
mRadiusRatio = radiusRatio;
|
}
|
|
@Override
|
public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
|
float cx = radius + offsetX;
|
float cy = radius + offsetY;
|
float cr = radius * mRadiusRatio;
|
canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, p);
|
}
|
|
@Override
|
public void addToPath(Path path, float offsetX, float offsetY, float radius) {
|
float cx = radius + offsetX;
|
float cy = radius + offsetY;
|
float cr = radius * mRadiusRatio;
|
path.addRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr,
|
Path.Direction.CW);
|
}
|
|
@Override
|
protected float getStartRadius(Rect startRect) {
|
return (startRect.width() / 2f) * mRadiusRatio;
|
}
|
}
|
|
public static class TearDrop extends PathShape {
|
|
/**
|
* Radio of short radius to large radius, based on the shape options defined in the config.
|
*/
|
private final float mRadiusRatio;
|
private final float[] mTempRadii = new float[8];
|
|
public TearDrop(float radiusRatio) {
|
mRadiusRatio = radiusRatio;
|
}
|
|
@Override
|
public void addToPath(Path p, float offsetX, float offsetY, float r1) {
|
float r2 = r1 * mRadiusRatio;
|
float cx = r1 + offsetX;
|
float cy = r1 + offsetY;
|
|
p.addRoundRect(cx - r1, cy - r1, cx + r1, cy + r1, getRadiiArray(r1, r2),
|
Path.Direction.CW);
|
}
|
|
private float[] getRadiiArray(float r1, float r2) {
|
mTempRadii[0] = mTempRadii [1] = mTempRadii[2] = mTempRadii[3] =
|
mTempRadii[6] = mTempRadii[7] = r1;
|
mTempRadii[4] = mTempRadii[5] = r2;
|
return mTempRadii;
|
}
|
|
@Override
|
protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
|
float endRadius, Path outPath) {
|
float r1 = startRect.width() / 2f;
|
float r2 = r1 * mRadiusRatio;
|
|
float[] startValues = new float[] {
|
startRect.left, startRect.top, startRect.right, startRect.bottom, r1, r2};
|
float[] endValues = new float[] {
|
endRect.left, endRect.top, endRect.right, endRect.bottom, endRadius, endRadius};
|
|
FloatArrayEvaluator evaluator = new FloatArrayEvaluator(new float[6]);
|
|
return (anim) -> {
|
float progress = (Float) anim.getAnimatedValue();
|
float[] values = evaluator.evaluate(progress, startValues, endValues);
|
outPath.addRoundRect(
|
values[0], values[1], values[2], values[3],
|
getRadiiArray(values[4], values[5]), Path.Direction.CW);
|
};
|
}
|
}
|
|
public static class Squircle extends PathShape {
|
|
/**
|
* Radio of radius to circle radius, based on the shape options defined in the config.
|
*/
|
private final float mRadiusRatio;
|
|
public Squircle(float radiusRatio) {
|
mRadiusRatio = radiusRatio;
|
}
|
|
@Override
|
public void addToPath(Path p, float offsetX, float offsetY, float r) {
|
float cx = r + offsetX;
|
float cy = r + offsetY;
|
float control = r - r * mRadiusRatio;
|
|
p.moveTo(cx, cy - r);
|
addLeftCurve(cx, cy, r, control, p);
|
addRightCurve(cx, cy, r, control, p);
|
addLeftCurve(cx, cy, -r, -control, p);
|
addRightCurve(cx, cy, -r, -control, p);
|
p.close();
|
}
|
|
private void addLeftCurve(float cx, float cy, float r, float control, Path path) {
|
path.cubicTo(
|
cx - control, cy - r,
|
cx - r, cy - control,
|
cx - r, cy);
|
}
|
|
private void addRightCurve(float cx, float cy, float r, float control, Path path) {
|
path.cubicTo(
|
cx - r, cy + control,
|
cx - control, cy + r,
|
cx, cy + r);
|
}
|
|
@Override
|
protected AnimatorUpdateListener newUpdateListener(Rect startRect, Rect endRect,
|
float endR, Path outPath) {
|
|
float startCX = startRect.exactCenterX();
|
float startCY = startRect.exactCenterY();
|
float startR = startRect.width() / 2f;
|
float startControl = startR - startR * mRadiusRatio;
|
float startHShift = 0;
|
float startVShift = 0;
|
|
float endCX = endRect.exactCenterX();
|
float endCY = endRect.exactCenterY();
|
// Approximate corner circle using bezier curves
|
// http://spencermortensen.com/articles/bezier-circle/
|
float endControl = endR * 0.551915024494f;
|
float endHShift = endRect.width() / 2f - endR;
|
float endVShift = endRect.height() / 2f - endR;
|
|
return (anim) -> {
|
float progress = (Float) anim.getAnimatedValue();
|
|
float cx = (1 - progress) * startCX + progress * endCX;
|
float cy = (1 - progress) * startCY + progress * endCY;
|
float r = (1 - progress) * startR + progress * endR;
|
float control = (1 - progress) * startControl + progress * endControl;
|
float hShift = (1 - progress) * startHShift + progress * endHShift;
|
float vShift = (1 - progress) * startVShift + progress * endVShift;
|
|
outPath.moveTo(cx, cy - vShift - r);
|
outPath.rLineTo(-hShift, 0);
|
|
addLeftCurve(cx - hShift, cy - vShift, r, control, outPath);
|
outPath.rLineTo(0, vShift + vShift);
|
|
addRightCurve(cx - hShift, cy + vShift, r, control, outPath);
|
outPath.rLineTo(hShift + hShift, 0);
|
|
addLeftCurve(cx + hShift, cy + vShift, -r, -control, outPath);
|
outPath.rLineTo(0, -vShift - vShift);
|
|
addRightCurve(cx + hShift, cy - vShift, -r, -control, outPath);
|
outPath.close();
|
};
|
}
|
}
|
|
/**
|
* Initializes the shape which is closest to the {@link AdaptiveIconDrawable}
|
*/
|
public static void init(Context context) {
|
if (!Utilities.ATLEAST_OREO) {
|
return;
|
}
|
pickBestShape(context);
|
}
|
|
private static IconShape getShapeDefinition(String type, float radius) {
|
switch (type) {
|
case "Circle":
|
return new Circle();
|
case "RoundedSquare":
|
return new RoundedSquare(radius);
|
case "TearDrop":
|
return new TearDrop(radius);
|
case "Squircle":
|
return new Squircle(radius);
|
default:
|
throw new IllegalArgumentException("Invalid shape type: " + type);
|
}
|
}
|
|
private static List<IconShape> getAllShapes(Context context) {
|
ArrayList<IconShape> result = new ArrayList<>();
|
try (XmlResourceParser parser = context.getResources().getXml(R.xml.folder_shapes)) {
|
|
// Find the root tag
|
int type;
|
while ((type = parser.next()) != XmlPullParser.END_TAG
|
&& type != XmlPullParser.END_DOCUMENT
|
&& !"shapes".equals(parser.getName()));
|
|
final int depth = parser.getDepth();
|
int[] radiusAttr = new int[] {R.attr.folderIconRadius};
|
IntArray keysToIgnore = new IntArray(0);
|
|
while (((type = parser.next()) != XmlPullParser.END_TAG ||
|
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
|
|
if (type == XmlPullParser.START_TAG) {
|
AttributeSet attrs = Xml.asAttributeSet(parser);
|
TypedArray a = context.obtainStyledAttributes(attrs, radiusAttr);
|
IconShape shape = getShapeDefinition(parser.getName(), a.getFloat(0, 1));
|
a.recycle();
|
|
shape.mAttrs = Themes.createValueMap(context, attrs, keysToIgnore);
|
result.add(shape);
|
}
|
}
|
} catch (IOException | XmlPullParserException e) {
|
throw new RuntimeException(e);
|
}
|
return result;
|
}
|
|
@TargetApi(Build.VERSION_CODES.O)
|
protected static void pickBestShape(Context context) {
|
// Pick any large size
|
final int size = 200;
|
|
Region full = new Region(0, 0, size, size);
|
Region iconR = new Region();
|
AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
|
new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
|
drawable.setBounds(0, 0, size, size);
|
iconR.setPath(drawable.getIconMask(), full);
|
|
Path shapePath = new Path();
|
Region shapeR = new Region();
|
|
// Find the shape with minimum area of divergent region.
|
int minArea = Integer.MAX_VALUE;
|
IconShape closestShape = null;
|
for (IconShape shape : getAllShapes(context)) {
|
shapePath.reset();
|
shape.addToPath(shapePath, 0, 0, size / 2f);
|
shapeR.setPath(shapePath, full);
|
shapeR.op(iconR, Op.XOR);
|
|
int area = GraphicsUtils.getArea(shapeR);
|
if (area < minArea) {
|
minArea = area;
|
closestShape = shape;
|
}
|
}
|
|
if (closestShape != null) {
|
sInstance = closestShape;
|
}
|
|
// Initialize shape properties
|
drawable.setBounds(0, 0, DEFAULT_PATH_SIZE, DEFAULT_PATH_SIZE);
|
sShapePath = new Path(drawable.getIconMask());
|
sNormalizationScale = IconNormalizer.normalizeAdaptiveIcon(drawable, size, null);
|
}
|
}
|