summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java315
-rw-r--r--library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java59
2 files changed, 263 insertions, 111 deletions
diff --git a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
index bf23732..6d082a1 100644
--- a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
+++ b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
@@ -23,15 +23,28 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
+import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import com.android.setupwizardlib.annotations.VisibleForTesting;
+import java.lang.ref.SoftReference;
+
+/**
+ * This class draws the GLIF pattern used as the status bar background for phones and background for
+ * tablets in GLIF layout.
+ */
public class GlifPatternDrawable extends Drawable {
+ /*
+ * This class essentially implements a simple SVG in Java code, with some special handling of
+ * scaling when given different bounds.
+ */
/* static section */
@@ -40,8 +53,28 @@ public class GlifPatternDrawable extends Drawable {
private static final float VIEWBOX_HEIGHT = 768f;
private static final float VIEWBOX_WIDTH = 1366f;
- private static final float SCALE_FOCUS_X = 200f;
- private static final float SCALE_FOCUS_Y = 175f;
+ // X coordinate of scale focus, as a fraction of of the width. (Range is 0 - 1)
+ private static final float SCALE_FOCUS_X = .146f;
+ // Y coordinate of scale focus, as a fraction of of the height. (Range is 0 - 1)
+ private static final float SCALE_FOCUS_Y = .228f;
+
+ // Alpha component of the color to be drawn, on top of the grayscale pattern. (Range is 0 - 1)
+ private static final float COLOR_ALPHA = .8f;
+ // Int version of COLOR_ALPHA. (Range is 0 - 255)
+ private static final int COLOR_ALPHA_INT = (int) (COLOR_ALPHA * 255);
+
+ // Cap the bitmap size, such that it won't hurt the performance too much
+ // and it won't crash due to a very large scale.
+ // The drawable will look blurry above this size.
+ // This is a multiplier applied on top of the viewbox size.
+ // Resulting max cache size = (1.5 x 1366, 1.5 x 768) = (2049, 1152)
+ private static final float MAX_CACHED_BITMAP_SCALE = 1.5f;
+
+ private static final int NUM_PATHS = 7;
+
+ private static SoftReference<Bitmap> sBitmapCache;
+ private static Path[] sPatternPaths;
+ private static int[] sPatternLightness;
public static GlifPatternDrawable getDefault(Context context) {
int colorPrimary = 0;
@@ -53,133 +86,194 @@ public class GlifPatternDrawable extends Drawable {
return new GlifPatternDrawable(colorPrimary);
}
+ @VisibleForTesting
+ public static void invalidatePattern() {
+ sBitmapCache = null;
+ }
+
/* non-static section */
private int mColor;
- private Paint mPaint;
- private Path mTempPath = new Path();
- private Bitmap mBitmap;
+ private Paint mTempPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private ColorFilter mColorFilter;
public GlifPatternDrawable(int color) {
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
setColor(color);
}
@Override
public void draw(Canvas canvas) {
- if (mBitmap == null) {
- mBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(),
- Bitmap.Config.ARGB_8888);
- Canvas bitmapCanvas = new Canvas(mBitmap);
- renderOnCanvas(bitmapCanvas);
- mPaint.reset();
- mPaint.setAlpha(51);
+ final Rect bounds = getBounds();
+ int drawableWidth = bounds.width();
+ int drawableHeight = bounds.height();
+ Bitmap bitmap = null;
+ if (sBitmapCache != null) {
+ bitmap = sBitmapCache.get();
+ }
+ if (bitmap != null) {
+ final int bitmapWidth = bitmap.getWidth();
+ final int bitmapHeight = bitmap.getHeight();
+ // Invalidate the cache if this drawable is bigger and we can still create a bigger
+ // cache.
+ if (drawableWidth > bitmapWidth
+ && bitmapWidth < VIEWBOX_WIDTH * MAX_CACHED_BITMAP_SCALE) {
+ bitmap = null;
+ } else if (drawableHeight > bitmapHeight
+ && bitmapHeight < VIEWBOX_HEIGHT * MAX_CACHED_BITMAP_SCALE) {
+ bitmap = null;
+ }
+ }
+
+ if (bitmap == null) {
+ // Reset the paint so it can be used to draw the paths in renderOnCanvas
+ mTempPaint.reset();
+
+ bitmap = createBitmapCache(drawableWidth, drawableHeight);
+ sBitmapCache = new SoftReference<>(bitmap);
+
+ // Reset the paint to so it can be used to draw the bitmap
+ mTempPaint.reset();
}
+
canvas.save();
- canvas.clipRect(getBounds());
- canvas.drawColor(mColor);
- canvas.drawBitmap(mBitmap, 0, 0, mPaint);
+ canvas.clipRect(bounds);
+
+ scaleCanvasToBounds(canvas, bitmap, bounds);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
+ && canvas.isHardwareAccelerated()) {
+ mTempPaint.setColorFilter(mColorFilter);
+ canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
+ } else {
+ // Software renderer doesn't work properly with ColorMatrix filter on ALPHA_8 bitmaps.
+ canvas.drawColor(Color.BLACK);
+ mTempPaint.setColor(Color.WHITE);
+ canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
+ canvas.drawColor(mColor);
+ }
+
canvas.restore();
}
- private void renderOnCanvas(Canvas canvas) {
+ @VisibleForTesting
+ public Bitmap createBitmapCache(int drawableWidth, int drawableHeight) {
+ float scaleX = drawableWidth / VIEWBOX_WIDTH;
+ float scaleY = drawableHeight / VIEWBOX_HEIGHT;
+ float scale = Math.max(scaleX, scaleY);
+ scale = Math.min(MAX_CACHED_BITMAP_SCALE, scale);
+
+
+ int scaledWidth = (int) (VIEWBOX_WIDTH * scale);
+ int scaledHeight = (int) (VIEWBOX_HEIGHT * scale);
+
+ // Use ALPHA_8 mask to save memory, since the pattern is grayscale only anyway.
+ Bitmap bitmap = Bitmap.createBitmap(
+ scaledWidth,
+ scaledHeight,
+ Bitmap.Config.ALPHA_8);
+ Canvas bitmapCanvas = new Canvas(bitmap);
+ renderOnCanvas(bitmapCanvas, scale);
+ return bitmap;
+ }
+
+ private void renderOnCanvas(Canvas canvas, float scale) {
canvas.save();
- canvas.clipRect(getBounds());
+ canvas.scale(scale, scale);
- scaleCanvasToBounds(canvas);
+ mTempPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
// Draw the pattern by creating the paths, adjusting the colors and drawing them. The path
// values are extracted from the SVG of the pattern file.
- Path p = mTempPath;
- p.reset();
- p.moveTo(1029.4f, 357.5f);
- p.lineTo(1366f, 759.1f);
- p.lineTo(1366f, 0f);
- p.lineTo(1137.7f, 0f);
- p.close();
- drawPath(canvas, p, 10);
-
- p.reset();
- p.moveTo(1138.1f, 0f);
- p.rLineTo(-144.8f, 768f);
- p.rLineTo(372.7f, 0f);
- p.rLineTo(0f, -524f);
- p.cubicTo(1290.7f, 121.6f, 1219.2f, 41.1f, 1178.7f, 0f);
- p.close();
- drawPath(canvas, p, 40);
-
- p.reset();
- p.moveTo(949.8f, 768f);
- p.rCubicTo(92.6f, -170.6f, 213f, -440.3f, 269.4f, -768f);
- p.lineTo(585f, 0f);
- p.rLineTo(2.1f, 766f);
- p.close();
- drawPath(canvas, p, 51);
-
- p.reset();
- p.moveTo(471.1f, 768f);
- p.rMoveTo(704.5f, 0f);
- p.cubicTo(1123.6f, 563.3f, 1027.4f, 275.2f, 856.2f, 0f);
- p.lineTo(476.4f, 0f);
- p.rLineTo(-5.3f, 768f);
- p.close();
- drawPath(canvas, p, 66);
-
- p.reset();
- p.moveTo(323.1f, 768f);
- p.moveTo(777.5f, 768f);
- p.cubicTo(661.9f, 348.8f, 427.2f, 21.4f, 401.2f, 25.4f);
- p.lineTo(323.1f, 768f);
- p.close();
- drawPath(canvas, p, 91);
-
- p.reset();
- p.moveTo(178.44286f, 766.85714f);
- p.lineTo(308.7f, 768f);
- p.cubicTo(381.7f, 604.6f, 481.6f, 344.3f, 562.2f, 0f);
- p.lineTo(0f, 0f);
- p.close();
- drawPath(canvas, p, 112);
-
- p.reset();
- p.moveTo(146f, 0f);
- p.lineTo(0f, 0f);
- p.lineTo(0f, 768f);
- p.lineTo(394.2f, 768f);
- p.cubicTo(327.7f, 475.3f, 228.5f, 201f, 146f, 0f);
- p.close();
- drawPath(canvas, p, 130);
+ if (sPatternPaths == null) {
+ sPatternPaths = new Path[NUM_PATHS];
+ // Lightness values of the pattern, range 0 - 255
+ sPatternLightness = new int[] { 10, 40, 51, 66, 91, 112, 130 };
+
+ Path p = sPatternPaths[0] = new Path();
+ p.moveTo(1029.4f, 357.5f);
+ p.lineTo(1366f, 759.1f);
+ p.lineTo(1366f, 0f);
+ p.lineTo(1137.7f, 0f);
+ p.close();
+
+ p = sPatternPaths[1] = new Path();
+ p.moveTo(1138.1f, 0f);
+ p.rLineTo(-144.8f, 768f);
+ p.rLineTo(372.7f, 0f);
+ p.rLineTo(0f, -524f);
+ p.cubicTo(1290.7f, 121.6f, 1219.2f, 41.1f, 1178.7f, 0f);
+ p.close();
+
+ p = sPatternPaths[2] = new Path();
+ p.moveTo(949.8f, 768f);
+ p.rCubicTo(92.6f, -170.6f, 213f, -440.3f, 269.4f, -768f);
+ p.lineTo(585f, 0f);
+ p.rLineTo(2.1f, 766f);
+ p.close();
+
+ p = sPatternPaths[3] = new Path();
+ p.moveTo(471.1f, 768f);
+ p.rMoveTo(704.5f, 0f);
+ p.cubicTo(1123.6f, 563.3f, 1027.4f, 275.2f, 856.2f, 0f);
+ p.lineTo(476.4f, 0f);
+ p.rLineTo(-5.3f, 768f);
+ p.close();
+
+ p = sPatternPaths[4] = new Path();
+ p.moveTo(323.1f, 768f);
+ p.moveTo(777.5f, 768f);
+ p.cubicTo(661.9f, 348.8f, 427.2f, 21.4f, 401.2f, 25.4f);
+ p.lineTo(323.1f, 768f);
+ p.close();
+
+ p = sPatternPaths[5] = new Path();
+ p.moveTo(178.44286f, 766.85714f);
+ p.lineTo(308.7f, 768f);
+ p.cubicTo(381.7f, 604.6f, 481.6f, 344.3f, 562.2f, 0f);
+ p.lineTo(0f, 0f);
+ p.close();
+
+ p = sPatternPaths[6] = new Path();
+ p.moveTo(146f, 0f);
+ p.lineTo(0f, 0f);
+ p.lineTo(0f, 768f);
+ p.lineTo(394.2f, 768f);
+ p.cubicTo(327.7f, 475.3f, 228.5f, 201f, 146f, 0f);
+ p.close();
+ }
+
+ for (int i = 0; i < NUM_PATHS; i++) {
+ // Color is 0xAARRGGBB, so alpha << 24 will create a color with (alpha)% black.
+ // Although the color components don't really matter, since the backing bitmap cache is
+ // ALPHA_8.
+ mTempPaint.setColor(sPatternLightness[i] << 24);
+ canvas.drawPath(sPatternPaths[i], mTempPaint);
+ }
canvas.restore();
+ mTempPaint.reset();
}
@VisibleForTesting
- public void scaleCanvasToBounds(Canvas canvas) {
- final Rect bounds = getBounds();
- final int height = bounds.height();
- final int width = bounds.width();
+ public void scaleCanvasToBounds(Canvas canvas, Bitmap bitmap, Rect drawableBounds) {
+ int bitmapWidth = bitmap.getWidth();
+ int bitmapHeight = bitmap.getHeight();
+ float scaleX = drawableBounds.width() / (float) bitmapWidth;
+ float scaleY = drawableBounds.height() / (float) bitmapHeight;
- float scaleY = height / VIEWBOX_HEIGHT;
- float scaleX = width / VIEWBOX_WIDTH;
// First scale both sides to fit independently.
canvas.scale(scaleX, scaleY);
if (scaleY > scaleX) {
- // Adjust x-scale to maintain aspect ratio, but using (200, 0) as the pivot instead,
- // so that more of the texture and less of the blank space on the left edge is seen.
- canvas.scale(scaleY / scaleX, 1f, SCALE_FOCUS_X, 0f);
- } else {
- // Adjust y-scale to maintain aspect ratio, but using (0, 175) as the pivot instead,
- // so that an intersection of two "circles" can always be seen.
- canvas.scale(1f, scaleX / scaleY, 0f, SCALE_FOCUS_Y);
+ // Adjust x-scale to maintain aspect ratio using the pivot, so that more of the texture
+ // and less of the blank space on the left edge is seen.
+ canvas.scale(scaleY / scaleX, 1f, SCALE_FOCUS_X * bitmapWidth, 0f);
+ } else if (scaleX > scaleY) {
+ // Adjust y-scale to maintain aspect ratio using the pivot, so that an intersection of
+ // two "circles" can always be seen.
+ canvas.scale(1f, scaleX / scaleY, 0f, SCALE_FOCUS_Y * bitmapHeight);
}
}
- private void drawPath(Canvas canvas, Path path, int lightness) {
- mPaint.setColor(Color.rgb(lightness, lightness, lightness));
- canvas.drawPath(path, mPaint);
- }
-
@Override
public void setAlpha(int i) {
// Ignore
@@ -195,20 +289,29 @@ public class GlifPatternDrawable extends Drawable {
return 0;
}
- @Override
- protected void onBoundsChange(Rect bounds) {
- mBitmap = null;
- super.onBoundsChange(bounds);
- }
-
+ /**
+ * Sets the color used as the base color of this pattern drawable. The alpha component of the
+ * color will be ignored.
+ */
public void setColor(int color) {
- mColor = color;
- mPaint.setColor(color);
- mBitmap = null;
+ final int r = Color.red(color);
+ final int g = Color.green(color);
+ final int b = Color.blue(color);
+ mColor = Color.argb(COLOR_ALPHA_INT, r, g, b);
+ mColorFilter = new ColorMatrixColorFilter(new float[] {
+ 0, 0, 0, 1 - COLOR_ALPHA, r * COLOR_ALPHA,
+ 0, 0, 0, 1 - COLOR_ALPHA, g * COLOR_ALPHA,
+ 0, 0, 0, 1 - COLOR_ALPHA, b * COLOR_ALPHA,
+ 0, 0, 0, 0, 255
+ });
invalidateSelf();
}
+ /**
+ * @return The color used as the base color of this pattern drawable. The alpha component of
+ * this is always 255.
+ */
public int getColor() {
- return mColor;
+ return Color.argb(255, Color.red(mColor), Color.green(mColor), Color.blue(mColor));
}
}
diff --git a/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java b/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
index cc86f18..19b4144 100644
--- a/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
+++ b/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
@@ -21,8 +21,10 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
+import android.os.Debug;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
import com.android.setupwizardlib.GlifPatternDrawable;
@@ -30,6 +32,14 @@ import junit.framework.AssertionFailedError;
public class GlifPatternDrawableTest extends AndroidTestCase {
+ private static final String TAG = "GlifPatternDrawableTest";
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ GlifPatternDrawable.invalidatePattern();
+ }
+
@SmallTest
public void testDraw() {
final Bitmap bitmap = Bitmap.createBitmap(1366, 768, Bitmap.Config.ARGB_8888);
@@ -74,9 +84,11 @@ public class GlifPatternDrawableTest extends AndroidTestCase {
final Canvas canvas = new Canvas();
Matrix expected = new Matrix(canvas.getMatrix());
+ Bitmap mockBitmapCache = Bitmap.createBitmap(1366, 768, Bitmap.Config.ALPHA_8);
+
final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
drawable.setBounds(0, 0, 683, 384); // half each side of the view box
- drawable.scaleCanvasToBounds(canvas);
+ drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
expected.postScale(0.5f, 0.5f);
@@ -88,12 +100,14 @@ public class GlifPatternDrawableTest extends AndroidTestCase {
final Canvas canvas = new Canvas();
final Matrix expected = new Matrix(canvas.getMatrix());
+ Bitmap mockBitmapCache = Bitmap.createBitmap(1366, 768, Bitmap.Config.ALPHA_8);
+
final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
drawable.setBounds(0, 0, 683, 768); // half the width only
- drawable.scaleCanvasToBounds(canvas);
+ drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
expected.postScale(1f, 1f);
- expected.postTranslate(-100f, 0f);
+ expected.postTranslate(-99.718f, 0f);
assertEquals("Matrices should match", expected, canvas.getMatrix());
}
@@ -103,16 +117,51 @@ public class GlifPatternDrawableTest extends AndroidTestCase {
final Canvas canvas = new Canvas();
final Matrix expected = new Matrix(canvas.getMatrix());
+ Bitmap mockBitmapCache = Bitmap.createBitmap(1366, 768, Bitmap.Config.ALPHA_8);
+
final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
drawable.setBounds(0, 0, 1366, 384); // half the height only
- drawable.scaleCanvasToBounds(canvas);
+ drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
expected.postScale(1f, 1f);
- expected.postTranslate(0f, -87.5f);
+ expected.postTranslate(0f, -87.552f);
+
+ assertEquals("Matrices should match", expected, canvas.getMatrix());
+ }
+
+ @SmallTest
+ public void testScaleToCanvasMaxSize() {
+ final Canvas canvas = new Canvas();
+ final Matrix expected = new Matrix(canvas.getMatrix());
+
+ Bitmap mockBitmapCache = Bitmap.createBitmap(2049, 1152, Bitmap.Config.ALPHA_8);
+
+ final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
+ drawable.setBounds(0, 0, 1366, 768); // original viewbox size
+ drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
+
+ expected.postScale(1 / 1.5f, 1 / 1.5f);
+ expected.postTranslate(0f, 0f);
assertEquals("Matrices should match", expected, canvas.getMatrix());
}
+ @SmallTest
+ public void testMemoryAllocation() {
+ Debug.MemoryInfo memoryInfo = new Debug.MemoryInfo();
+ Debug.getMemoryInfo(memoryInfo);
+ final long memoryBefore = memoryInfo.getTotalPss(); // Get memory usage in KB
+
+ final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
+ drawable.setBounds(0, 0, 1366, 768);
+ drawable.createBitmapCache(2049, 1152);
+
+ Debug.getMemoryInfo(memoryInfo);
+ final long memoryAfter = memoryInfo.getTotalPss();
+ Log.i(TAG, "Memory allocated for bitmap cache: " + (memoryAfter - memoryBefore));
+ assertTrue("Memory allocation should not exceed 5MB", memoryAfter < memoryBefore + 5000);
+ }
+
private void assertSameColor(String message, int expected, int actual) {
try {
assertEquals(expected, actual);