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
This commit is contained in:
Hyunyoung Song
2017-02-03 15:53:26 -08:00
parent 620f72841f
commit 9ee90a42dc
6 changed files with 142 additions and 25 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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()) {

View File

@@ -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) {