| /* |
| * 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.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.TestApi; |
| 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; |
| |
| /** |
| * <p>This class can also be created via XML inflation using <code><adaptive-icon></code> tag |
| * in addition to dynamic creation. |
| * |
| * <p>This drawable supports two drawable layers: foreground and background. The layers are clipped |
| * when rendering using the mask defined in the device configuration. |
| * |
| * <ul> |
| * <li>Both foreground and background layers should be sized at 108 x 108 dp.</li> |
| * <li>The inner 72 x 72 dp of the icon appears within the masked viewport.</li> |
| * <li>The outer 18 dp on each of the 4 sides of the layers is reserved for use by the system UI |
| * surfaces to create interesting visual effects, such as parallax or pulsing.</li> |
| * </ul> |
| * |
| * Such motion effect is achieved by internally setting the bounds of the foreground and |
| * background layer as following: |
| * <pre> |
| * Rect(getBounds().left - getBounds().getWidth() * #getExtraInsetFraction(), |
| * getBounds().top - getBounds().getHeight() * #getExtraInsetFraction(), |
| * getBounds().right + getBounds().getWidth() * #getExtraInsetFraction(), |
| * getBounds().bottom + getBounds().getHeight() * #getExtraInsetFraction()) |
| * </pre> |
| */ |
| 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 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. |
| */ |
| 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); |
| |
| if (sMask == null) { |
| sMask = PathParser.createPathFromPathData( |
| Resources.getSystem().getString(R.string.config_icon_mask)); |
| } |
| mMask = PathParser.createPathFromPathData( |
| Resources.getSystem().getString(R.string.config_icon_mask)); |
| 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((LayerState)null, null); |
| if (backgroundDrawable != null) { |
| addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable)); |
| } |
| if (foregroundDrawable != null) { |
| 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 deviceDensity = Drawable.resolveDensity(r, 0); |
| state.setDensity(deviceDensity); |
| state.mSrcDensityOverride = mSrcDensityOverride; |
| |
| final ChildDrawable[] array = state.mChildren; |
| for (int i = 0; i < state.mChildren.length; i++) { |
| final ChildDrawable layer = array[i]; |
| layer.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; |
| } |
| |
| @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]; |
| if (r == null) { |
| continue; |
| } |
| 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) { |
| mMaskMatrix.setScale(b.width() / MASK_SIZE, b.height() / MASK_SIZE); |
| sMask.transform(mMaskMatrix, mMask); |
| |
| if (mMaskBitmap == null || mMaskBitmap.getWidth() != b.width() || |
| mMaskBitmap.getHeight() != b.height()) { |
| mMaskBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ALPHA_8); |
| mLayersBitmap = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888); |
| } |
| // mMaskBitmap bound [0, w] x [0, h] |
| mCanvas.setBitmap(mMaskBitmap); |
| mPaint.setShader(null); |
| mCanvas.drawPath(mMask, mPaint); |
| |
| // mMask bound [left, top, right, bottom] |
| mMaskMatrix.postTranslate(b.left, b.top); |
| mMask.reset(); |
| sMask.transform(mMaskMatrix, mMask); |
| // reset everything that depends on the view bounds |
| mTransparentRegion.setEmpty(); |
| mLayersShader = null; |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| if (mLayersBitmap == null) { |
| return; |
| } |
| if (mLayersShader == null) { |
| mCanvas.setBitmap(mLayersBitmap); |
| mCanvas.drawColor(Color.BLACK); |
| for (int i = 0; i < mLayerState.N_CHILDREN; i++) { |
| if (mLayerState.mChildren[i] == null) { |
| continue; |
| } |
| final Drawable dr = mLayerState.mChildren[i].mDrawable; |
| if (dr != null) { |
| dr.draw(mCanvas); |
| } |
| } |
| mLayersShader = new BitmapShader(mLayersBitmap, TileMode.CLAMP, TileMode.CLAMP); |
| mPaint.setShader(mLayersShader); |
| } |
| if (mMaskBitmap != null) { |
| Rect bounds = getBounds(); |
| canvas.drawBitmap(mMaskBitmap, bounds.left, bounds.top, mPaint); |
| } |
| } |
| |
| @Override |
| public void invalidateSelf() { |
| mLayersShader = null; |
| super.invalidateSelf(); |
| } |
| |
| @Override |
| public void getOutline(@NonNull Outline outline) { |
| outline.setConvexPath(mMask); |
| } |
| |
| /** @hide */ |
| @TestApi |
| public Region getSafeZone() { |
| mMaskMatrix.reset(); |
| mMaskMatrix.setScale(SAFEZONE_SCALE, SAFEZONE_SCALE, getBounds().centerX(), getBounds().centerY()); |
| Path p = new Path(); |
| mMask.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(); |
| } |
| } |
| } |
| |
| /** |
| * 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(); |
| 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.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() |
| + ": <foreground> or <background> tag requires a 'drawable'" |
| + "attribute or child tag defining a drawable"); |
| } |
| |
| // We found a child drawable. Take ownership. |
| layer.mDrawable = Drawable.createFromXmlInnerForDensity(r, parser, attrs, |
| mLayerState.mSrcDensityOverride, 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.getDrawableForDensity(R.styleable.AdaptiveIconDrawableLayer_drawable, |
| state.mSrcDensityOverride); |
| 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) { |
| mPaint.setAlpha(alpha); |
| } |
| |
| @Override |
| public int getAlpha() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| @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); |
| } |
| } |
| } |
| |
| 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(); |
| } |
| |
| /** @hide */ |
| @Override |
| public boolean hasFocusStateSpecified() { |
| return mLayerState.hasFocusStateSpecified(); |
| } |
| |
| @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]; |
| if (r.mDrawable == null) { |
| continue; |
| } |
| 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]; |
| if (r.mDrawable == null) { |
| continue; |
| } |
| 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 AdaptiveIconDrawable 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; |
| |
| // The density at which to render the drawable and its children. |
| int mDensity; |
| |
| // The density to use when inflating/looking up the children drawables. A value of 0 means |
| // use the system's density. |
| int mSrcDensityOverride = 0; |
| |
| 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 AdaptiveIconDrawable 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; |
| mSrcDensityOverride = orig.mSrcDensityOverride; |
| } 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 AdaptiveIconDrawable(this, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(@Nullable Resources res) { |
| return new AdaptiveIconDrawable(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 hasFocusStateSpecified() { |
| final ChildDrawable[] array = mChildren; |
| for (int i = 0; i < N_CHILDREN; i++) { |
| final Drawable dr = array[i].mDrawable; |
| if (dr != null && dr.hasFocusStateSpecified()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| 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; |
| } |
| } |
| } |