Icon class should support Maskable bitmap type
Test: Unit test on IconTest
$ runtest --path=frameworks/base/graphics/tests/graphicstests/src/android/graphics/drawable/IconTest.java
b/34196580
Change-Id: I321c4b02f17ad9426c053216c4c88616a605aacf
diff --git a/api/current.txt b/api/current.txt
index d772181..ea0ed46 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -13766,6 +13766,7 @@
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 @@
}
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 @@
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 6affa7f..a0c296a 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -14334,6 +14334,7 @@
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 @@
}
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 @@
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 bbb7f89..6d009f1 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -13800,6 +13800,7 @@
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 @@
}
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 9772009..60c3b1c 100644
--- a/graphics/java/android/graphics/drawable/Icon.java
+++ b/graphics/java/android/graphics/drawable/Icon.java
@@ -67,6 +67,8 @@
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 @@
* {@link #TYPE_RESOURCE},
* {@link #TYPE_DATA}, or
* {@link #TYPE_URI}.
+ * {@link #TYPE_BITMAP_MASKABLE}
* @hide
*/
public int getType() {
@@ -112,7 +115,7 @@
* @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 @@
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 @@
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 @@
* @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 @@
switch (mType) {
case TYPE_BITMAP:
+ case TYPE_BITMAP_MASKABLE:
getBitmap().compress(Bitmap.CompressFormat.PNG, 100, dataStream);
break;
case TYPE_DATA:
@@ -444,6 +452,8 @@
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 @@
}
switch (mType) {
case TYPE_BITMAP:
+ case TYPE_BITMAP_MASKABLE:
return getBitmap() == otherIcon.getBitmap();
case TYPE_DATA:
return getDataLength() == otherIcon.getDataLength()
@@ -551,6 +562,20 @@
}
/**
+ * 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
* {@link android.graphics.BitmapFactory}
@@ -654,6 +679,7 @@
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 @@
* 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 @@
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 @@
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 043f092..e4f1788a 100644
--- a/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
+++ b/graphics/java/android/graphics/drawable/MaskableIconDrawable.java
@@ -20,6 +20,7 @@
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 @@
/**
* 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 @@
*
* @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));
+ }
}
/**
@@ -199,6 +210,15 @@
}
/**
+ * 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
*/
public Path getIconMask() {
@@ -242,13 +262,20 @@
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 @@
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 @@
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 a214b9e..50c498b 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 @@
import android.graphics.Bitmap;
import android.graphics.Canvas;
+import android.graphics.Region;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Parcel;
@@ -108,6 +109,40 @@
}
@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))
.getBitmap();
@@ -294,17 +329,31 @@
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) {