summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaurice Lam <yukl@google.com>2016-08-11 17:06:40 -0700
committerMaurice Lam <yukl@google.com>2016-08-23 00:40:09 +0000
commite59cdf30748da38326b1bc0071065bd3bf4d1db5 (patch)
treeb0a6c1aab4d68cf5b18abe2a7a2365272e13c3ed
parent8695624ce6c379ec2873a0b9bbee6e34515659ac (diff)
downloadsetupwizard-e59cdf30748da38326b1bc0071065bd3bf4d1db5.tar.gz
Share GlifPatternDrawable bitmap cache
Reduce memory usage of GlifPatternDrawable by - Setting max bitmap cache scale to 1.5x (similar to VectorDrawableCompat, which caps at 2048px) - Use ALPHA_8 mask instead of ARGB8888 - Share the cache across all pattern drawables, using a SoftReference The memory allocation of a single bitmap cache, measured on Pixel C, dropped from 17MB to 2.3MB. Memory consumption was measured in Android studio. 1. Open SetupLibrarySample app in Android Studio 2. Run app 3. Open "Android Monitor" tab, and go to Monitors tab 4. Press "Trigger GC" in Studio 5. Press "Start Allocation Tracking" 6. Start a new activity in the test app 7. press "Stop Allocation Tracking" Test: ./gradlew connectedAndroidTest Bug: 30813561 Change-Id: Ic78d6f2bdfa6a18a7dd2afc73f9647e2a7ed4cf1 (cherry picked from commit 515c0b156d03f7ea348303c75e37ee4c16d90557)
-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);