/* * 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()) **/ 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; /** * 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((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; state.mSourceDrawableId = Resources.getAttributeSetSourceResId(attrs); 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) { // 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); 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 (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() { 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(); } } } /** * 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(); 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() + ":