diff --git a/src/api-impl/android/graphics/drawable/AdaptiveIconDrawable.java b/src/api-impl/android/graphics/drawable/AdaptiveIconDrawable.java new file mode 100644 index 00000000..89fef5d5 --- /dev/null +++ b/src/api-impl/android/graphics/drawable/AdaptiveIconDrawable.java @@ -0,0 +1,1152 @@ +/* + * 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 java.io.IOException; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + *
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 its 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 ? 0 : 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() + ":