diff --git a/api/current.txt b/api/current.txt index d7721819e404d..ea0ed4611f92b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -13766,6 +13766,7 @@ package android.graphics.drawable { method public static android.graphics.drawable.Icon createWithContentUri(android.net.Uri); method public static android.graphics.drawable.Icon createWithData(byte[], int, int); method public static android.graphics.drawable.Icon createWithFilePath(java.lang.String); + method public static android.graphics.drawable.Icon createWithMaskableBitmap(android.graphics.Bitmap); method public static android.graphics.drawable.Icon createWithResource(android.content.Context, int); method public static android.graphics.drawable.Icon createWithResource(java.lang.String, int); method public int describeContents(); @@ -13849,9 +13850,9 @@ package android.graphics.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 static float getExtraInsetPercentage(); method public android.graphics.drawable.Drawable getForeground(); method public android.graphics.Path getIconMask(); method public int getOpacity(); @@ -13861,8 +13862,6 @@ package android.graphics.drawable { 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 { diff --git a/api/system-current.txt b/api/system-current.txt index 6affa7fea40f5..a0c296ab74548 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -14334,6 +14334,7 @@ package android.graphics.drawable { method public static android.graphics.drawable.Icon createWithContentUri(android.net.Uri); method public static android.graphics.drawable.Icon createWithData(byte[], int, int); method public static android.graphics.drawable.Icon createWithFilePath(java.lang.String); + method public static android.graphics.drawable.Icon createWithMaskableBitmap(android.graphics.Bitmap); method public static android.graphics.drawable.Icon createWithResource(android.content.Context, int); method public static android.graphics.drawable.Icon createWithResource(java.lang.String, int); method public int describeContents(); @@ -14417,9 +14418,9 @@ package android.graphics.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 static float getExtraInsetPercentage(); method public android.graphics.drawable.Drawable getForeground(); method public android.graphics.Path getIconMask(); method public int getOpacity(); @@ -14429,8 +14430,6 @@ package android.graphics.drawable { 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 { diff --git a/api/test-current.txt b/api/test-current.txt index bbb7f898d201a..6d009f15297f6 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -13800,6 +13800,7 @@ package android.graphics.drawable { method public static android.graphics.drawable.Icon createWithContentUri(android.net.Uri); method public static android.graphics.drawable.Icon createWithData(byte[], int, int); method public static android.graphics.drawable.Icon createWithFilePath(java.lang.String); + method public static android.graphics.drawable.Icon createWithMaskableBitmap(android.graphics.Bitmap); method public static android.graphics.drawable.Icon createWithResource(android.content.Context, int); method public static android.graphics.drawable.Icon createWithResource(java.lang.String, int); method public int describeContents(); @@ -13883,20 +13884,19 @@ package android.graphics.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 static float getExtraInsetPercentage(); method public android.graphics.drawable.Drawable getForeground(); method public android.graphics.Path getIconMask(); method public int getOpacity(); + method public android.graphics.Region getSafeZone(); 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 { diff --git a/graphics/java/android/graphics/drawable/Icon.java b/graphics/java/android/graphics/drawable/Icon.java index 977200906d626..60c3b1c37b55b 100644 --- a/graphics/java/android/graphics/drawable/Icon.java +++ b/graphics/java/android/graphics/drawable/Icon.java @@ -67,6 +67,8 @@ public final class Icon implements Parcelable { public static final int TYPE_DATA = 3; /** @hide */ public static final int TYPE_URI = 4; + /** @hide */ + public static final int TYPE_BITMAP_MASKABLE = 5; private static final int VERSION_STREAM_SERIALIZER = 1; @@ -101,6 +103,7 @@ public final class Icon implements Parcelable { * {@link #TYPE_RESOURCE}, * {@link #TYPE_DATA}, or * {@link #TYPE_URI}. + * {@link #TYPE_BITMAP_MASKABLE} * @hide */ public int getType() { @@ -112,7 +115,7 @@ public final class Icon implements Parcelable { * @hide */ public Bitmap getBitmap() { - if (mType != TYPE_BITMAP) { + if (mType != TYPE_BITMAP && mType != TYPE_BITMAP_MASKABLE) { throw new IllegalStateException("called getBitmap() on " + this); } return (Bitmap) mObj1; @@ -218,6 +221,7 @@ public final class Icon implements Parcelable { private static final String typeToString(int x) { switch (x) { case TYPE_BITMAP: return "BITMAP"; + case TYPE_BITMAP_MASKABLE: return "BITMAP_MASKABLE"; case TYPE_DATA: return "DATA"; case TYPE_RESOURCE: return "RESOURCE"; case TYPE_URI: return "URI"; @@ -285,6 +289,9 @@ public final class Icon implements Parcelable { switch (mType) { case TYPE_BITMAP: return new BitmapDrawable(context.getResources(), getBitmap()); + case TYPE_BITMAP_MASKABLE: + return new MaskableIconDrawable(null, + new BitmapDrawable(context.getResources(), getBitmap())); case TYPE_RESOURCE: if (getResources() == null) { // figure out where to load resources from @@ -388,7 +395,7 @@ public final class Icon implements Parcelable { * @hide */ public void convertToAshmem() { - if (mType == TYPE_BITMAP && + if ((mType == TYPE_BITMAP || mType == TYPE_BITMAP_MASKABLE) && getBitmap().isMutable() && getBitmap().getAllocationByteCount() >= MIN_ASHMEM_ICON_SIZE) { setBitmap(getBitmap().createAshmemBitmap()); @@ -409,6 +416,7 @@ public final class Icon implements Parcelable { switch (mType) { case TYPE_BITMAP: + case TYPE_BITMAP_MASKABLE: getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream); break; case TYPE_DATA: @@ -444,6 +452,8 @@ public final class Icon implements Parcelable { switch (type) { case TYPE_BITMAP: return createWithBitmap(BitmapFactory.decodeStream(inputStream)); + case TYPE_BITMAP_MASKABLE: + return createWithMaskableBitmap(BitmapFactory.decodeStream(inputStream)); case TYPE_DATA: final int length = inputStream.readInt(); final byte[] data = new byte[length]; @@ -478,6 +488,7 @@ public final class Icon implements Parcelable { } switch (mType) { case TYPE_BITMAP: + case TYPE_BITMAP_MASKABLE: return getBitmap() == otherIcon.getBitmap(); case TYPE_DATA: return getDataLength() == otherIcon.getDataLength() @@ -550,6 +561,20 @@ public final class Icon implements Parcelable { return rep; } + /** + * Create an Icon pointing to a bitmap in memory that follows the icon design guideline defined + * by {@link MaskableIconDrawable}. + * @param bits A valid {@link android.graphics.Bitmap} object + */ + public static Icon createWithMaskableBitmap(Bitmap bits) { + if (bits == null) { + throw new IllegalArgumentException("Bitmap must not be null."); + } + final Icon rep = new Icon(TYPE_BITMAP_MASKABLE); + rep.setBitmap(bits); + return rep; + } + /** * Create an Icon pointing to a compressed bitmap stored in a byte array. * @param data Byte array storing compressed bitmap data of a type that @@ -654,6 +679,7 @@ public final class Icon implements Parcelable { final StringBuilder sb = new StringBuilder("Icon(typ=").append(typeToString(mType)); switch (mType) { case TYPE_BITMAP: + case TYPE_BITMAP_MASKABLE: sb.append(" size=") .append(getBitmap().getWidth()) .append("x") @@ -692,7 +718,7 @@ public final class Icon implements Parcelable { * Parcelable interface */ public int describeContents() { - return (mType == TYPE_BITMAP || mType == TYPE_DATA) + return (mType == TYPE_BITMAP || mType == TYPE_BITMAP_MASKABLE || mType == TYPE_DATA) ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; } @@ -702,6 +728,7 @@ public final class Icon implements Parcelable { this(in.readInt()); switch (mType) { case TYPE_BITMAP: + case TYPE_BITMAP_MASKABLE: final Bitmap bits = Bitmap.CREATOR.createFromParcel(in); mObj1 = bits; break; @@ -740,6 +767,7 @@ public final class Icon implements Parcelable { dest.writeInt(mType); switch (mType) { case TYPE_BITMAP: + case TYPE_BITMAP_MASKABLE: final Bitmap bits = getBitmap(); getBitmap().writeToParcel(dest, flags); break; diff --git a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java index 043f0926bdb7a..e4f1788a17eb7 100644 --- a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java +++ b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java @@ -20,6 +20,7 @@ import static android.graphics.drawable.Drawable.obtainAttributes; 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; @@ -62,17 +63,22 @@ public class MaskableIconDrawable extends Drawable implements Drawable.Callback /** * Mask path is defined inside device configuration in following dimension: [100 x 100] + * @hide */ public static final float MASK_SIZE = 100f; + private static final float SAFEZONE_SCALE = .9f; /** - * 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 + * 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. */ - public static final float DEFAULT_VIEW_PORT_SCALE = 2f / 3f; + 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 {@link com.android.internal.R.string.config_icon_mask}. @@ -155,12 +161,17 @@ public class MaskableIconDrawable extends Drawable implements Drawable.Callback * * @param backgroundDrawable drawable that should be rendered in the background * @param foregroundDrawable drawable that should be rendered in the foreground + * @hide */ public MaskableIconDrawable(Drawable backgroundDrawable, Drawable foregroundDrawable) { this((LayerState)null, null); - addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable)); - addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable)); + if (backgroundDrawable != null) { + addLayer(BACKGROUND_ID, createChildDrawable(backgroundDrawable)); + } + if (foregroundDrawable != null) { + addLayer(FOREGROUND_ID, createChildDrawable(foregroundDrawable)); + } } /** @@ -198,6 +209,15 @@ public class MaskableIconDrawable extends Drawable implements Drawable.Callback 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. + */ + public static float getExtraInsetPercentage() { + return EXTRA_INSET_PERCENTAGE; + } + /** * @return the mask path object used to clip the drawable */ @@ -242,13 +262,20 @@ public class MaskableIconDrawable extends Drawable implements Drawable.Callback int cY = bounds.centerY(); 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); - final ChildDrawable r = mLayerState.mChildren[i]; - final Drawable d = r.mDrawable; d.setBounds(outRect); } } @@ -273,6 +300,9 @@ public class MaskableIconDrawable extends Drawable implements Drawable.Callback if (mLayersShader == null) { mCanvas.setBitmap(mLayersBitmap); 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); @@ -295,6 +325,18 @@ public class MaskableIconDrawable extends Drawable implements Drawable.Callback 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()) { diff --git a/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java b/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java index a214b9e4ecbef..50c498bf95d36 100644 --- a/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java +++ b/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java @@ -18,6 +18,7 @@ package android.graphics.drawable; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Region; import android.os.Handler; import android.os.HandlerThread; import android.os.Parcel; @@ -107,6 +108,40 @@ public class IconTest extends AndroidTestCase { } } + @SmallTest + public void testWithMaskableBitmap() throws Exception { + final Bitmap bm1 = Bitmap.createBitmap(150, 150, Bitmap.Config.ARGB_8888); + + final Canvas can1 = new Canvas(bm1); + can1.drawColor(0xFFFF0000); + + final Icon im1 = Icon.createWithMaskableBitmap(bm1); + + final MaskableIconDrawable draw1 = (MaskableIconDrawable) im1.loadDrawable(mContext); + + final Bitmap test1 = Bitmap.createBitmap( + (int)(draw1.getIntrinsicWidth() * (1 + 2 * MaskableIconDrawable.getExtraInsetPercentage())), + (int)(draw1.getIntrinsicHeight() * (1 + 2 * MaskableIconDrawable.getExtraInsetPercentage())), + Bitmap.Config.ARGB_8888); + + draw1.setBounds(0, 0, + (int) (draw1.getIntrinsicWidth() * (1 + 2 * MaskableIconDrawable.getExtraInsetPercentage())), + (int) (draw1.getIntrinsicHeight() * (1 + 2 * MaskableIconDrawable.getExtraInsetPercentage()))); + draw1.draw(new Canvas(test1)); + + final File dir = getContext().getExternalFilesDir(null); + L("writing temp bitmaps to %s...", dir); + + bm1.compress(Bitmap.CompressFormat.PNG, 100, + new FileOutputStream(new File(dir, "maskable-bitmap1-original.png"))); + test1.compress(Bitmap.CompressFormat.PNG, 100, + new FileOutputStream(new File(dir, "maskable-bitmap1-test.png"))); + if (!equalBitmaps(bm1, test1, draw1.getSafeZone())) { + findBitmapDifferences(bm1, test1); + fail("maskable bitmap1 differs, check " + dir); + } + } + @SmallTest public void testWithBitmapResource() throws Exception { final Bitmap res1 = ((BitmapDrawable) getContext().getDrawable(R.drawable.landscape)) @@ -294,17 +329,31 @@ public class IconTest extends AndroidTestCase { printBits(aPix, w, h); } boolean equalBitmaps(Bitmap a, Bitmap b) { + return equalBitmaps(a, b, null); + } + + boolean equalBitmaps(Bitmap a, Bitmap b, Region region) { if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight()) return false; - + final int w = a.getWidth(); final int h = a.getHeight(); int[] aPix = new int[w * h]; int[] bPix = new int[w * h]; - a.getPixels(aPix, 0, w, 0, 0, w, h); - b.getPixels(bPix, 0, w, 0, 0, w, h); - - return Arrays.equals(aPix, bPix); + if (region != null) { + for (int i = 0; i < w; i++) { + for (int j = 0; j < h; j++) { + if (region.contains(i, j) && a.getPixel(i, j) != b.getPixel(i, j)) { + return false; + } + } + } + return true; + } else { + a.getPixels(aPix, 0, w, 0, 0, w, h); + b.getPixels(bPix, 0, w, 0, 0, w, h); + return Arrays.equals(aPix, bPix); + } } void findBitmapDifferences(Bitmap a, Bitmap b) {