/* * Copyright (C) 2017 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 android.graphics.drawable; import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.app.ActivityThread; import android.content.pm.ActivityInfo.Config; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.BlendMode; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.Region; import android.graphics.Shader; import android.graphics.Shader.TileMode; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.PathParser; import com.android.internal.R; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; /** *
This class can also be created via XML inflation using <adaptive-icon>
tag
* in addition to dynamic creation.
*
*
This drawable supports two drawable layers: foreground and background. The layers are clipped * when rendering using the mask defined in the device configuration. * *
* Rect(getBounds().left - getBounds().getWidth() * #getExtraInsetFraction(), * getBounds().top - getBounds().getHeight() * #getExtraInsetFraction(), * getBounds().right + getBounds().getWidth() * #getExtraInsetFraction(), * getBounds().bottom + getBounds().getHeight() * #getExtraInsetFraction()) ** *
An alternate drawable can be specified using <monochrome>
tag which can be
* drawn in place of the two (background and foreground) layers. This drawable is tinted
* according to the device or surface theme.
*/
public class AdaptiveIconDrawable extends Drawable implements Drawable.Callback {
/**
* Mask path is defined inside device configuration in following dimension: [100 x 100]
* @hide
*/
@TestApi
public static final float MASK_SIZE = 100f;
/**
* Launcher icons design guideline
*/
private static final float SAFEZONE_SCALE = 66f/72f;
/**
* All four sides of the layers are padded with extra inset so as to provide
* extra content to reveal within the clip path when performing affine transformations on the
* layers.
*
* Each layers will reserve 25% of it's width and height.
*
* As a result, the view port of the layers is smaller than their intrinsic width and height.
*/
private static final float EXTRA_INSET_PERCENTAGE = 1 / 4f;
private static final float DEFAULT_VIEW_PORT_SCALE = 1f / (1 + 2 * EXTRA_INSET_PERCENTAGE);
/**
* Clip path defined in R.string.config_icon_mask.
*/
private static Path sMask;
/**
* Scaled mask based on the view bounds.
*/
private final Path mMask;
private final Path mMaskScaleOnly;
private final Matrix mMaskMatrix;
private final Region mTransparentRegion;
/**
* Indices used to access {@link #mLayerState.mChildDrawable} array for foreground and
* background layer.
*/
private static final int BACKGROUND_ID = 0;
private static final int FOREGROUND_ID = 1;
private static final int MONOCHROME_ID = 2;
/**
* State variable that maintains the {@link ChildDrawable} array.
*/
LayerState mLayerState;
private Shader mLayersShader;
private Bitmap mLayersBitmap;
private final Rect mTmpOutRect = new Rect();
private Rect mHotspotBounds;
private boolean mMutated;
private boolean mSuspendChildInvalidation;
private boolean mChildRequestedInvalidation;
private final Canvas mCanvas;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG |
Paint.FILTER_BITMAP_FLAG);
/**
* Constructor used for xml inflation.
*/
AdaptiveIconDrawable() {
this((LayerState) null, null);
}
/**
* The one constructor to rule them all. This is called by all public
* constructors to set the state and initialize local properties.
*/
AdaptiveIconDrawable(@Nullable LayerState state, @Nullable Resources res) {
mLayerState = createConstantState(state, res);
// config_icon_mask from context bound resource may have been chaged using
// OverlayManager. Read that one first.
Resources r = ActivityThread.currentActivityThread() == null
? Resources.getSystem()
: ActivityThread.currentActivityThread().getApplication().getResources();
// TODO: either make sMask update only when config_icon_mask changes OR
// get rid of it all-together in layoutlib
sMask = PathParser.createPathFromPathData(r.getString(R.string.config_icon_mask));
mMask = new Path(sMask);
mMaskScaleOnly = new Path(mMask);
mMaskMatrix = new Matrix();
mCanvas = new Canvas();
mTransparentRegion = new Region();
}
private ChildDrawable createChildDrawable(Drawable drawable) {
final ChildDrawable layer = new ChildDrawable(mLayerState.mDensity);
layer.mDrawable = drawable;
layer.mDrawable.setCallback(this);
mLayerState.mChildrenChangingConfigurations |=
layer.mDrawable.getChangingConfigurations();
return layer;
}
LayerState createConstantState(@Nullable LayerState state, @Nullable Resources res) {
return new LayerState(state, this, res);
}
/**
* Constructor used to dynamically create this drawable.
*
* @param backgroundDrawable drawable that should be rendered in the background
* @param foregroundDrawable drawable that should be rendered in the foreground
*/
public AdaptiveIconDrawable(Drawable backgroundDrawable,
Drawable foregroundDrawable) {
this(backgroundDrawable, foregroundDrawable, null);
}
/**
* Constructor used to dynamically create this drawable.
*
* @param backgroundDrawable drawable that should be rendered in the background
* @param foregroundDrawable drawable that should be rendered in the foreground
* @param monochromeDrawable an alternate drawable which can be tinted per system theme color
*/
public AdaptiveIconDrawable(@Nullable Drawable backgroundDrawable,
@Nullable Drawable foregroundDrawable, @Nullable Drawable monochromeDrawable) {
this((LayerState)null, null);
if (backgroundDrawable != null) {
addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable));
}
if (foregroundDrawable != null) {
addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable));
}
if (monochromeDrawable != null) {
addLayer(MONOCHROME_ID, createChildDrawable(monochromeDrawable));
}
}
/**
* Sets the layer to the {@param index} and invalidates cache.
*
* @param index The index of the layer.
* @param layer The layer to add.
*/
private void addLayer(int index, @NonNull ChildDrawable layer) {
mLayerState.mChildren[index] = layer;
mLayerState.invalidateCache();
}
@Override
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
super.inflate(r, parser, attrs, theme);
final LayerState state = mLayerState;
if (state == null) {
return;
}
// The density may have changed since the last update. This will
// apply scaling to any existing constant state properties.
final int deviceDensity = Drawable.resolveDensity(r, 0);
state.setDensity(deviceDensity);
state.mSrcDensityOverride = mSrcDensityOverride;
state.mSourceDrawableId = Resources.getAttributeSetSourceResId(attrs);
final ChildDrawable[] array = state.mChildren;
for (int i = 0; i < array.length; i++) {
array[i].setDensity(deviceDensity);
}
inflateLayers(r, parser, attrs, theme);
}
/**
* All four sides of the layers are padded with extra inset so as to provide
* extra content to reveal within the clip path when performing affine transformations on the
* layers.
*
* @see #getForeground() and #getBackground() for more info on how this value is used
*/
public static float getExtraInsetFraction() {
return EXTRA_INSET_PERCENTAGE;
}
/**
* @hide
*/
public static float getExtraInsetPercentage() {
return EXTRA_INSET_PERCENTAGE;
}
/**
* When called before the bound is set, the returned path is identical to
* R.string.config_icon_mask. After the bound is set, the
* returned path's computed bound is same as the #getBounds().
*
* @return the mask path object used to clip the drawable
*/
public Path getIconMask() {
return mMask;
}
/**
* Returns the foreground drawable managed by this class. The bound of this drawable is
* extended by {@link #getExtraInsetFraction()} * getBounds().width on left/right sides and by
* {@link #getExtraInsetFraction()} * getBounds().height on top/bottom sides.
*
* @return the foreground drawable managed by this drawable
*/
public Drawable getForeground() {
return mLayerState.mChildren[FOREGROUND_ID].mDrawable;
}
/**
* Returns the foreground drawable managed by this class. The bound of this drawable is
* extended by {@link #getExtraInsetFraction()} * getBounds().width on left/right sides and by
* {@link #getExtraInsetFraction()} * getBounds().height on top/bottom sides.
*
* @return the background drawable managed by this drawable
*/
public Drawable getBackground() {
return mLayerState.mChildren[BACKGROUND_ID].mDrawable;
}
/**
* Returns the monochrome version of this drawable. Callers can use a tinted version of
* this drawable instead of the original drawable on surfaces stressing user theming.
*
* @return the monochrome drawable
*/
@Nullable
public Drawable getMonochrome() {
return mLayerState.mChildren[MONOCHROME_ID].mDrawable;
}
@Override
protected void onBoundsChange(Rect bounds) {
if (bounds.isEmpty()) {
return;
}
updateLayerBounds(bounds);
}
private void updateLayerBounds(Rect bounds) {
if (bounds.isEmpty()) {
return;
}
try {
suspendChildInvalidation();
updateLayerBoundsInternal(bounds);
updateMaskBoundsInternal(bounds);
} finally {
resumeChildInvalidation();
}
}
/**
* Set the child layer bounds bigger than the view port size by {@link #DEFAULT_VIEW_PORT_SCALE}
*/
private void updateLayerBoundsInternal(Rect bounds) {
int cX = bounds.width() / 2;
int cY = bounds.height() / 2;
for (int i = 0, count = mLayerState.N_CHILDREN; i < count; i++) {
final ChildDrawable r = mLayerState.mChildren[i];
final Drawable d = r.mDrawable;
if (d == null) {
continue;
}
int insetWidth = (int) (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2));
int insetHeight = (int) (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2));
final Rect outRect = mTmpOutRect;
outRect.set(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight);
d.setBounds(outRect);
}
}
private void updateMaskBoundsInternal(Rect b) {
// reset everything that depends on the view bounds
mMaskMatrix.setScale(b.width() / MASK_SIZE, b.height() / MASK_SIZE);
sMask.transform(mMaskMatrix, mMaskScaleOnly);
mMaskMatrix.postTranslate(b.left, b.top);
sMask.transform(mMaskMatrix, mMask);
if (mLayersBitmap == null || mLayersBitmap.getWidth() != b.width()
|| mLayersBitmap.getHeight() != b.height()) {
mLayersBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888);
}
mPaint.setShader(null);
mTransparentRegion.setEmpty();
mLayersShader = null;
}
@Override
public void draw(Canvas canvas) {
if (mLayersBitmap == null) {
return;
}
if (mLayersShader == null) {
mCanvas.setBitmap(mLayersBitmap);
mCanvas.drawColor(Color.BLACK);
if (mLayerState.mChildren[BACKGROUND_ID].mDrawable != null) {
mLayerState.mChildren[BACKGROUND_ID].mDrawable.draw(mCanvas);
}
if (mLayerState.mChildren[FOREGROUND_ID].mDrawable != null) {
mLayerState.mChildren[FOREGROUND_ID].mDrawable.draw(mCanvas);
}
mLayersShader = new BitmapShader(mLayersBitmap, TileMode.CLAMP, TileMode.CLAMP);
mPaint.setShader(mLayersShader);
}
if (mMaskScaleOnly != null) {
Rect bounds = getBounds();
canvas.translate(bounds.left, bounds.top);
canvas.drawPath(mMaskScaleOnly, mPaint);
canvas.translate(-bounds.left, -bounds.top);
}
}
@Override
public void invalidateSelf() {
mLayersShader = null;
super.invalidateSelf();
}
@Override
public void getOutline(@NonNull Outline outline) {
outline.setPath(mMask);
}
/** @hide */
@TestApi
public Region getSafeZone() {
Path mask = getIconMask();
mMaskMatrix.setScale(SAFEZONE_SCALE, SAFEZONE_SCALE, getBounds().centerX(), getBounds().centerY());
Path p = new Path();
mask.transform(mMaskMatrix, p);
Region safezoneRegion = new Region(getBounds());
safezoneRegion.setPath(p, safezoneRegion);
return safezoneRegion;
}
@Override
public @Nullable Region getTransparentRegion() {
if (mTransparentRegion.isEmpty()) {
mMask.toggleInverseFillType();
mTransparentRegion.set(getBounds());
mTransparentRegion.setPath(mMask, mTransparentRegion);
mMask.toggleInverseFillType();
}
return mTransparentRegion;
}
@Override
public void applyTheme(@NonNull Theme t) {
super.applyTheme(t);
final LayerState state = mLayerState;
if (state == null) {
return;
}
final int density = Drawable.resolveDensity(t.getResources(), 0);
state.setDensity(density);
final ChildDrawable[] array = state.mChildren;
for (int i = 0; i < state.N_CHILDREN; i++) {
final ChildDrawable layer = array[i];
layer.setDensity(density);
if (layer.mThemeAttrs != null) {
final TypedArray a = t.resolveAttributes(
layer.mThemeAttrs, R.styleable.AdaptiveIconDrawableLayer);
updateLayerFromTypedArray(layer, a);
a.recycle();
}
final Drawable d = layer.mDrawable;
if (d != null && d.canApplyTheme()) {
d.applyTheme(t);
// Update cached mask of child changing configurations.
state.mChildrenChangingConfigurations |= d.getChangingConfigurations();
}
}
}
/**
* If the drawable was inflated from XML, this returns the resource ID for the drawable
*
* @hide
*/
@DrawableRes
public int getSourceDrawableResId() {
final LayerState state = mLayerState;
return state == null ? Resources.ID_NULL : state.mSourceDrawableId;
}
/**
* Inflates child layers using the specified parser.
*/
private void inflateLayers(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
final LayerState state = mLayerState;
final int innerDepth = parser.getDepth() + 1;
int type;
int depth;
int childIndex = 0;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth) {
continue;
}
String tagName = parser.getName();
switch (tagName) {
case "background":
childIndex = BACKGROUND_ID;
break;
case "foreground":
childIndex = FOREGROUND_ID;
break;
case "monochrome":
childIndex = MONOCHROME_ID;
break;
default:
continue;
}
final ChildDrawable layer = new ChildDrawable(state.mDensity);
final TypedArray a = obtainAttributes(r, theme, attrs,
R.styleable.AdaptiveIconDrawableLayer);
updateLayerFromTypedArray(layer, a);
a.recycle();
// If the layer doesn't have a drawable or unresolved theme
// attribute for a drawable, attempt to parse one from the child
// element. If multiple child elements exist, we'll only use the
// first one.
if (layer.mDrawable == null && (layer.mThemeAttrs == null)) {
while ((type = parser.next()) == XmlPullParser.TEXT) {
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException(parser.getPositionDescription()
+ ":