blob: ae279cb3dc04c3e8ac130743be31fcfcce773e48 [file] [log] [blame]
/*
* 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.folder;
import static com.android.launcher3.Workspace.MAP_NO_RECURSE;
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.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.RegionIterator;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.view.ViewOutlineProvider;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.MainThreadExecutor;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
/**
* Abstract representation of the shape of a folder icon
*/
public abstract class FolderShape {
private static FolderShape sInstance = new Circle();
public static FolderShape getShape() {
return sInstance;
}
private static FolderShape[] getAllShapes() {
return new FolderShape[] {
new Circle(),
new RoundedSquare(8f / 50), // Ratios based on path defined in config_icon_mask
new RoundedSquare(30f / 50),
new Square(),
new TearDrop(),
new Squircle()};
}
public abstract void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,
Paint paint);
public abstract void addShape(Path path, float offsetX, float offsetY, float radius);
public abstract Animator createRevealAnimator(Folder target, Rect startRect, Rect endRect,
float endRadius, boolean isReversed);
/**
* Abstract shape where the reveal animation is a derivative of a round rect animation
*/
private static abstract class SimpleRectShape extends FolderShape {
@Override
public final Animator createRevealAnimator(Folder 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 FolderShape {
private final Path mTmpPath = new Path();
@Override
public final void drawShape(Canvas canvas, float offsetX, float offsetY, float radius,
Paint paint) {
mTmpPath.reset();
addShape(mTmpPath, offsetX, offsetY, radius);
canvas.drawPath(mTmpPath, paint);
}
protected abstract AnimatorUpdateListener newUpdateListener(
Rect startRect, Rect endRect, float endRadius, Path outPath);
@Override
public final Animator createRevealAnimator(Folder 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 addShape(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;
}
}
public static class Square extends SimpleRectShape {
@Override
public void drawShape(Canvas canvas, float offsetX, float offsetY, float radius, Paint p) {
float cx = radius + offsetX;
float cy = radius + offsetY;
canvas.drawRect(cx - radius, cy - radius, cx + radius, cy + radius, p);
}
@Override
public void addShape(Path path, float offsetX, float offsetY, float radius) {
float cx = radius + offsetX;
float cy = radius + offsetY;
path.addRect(cx - radius, cy - radius, cx + radius, cy + radius, Path.Direction.CW);
}
@Override
protected float getStartRadius(Rect startRect) {
return 0;
}
}
public static class RoundedSquare extends SimpleRectShape {
/**
* Ratio of corner radius to half size. Based on the
*/
private final float mRadiusFactor;
public RoundedSquare(float radiusFactor) {
mRadiusFactor = radiusFactor;
}
@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 * mRadiusFactor;
canvas.drawRoundRect(cx - radius, cy - radius, cx + radius, cy + radius, cr, cr, p);
}
@Override
public void addShape(Path path, float offsetX, float offsetY, float radius) {
float cx = radius + offsetX;
float cy = radius + offsetY;
float cr = radius * mRadiusFactor;
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) * mRadiusFactor;
}
}
public static class TearDrop extends PathShape {
/**
* Radio of short radius to large radius, based on the shape options defined in the config.
*/
private static final float RADIUS_RATIO = 15f / 50;
private final float[] mTempRadii = new float[8];
@Override
public void addShape(Path p, float offsetX, float offsetY, float r1) {
float r2 = r1 * RADIUS_RATIO;
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 * RADIUS_RATIO;
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 static final float RADIUS_RATIO = 10f / 50;
@Override
public void addShape(Path p, float offsetX, float offsetY, float r) {
float cx = r + offsetX;
float cy = r + offsetY;
float control = r - r * RADIUS_RATIO;
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 * RADIUS_RATIO;
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 closest to the {@link AdaptiveIconDrawable}
*/
public static void init() {
if (!Utilities.ATLEAST_OREO) {
return;
}
new MainThreadExecutor().execute(FolderShape::pickShapeInBackground);
}
@TargetApi(Build.VERSION_CODES.O)
protected static void pickShapeInBackground() {
// Pick any large size
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();
Rect tempRect = new Rect();
// Find the shape with minimum area of divergent region.
int minArea = Integer.MAX_VALUE;
FolderShape closestShape = null;
for (FolderShape shape : getAllShapes()) {
shapePath.reset();
shape.addShape(shapePath, 0, 0, size / 2f);
shapeR.setPath(shapePath, full);
shapeR.op(iconR, Op.XOR);
RegionIterator itr = new RegionIterator(shapeR);
int area = 0;
while (itr.next(tempRect)) {
area += tempRect.width() * tempRect.height();
}
if (area < minArea) {
minArea = area;
closestShape = shape;
}
}
if (closestShape != null) {
FolderShape shape = closestShape;
new MainThreadExecutor().execute(() -> updateFolderShape(shape));
}
}
private static void updateFolderShape(FolderShape shape) {
sInstance = shape;
LauncherAppState app = LauncherAppState.getInstanceNoCreate();
if (app == null) {
return;
}
Launcher launcher = (Launcher) app.getModel().getCallback();
if (launcher != null) {
launcher.getWorkspace().mapOverItems(MAP_NO_RECURSE, (i, v) -> {
if (v instanceof FolderIcon) {
v.invalidate();
}
return false;
});
}
}
}