diff options
-rw-r--r-- | library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java | 315 | ||||
-rw-r--r-- | library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java | 59 |
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); |