diff --git a/api/current.txt b/api/current.txt index e56eb36b91edc..93993b01f1514 100644 --- a/api/current.txt +++ b/api/current.txt @@ -13640,6 +13640,23 @@ package android.graphics.drawable { method public void addLevel(int, int, android.graphics.drawable.Drawable); } + public class MaskableIconDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback { + ctor public MaskableIconDrawable(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable); + method public void draw(android.graphics.Canvas); + method public android.graphics.drawable.Drawable getBackground(); + method public android.graphics.drawable.Drawable getForeground(); + method public android.graphics.Path getIconMask(); + method public int getOpacity(); + method public void invalidateDrawable(android.graphics.drawable.Drawable); + method public void scheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable, long); + method public void setAlpha(int); + method public void setColorFilter(android.graphics.ColorFilter); + method public void setOpacity(int); + method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable); + field public static final float DEFAULT_VIEW_PORT_SCALE = 0.6666667f; + field public static final float MASK_SIZE = 100.0f; + } + public class NinePatchDrawable extends android.graphics.drawable.Drawable { ctor public deprecated NinePatchDrawable(android.graphics.Bitmap, byte[], android.graphics.Rect, java.lang.String); ctor public NinePatchDrawable(android.content.res.Resources, android.graphics.Bitmap, byte[], android.graphics.Rect, java.lang.String); diff --git a/api/system-current.txt b/api/system-current.txt index 9f78ff22286ec..0695024eea936 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -14183,6 +14183,23 @@ package android.graphics.drawable { method public void addLevel(int, int, android.graphics.drawable.Drawable); } + public class MaskableIconDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback { + ctor public MaskableIconDrawable(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable); + method public void draw(android.graphics.Canvas); + method public android.graphics.drawable.Drawable getBackground(); + method public android.graphics.drawable.Drawable getForeground(); + method public android.graphics.Path getIconMask(); + method public int getOpacity(); + method public void invalidateDrawable(android.graphics.drawable.Drawable); + method public void scheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable, long); + method public void setAlpha(int); + method public void setColorFilter(android.graphics.ColorFilter); + method public void setOpacity(int); + method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable); + field public static final float DEFAULT_VIEW_PORT_SCALE = 0.6666667f; + field public static final float MASK_SIZE = 100.0f; + } + public class NinePatchDrawable extends android.graphics.drawable.Drawable { ctor public deprecated NinePatchDrawable(android.graphics.Bitmap, byte[], android.graphics.Rect, java.lang.String); ctor public NinePatchDrawable(android.content.res.Resources, android.graphics.Bitmap, byte[], android.graphics.Rect, java.lang.String); diff --git a/api/test-current.txt b/api/test-current.txt index 72b2a6244a3f2..6136542b468d4 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -13672,6 +13672,23 @@ package android.graphics.drawable { method public void addLevel(int, int, android.graphics.drawable.Drawable); } + public class MaskableIconDrawable extends android.graphics.drawable.Drawable implements android.graphics.drawable.Drawable.Callback { + ctor public MaskableIconDrawable(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable); + method public void draw(android.graphics.Canvas); + method public android.graphics.drawable.Drawable getBackground(); + method public android.graphics.drawable.Drawable getForeground(); + method public android.graphics.Path getIconMask(); + method public int getOpacity(); + method public void invalidateDrawable(android.graphics.drawable.Drawable); + method public void scheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable, long); + method public void setAlpha(int); + method public void setColorFilter(android.graphics.ColorFilter); + method public void setOpacity(int); + method public void unscheduleDrawable(android.graphics.drawable.Drawable, java.lang.Runnable); + field public static final float DEFAULT_VIEW_PORT_SCALE = 0.6666667f; + field public static final float MASK_SIZE = 100.0f; + } + public class NinePatchDrawable extends android.graphics.drawable.Drawable { ctor public deprecated NinePatchDrawable(android.graphics.Bitmap, byte[], android.graphics.Rect, java.lang.String); ctor public NinePatchDrawable(android.content.res.Resources, android.graphics.Bitmap, byte[], android.graphics.Rect, java.lang.String); diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 87c2b255115ae..f9df5acf7fe1a 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -5843,6 +5843,14 @@ + + + + + + + + diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 7de48d3305bb0..2c4b159adfa74 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2711,6 +2711,9 @@ com.android.cellbroadcastreceiver/.PrivilegedCellBroadcastReceiver + + "M50,0L100,0 100,100 0,100 0,0z" + diff --git a/graphics/java/android/graphics/drawable/DrawableInflater.java b/graphics/java/android/graphics/drawable/DrawableInflater.java index 348af70d55688..6d0bbdf549f28 100644 --- a/graphics/java/android/graphics/drawable/DrawableInflater.java +++ b/graphics/java/android/graphics/drawable/DrawableInflater.java @@ -147,6 +147,8 @@ public final class DrawableInflater { return new TransitionDrawable(); case "ripple": return new RippleDrawable(); + case "maskable-icon": + return new MaskableIconDrawable(); case "color": return new ColorDrawable(); case "shape": diff --git a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java new file mode 100644 index 0000000000000..3467b1a789e74 --- /dev/null +++ b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java @@ -0,0 +1,1001 @@ +/* + * 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 static android.graphics.drawable.Drawable.obtainAttributes; + +import android.annotation.NonNull; +import android.annotation.Nullable; +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.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.PorterDuff.Mode; +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 drawable supports two layers: foreground and background. + * + *

The layers are clipped when rendering using the mask path defined in the device configuration. + * + *

This class can also be created via XML inflation using <maskable-icon> tag + * in addition to dynamic creation. + */ +public class MaskableIconDrawable extends Drawable implements Drawable.Callback { + + /** + * Mask path is defined inside device configuration in following dimension: [100 x 100] + */ + public static final float MASK_SIZE = 100f; + + /** + * The view port of the layers is smaller than their intrinsic width and height by this factor. + * + * It is part of the API contract that all four sides of the layers are padded so as to provide + * extra content to reveal within the clip path when performing affine transformations on the + * layers. + */ + public static final float DEFAULT_VIEW_PORT_SCALE = 2f / 3f; + + /** + * Clip path defined in {@link com.android.internal.R.string.config_icon_mask}. + */ + private static Path sMask; + + /** + * Scaled mask based on the view bounds. + */ + private final Path mMask; + private final Matrix mMaskMatrix; + private final Region mTransparentRegion; + + private Bitmap mMaskBitmap; + + /** + * 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; + + /** + * 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. + */ + MaskableIconDrawable() { + 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. + */ + MaskableIconDrawable(@Nullable LayerState state, @Nullable Resources res) { + mLayerState = createConstantState(state, res); + + if (sMask == null) { + sMask = PathParser.createPathFromPathData( + Resources.getSystem().getString(com.android.internal.R.string.config_icon_mask)); + } + mMask = new Path(); + 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 MaskableIconDrawable(Drawable backgroundDrawable, + Drawable foregroundDrawable) { + this((LayerState)null, null); + addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable)); + addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable)); + } + + /** + * 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 density = Drawable.resolveDensity(r, 0); + state.setDensity(density); + + final ChildDrawable[] array = state.mChildren; + for (int i = 0; i < state.mChildren.length; i++) { + final ChildDrawable layer = array[i]; + layer.setDensity(density); + } + inflateLayers(r, parser, attrs, theme); + } + + /** + * @return the mask path object used to clip the drawable + */ + public Path getIconMask() { + return mMask; + } + + /** + * @return the foreground drawable managed by this drawable + */ + public Drawable getForeground() { + return mLayerState.mChildren[FOREGROUND_ID].mDrawable; + } + + /** + * @return the background drawable managed by this drawable + */ + public Drawable getBackground() { + return mLayerState.mChildren[BACKGROUND_ID].mDrawable; + } + + @Override + protected void onBoundsChange(Rect bounds) { + if (bounds.isEmpty()) { + return; + } + updateLayerBounds(bounds); + } + + private void updateLayerBounds(Rect bounds) { + try { + suspendChildInvalidation(); + updateLayerBoundsInternal(bounds); + updateMaskBoundsInternal(bounds); + } finally { + resumeChildInvalidation(); + } + } + + private void updateLayerBoundsInternal(Rect bounds) { + int cX = bounds.centerX(); + int cY = bounds.centerY(); + + for (int i = 0, count = mLayerState.N_CHILDREN; i < count; i++) { + 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); + + final ChildDrawable r = mLayerState.mChildren[i]; + final Drawable d = r.mDrawable; + d.setBounds(outRect); + } + } + + private void updateMaskBoundsInternal(Rect b) { + mMaskMatrix.setScale(b.width() / MASK_SIZE, b.height() / MASK_SIZE); + sMask.transform(mMaskMatrix, mMask); + + mMaskBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ALPHA_8); + mCanvas.setBitmap(mMaskBitmap); + mPaint.setShader(null); + mCanvas.drawPath(mMask, mPaint); + + // reset everything that depends on the view bounds + mTransparentRegion.setEmpty(); + mLayersShader = null; + mLayersBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888); + } + + @Override + public void draw(Canvas canvas) { + if (mLayersShader == null) { + mCanvas.setBitmap(mLayersBitmap); + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = mLayerState.mChildren[i].mDrawable; + if (dr != null) { + dr.draw(mCanvas); + } + } + mLayersShader = new BitmapShader(mLayersBitmap, TileMode.CLAMP, TileMode.CLAMP); + mPaint.setShader(mLayersShader); + } + canvas.drawBitmap(mMaskBitmap, 0.0f, 0.0f, mPaint); + } + + @Override + public void invalidateSelf() { + mLayersShader = null; + super.invalidateSelf(); + } + + @Override + public void getOutline(@NonNull Outline outline) { + outline.setConvexPath(mMask); + } + + @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.MaskableIconDrawableLayer); + 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(); + } + } + } + + /** + * Inflates child layers using the specified parser. + */ + 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(); + if (tagName.equals("background")) { + childIndex = BACKGROUND_ID; + } else if (tagName.equals("foreground")) { + childIndex = FOREGROUND_ID; + } else { + continue; + } + + final ChildDrawable layer = new ChildDrawable(state.mDensity); + final TypedArray a = obtainAttributes(r, theme, attrs, + R.styleable.MaskableIconDrawableLayer); + 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() + + ": or tag requires a 'color' or 'drawable'" + + "attribute or child tag defining a drawable"); + } + + // We found a child drawable. Take ownership. + layer.mDrawable = Drawable.createFromXmlInner(r, parser, attrs, theme); + layer.mDrawable.setCallback(this); + state.mChildrenChangingConfigurations |= + layer.mDrawable.getChangingConfigurations(); + } + addLayer(childIndex, layer); + } + } + + private void updateLayerFromTypedArray(@NonNull ChildDrawable layer, @NonNull TypedArray a) { + final LayerState state = mLayerState; + + // Account for any configuration changes. + state.mChildrenChangingConfigurations |= a.getChangingConfigurations(); + + // Extract the theme attributes, if any. + layer.mThemeAttrs = a.extractThemeAttrs(); + + Drawable dr = a.getDrawable(R.styleable.MaskableIconDrawableLayer_drawable); + if (dr == null) { + int color = a.getColor(R.styleable.MaskableIconDrawableLayer_color, Color.TRANSPARENT); + if (color != Color.TRANSPARENT) { + dr = new ColorDrawable(color); + } + } + if (dr != null) { + if (layer.mDrawable != null) { + // It's possible that a drawable was already set, in which case + // we should clear the callback. We may have also integrated the + // drawable's changing configurations, but we don't have enough + // information to revert that change. + layer.mDrawable.setCallback(null); + } + + // Take ownership of the new drawable. + layer.mDrawable = dr; + layer.mDrawable.setCallback(this); + state.mChildrenChangingConfigurations |= + layer.mDrawable.getChangingConfigurations(); + } + } + + @Override + public boolean canApplyTheme() { + return (mLayerState != null && mLayerState.canApplyTheme()) || super.canApplyTheme(); + } + + /** + * @hide + */ + @Override + public boolean isProjected() { + if (super.isProjected()) { + return true; + } + + final ChildDrawable[] layers = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + if (layers[i].mDrawable.isProjected()) { + return true; + } + } + return false; + } + + /** + * Temporarily suspends child invalidation. + * + * @see #resumeChildInvalidation() + */ + private void suspendChildInvalidation() { + mSuspendChildInvalidation = true; + } + + /** + * Resumes child invalidation after suspension, immediately performing an + * invalidation if one was requested by a child during suspension. + * + * @see #suspendChildInvalidation() + */ + private void resumeChildInvalidation() { + mSuspendChildInvalidation = false; + + if (mChildRequestedInvalidation) { + mChildRequestedInvalidation = false; + invalidateSelf(); + } + } + + @Override + public void invalidateDrawable(@NonNull Drawable who) { + if (mSuspendChildInvalidation) { + mChildRequestedInvalidation = true; + } else { + invalidateSelf(); + } + } + + @Override + public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + unscheduleSelf(what); + } + + @Override + public @Config int getChangingConfigurations() { + return super.getChangingConfigurations() | mLayerState.getChangingConfigurations(); + } + + @Override + public void setHotspot(float x, float y) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setHotspot(x, y); + } + } + } + + @Override + public void setHotspotBounds(int left, int top, int right, int bottom) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setHotspotBounds(left, top, right, bottom); + } + } + + if (mHotspotBounds == null) { + mHotspotBounds = new Rect(left, top, right, bottom); + } else { + mHotspotBounds.set(left, top, right, bottom); + } + } + + @Override + public void getHotspotBounds(Rect outRect) { + if (mHotspotBounds != null) { + outRect.set(mHotspotBounds); + } else { + super.getHotspotBounds(outRect); + } + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + final boolean changed = super.setVisible(visible, restart); + final ChildDrawable[] array = mLayerState.mChildren; + + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setVisible(visible, restart); + } + } + + return changed; + } + + @Override + public void setDither(boolean dither) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setDither(dither); + } + } + } + + @Override + public void setAlpha(int alpha) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setAlpha(alpha); + } + } + } + + @Override + public int getAlpha() { + final Drawable dr = getFirstNonNullDrawable(); + if (dr != null) { + return dr.getAlpha(); + } else { + return super.getAlpha(); + } + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setColorFilter(colorFilter); + } + } + } + + @Override + public void setTintList(ColorStateList tint) { + final ChildDrawable[] array = mLayerState.mChildren; + final int N = mLayerState.N_CHILDREN; + for (int i = 0; i < N; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setTintList(tint); + } + } + } + + @Override + public void setTintMode(Mode tintMode) { + final ChildDrawable[] array = mLayerState.mChildren; + final int N = mLayerState.N_CHILDREN; + for (int i = 0; i < N; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setTintMode(tintMode); + } + } + } + + private Drawable getFirstNonNullDrawable() { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + return dr; + } + } + return null; + } + + public void setOpacity(int opacity) { + mLayerState.mOpacityOverride = opacity; + } + + @Override + public int getOpacity() { + if (mLayerState.mOpacityOverride != PixelFormat.UNKNOWN) { + return mLayerState.mOpacityOverride; + } + return mLayerState.getOpacity(); + } + + @Override + public void setAutoMirrored(boolean mirrored) { + mLayerState.mAutoMirrored = mirrored; + + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.setAutoMirrored(mirrored); + } + } + } + + @Override + public boolean isAutoMirrored() { + return mLayerState.mAutoMirrored; + } + + @Override + public void jumpToCurrentState() { + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.jumpToCurrentState(); + } + } + } + + @Override + public boolean isStateful() { + return mLayerState.isStateful(); + } + + @Override + protected boolean onStateChange(int[] state) { + boolean changed = false; + + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.isStateful() && dr.setState(state)) { + changed = true; + } + } + + if (changed) { + updateLayerBounds(getBounds()); + } + + return changed; + } + + @Override + protected boolean onLevelChange(int level) { + boolean changed = false; + + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.setLevel(level)) { + changed = true; + } + } + + if (changed) { + updateLayerBounds(getBounds()); + } + + return changed; + } + + @Override + public int getIntrinsicWidth() { + return (int)(getMaxIntrinsicWidth() * DEFAULT_VIEW_PORT_SCALE); + } + + private int getMaxIntrinsicWidth() { + int width = -1; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final ChildDrawable r = mLayerState.mChildren[i]; + final int w = r.mDrawable.getIntrinsicWidth(); + if (w > width) { + width = w; + } + } + return width; + } + + @Override + public int getIntrinsicHeight() { + return (int)(getMaxIntrinsicHeight() * DEFAULT_VIEW_PORT_SCALE); + } + + private int getMaxIntrinsicHeight() { + int height = -1; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final ChildDrawable r = mLayerState.mChildren[i]; + final int h = r.mDrawable.getIntrinsicHeight(); + if (h > height) { + height = h; + } + } + return height; + } + + @Override + public ConstantState getConstantState() { + if (mLayerState.canConstantState()) { + mLayerState.mChangingConfigurations = getChangingConfigurations(); + return mLayerState; + } + return null; + } + + @Override + public Drawable mutate() { + if (!mMutated && super.mutate() == this) { + mLayerState = createConstantState(mLayerState, null); + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = mLayerState.mChildren[i].mDrawable; + if (dr != null) { + dr.mutate(); + } + } + mMutated = true; + } + return this; + } + + /** + * @hide + */ + public void clearMutated() { + super.clearMutated(); + final ChildDrawable[] array = mLayerState.mChildren; + for (int i = 0; i < mLayerState.N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + dr.clearMutated(); + } + } + mMutated = false; + } + + static class ChildDrawable { + public Drawable mDrawable; + public int[] mThemeAttrs; + public int mDensity = DisplayMetrics.DENSITY_DEFAULT; + + ChildDrawable(int density) { + mDensity = density; + } + + ChildDrawable(@NonNull ChildDrawable orig, @NonNull MaskableIconDrawable owner, + @Nullable Resources res) { + + final Drawable dr = orig.mDrawable; + final Drawable clone; + if (dr != null) { + final ConstantState cs = dr.getConstantState(); + if (cs == null) { + clone = dr; + } else if (res != null) { + clone = cs.newDrawable(res); + } else { + clone = cs.newDrawable(); + } + clone.setCallback(owner); + clone.setBounds(dr.getBounds()); + clone.setLevel(dr.getLevel()); + } else { + clone = null; + } + + mDrawable = clone; + mThemeAttrs = orig.mThemeAttrs; + + mDensity = Drawable.resolveDensity(res, orig.mDensity); + } + + public boolean canApplyTheme() { + return mThemeAttrs != null + || (mDrawable != null && mDrawable.canApplyTheme()); + } + + public final void setDensity(int targetDensity) { + if (mDensity != targetDensity) { + mDensity = targetDensity; + } + } + } + + static class LayerState extends ConstantState { + private int[] mThemeAttrs; + + final static int N_CHILDREN = 2; + ChildDrawable[] mChildren; + + int mDensity; + int mOpacityOverride = PixelFormat.UNKNOWN; + + @Config int mChangingConfigurations; + @Config int mChildrenChangingConfigurations; + + private boolean mCheckedOpacity; + private int mOpacity; + + private boolean mCheckedStateful; + private boolean mIsStateful; + private boolean mAutoMirrored = false; + + LayerState(@Nullable LayerState orig, @NonNull MaskableIconDrawable owner, + @Nullable Resources res) { + mDensity = Drawable.resolveDensity(res, orig != null ? orig.mDensity : 0); + mChildren = new ChildDrawable[N_CHILDREN]; + if (orig != null) { + final ChildDrawable[] origChildDrawable = orig.mChildren; + + mChangingConfigurations = orig.mChangingConfigurations; + mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations; + + for (int i = 0; i < N_CHILDREN; i++) { + final ChildDrawable or = origChildDrawable[i]; + mChildren[i] = new ChildDrawable(or, owner, res); + } + + mCheckedOpacity = orig.mCheckedOpacity; + mOpacity = orig.mOpacity; + mCheckedStateful = orig.mCheckedStateful; + mIsStateful = orig.mIsStateful; + mAutoMirrored = orig.mAutoMirrored; + mThemeAttrs = orig.mThemeAttrs; + mOpacityOverride = orig.mOpacityOverride; + } else { + for (int i = 0; i < N_CHILDREN; i++) { + mChildren[i] = new ChildDrawable(mDensity); + } + } + } + + public final void setDensity(int targetDensity) { + if (mDensity != targetDensity) { + mDensity = targetDensity; + } + } + + @Override + public boolean canApplyTheme() { + if (mThemeAttrs != null || super.canApplyTheme()) { + return true; + } + + final ChildDrawable[] array = mChildren; + for (int i = 0; i < N_CHILDREN; i++) { + final ChildDrawable layer = array[i]; + if (layer.canApplyTheme()) { + return true; + } + } + return false; + } + + @Override + public Drawable newDrawable() { + return new MaskableIconDrawable(this, null); + } + + @Override + public Drawable newDrawable(@Nullable Resources res) { + return new MaskableIconDrawable(this, res); + } + + @Override + public @Config int getChangingConfigurations() { + return mChangingConfigurations + | mChildrenChangingConfigurations; + } + + public final int getOpacity() { + if (mCheckedOpacity) { + return mOpacity; + } + + final ChildDrawable[] array = mChildren; + + // Seek to the first non-null drawable. + int firstIndex = -1; + for (int i = 0; i < N_CHILDREN; i++) { + if (array[i].mDrawable != null) { + firstIndex = i; + break; + } + } + + int op; + if (firstIndex >= 0) { + op = array[firstIndex].mDrawable.getOpacity(); + } else { + op = PixelFormat.TRANSPARENT; + } + + // Merge all remaining non-null drawables. + for (int i = firstIndex + 1; i < N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null) { + op = Drawable.resolveOpacity(op, dr.getOpacity()); + } + } + + mOpacity = op; + mCheckedOpacity = true; + return op; + } + + public final boolean isStateful() { + if (mCheckedStateful) { + return mIsStateful; + } + + final ChildDrawable[] array = mChildren; + boolean isStateful = false; + for (int i = 0; i < N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.isStateful()) { + isStateful = true; + break; + } + } + + mIsStateful = isStateful; + mCheckedStateful = true; + return isStateful; + } + + public final boolean canConstantState() { + final ChildDrawable[] array = mChildren; + for (int i = 0; i < N_CHILDREN; i++) { + final Drawable dr = array[i].mDrawable; + if (dr != null && dr.getConstantState() == null) { + return false; + } + } + + // Don't cache the result, this method is not called very often. + return true; + } + + public void invalidateCache() { + mCheckedOpacity = false; + mCheckedStateful = false; + } + } +} \ No newline at end of file