GlifPatternDrawable.java revision 6e0e22635d5fa943f72251e534f37527d3b3468c
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.setupwizardlib;
18
19import android.annotation.SuppressLint;
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Color;
25import android.graphics.ColorFilter;
26import android.graphics.ColorMatrixColorFilter;
27import android.graphics.Paint;
28import android.graphics.Path;
29import android.graphics.PixelFormat;
30import android.graphics.PorterDuff;
31import android.graphics.PorterDuffXfermode;
32import android.graphics.Rect;
33import android.graphics.drawable.Drawable;
34import android.os.Build;
35
36import com.android.setupwizardlib.annotations.VisibleForTesting;
37
38import java.lang.ref.SoftReference;
39
40/**
41 * This class draws the GLIF pattern used as the status bar background for phones and background for
42 * tablets in GLIF layout.
43 */
44public class GlifPatternDrawable extends Drawable {
45    /*
46     * This class essentially implements a simple SVG in Java code, with some special handling of
47     * scaling when given different bounds.
48     */
49
50    /* static section */
51
52    @SuppressLint("InlinedApi")
53    private static final int[] ATTRS_PRIMARY_COLOR = new int[]{ android.R.attr.colorPrimary };
54
55    private static final float VIEWBOX_HEIGHT = 768f;
56    private static final float VIEWBOX_WIDTH = 1366f;
57    // X coordinate of scale focus, as a fraction of of the width. (Range is 0 - 1)
58    private static final float SCALE_FOCUS_X = .146f;
59    // Y coordinate of scale focus, as a fraction of of the height. (Range is 0 - 1)
60    private static final float SCALE_FOCUS_Y = .228f;
61
62    // Alpha component of the color to be drawn, on top of the grayscale pattern. (Range is 0 - 1)
63    private static final float COLOR_ALPHA = .8f;
64    // Int version of COLOR_ALPHA. (Range is 0 - 255)
65    private static final int COLOR_ALPHA_INT = (int) (COLOR_ALPHA * 255);
66
67    // Cap the bitmap size, such that it won't hurt the performance too much
68    // and it won't crash due to a very large scale.
69    // The drawable will look blurry above this size.
70    // This is a multiplier applied on top of the viewbox size.
71    // Resulting max cache size = (1.5 x 1366, 1.5 x 768) = (2049, 1152)
72    private static final float MAX_CACHED_BITMAP_SCALE = 1.5f;
73
74    private static final int NUM_PATHS = 7;
75
76    private static SoftReference<Bitmap> sBitmapCache;
77    private static Path[] sPatternPaths;
78    private static int[] sPatternLightness;
79
80    public static GlifPatternDrawable getDefault(Context context) {
81        int colorPrimary = 0;
82        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
83            final TypedArray a = context.obtainStyledAttributes(ATTRS_PRIMARY_COLOR);
84            colorPrimary = a.getColor(0, Color.BLACK);
85            a.recycle();
86        }
87        return new GlifPatternDrawable(colorPrimary);
88    }
89
90    @VisibleForTesting
91    public static void invalidatePattern() {
92        sBitmapCache = null;
93    }
94
95    /* non-static section */
96
97    private int mColor;
98    private Paint mTempPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
99    private ColorFilter mColorFilter;
100
101    public GlifPatternDrawable(int color) {
102        setColor(color);
103    }
104
105    @Override
106    public void draw(Canvas canvas) {
107        final Rect bounds = getBounds();
108        int drawableWidth = bounds.width();
109        int drawableHeight = bounds.height();
110        Bitmap bitmap = null;
111        if (sBitmapCache != null) {
112            bitmap = sBitmapCache.get();
113        }
114        if (bitmap != null) {
115            final int bitmapWidth = bitmap.getWidth();
116            final int bitmapHeight = bitmap.getHeight();
117            // Invalidate the cache if this drawable is bigger and we can still create a bigger
118            // cache.
119            if (drawableWidth > bitmapWidth
120                    && bitmapWidth < VIEWBOX_WIDTH * MAX_CACHED_BITMAP_SCALE) {
121                bitmap = null;
122            } else if (drawableHeight > bitmapHeight
123                    && bitmapHeight < VIEWBOX_HEIGHT * MAX_CACHED_BITMAP_SCALE) {
124                bitmap = null;
125            }
126        }
127
128        if (bitmap == null) {
129            // Reset the paint so it can be used to draw the paths in renderOnCanvas
130            mTempPaint.reset();
131
132            bitmap = createBitmapCache(drawableWidth, drawableHeight);
133            sBitmapCache = new SoftReference<>(bitmap);
134
135            // Reset the paint to so it can be used to draw the bitmap
136            mTempPaint.reset();
137        }
138
139        canvas.save();
140        canvas.clipRect(bounds);
141
142        scaleCanvasToBounds(canvas, bitmap, bounds);
143        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
144                && canvas.isHardwareAccelerated()) {
145            mTempPaint.setColorFilter(mColorFilter);
146            canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
147        } else {
148            // Software renderer doesn't work properly with ColorMatrix filter on ALPHA_8 bitmaps.
149            canvas.drawColor(Color.BLACK);
150            mTempPaint.setColor(Color.WHITE);
151            canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
152            canvas.drawColor(mColor);
153        }
154
155        canvas.restore();
156    }
157
158    @VisibleForTesting
159    public Bitmap createBitmapCache(int drawableWidth, int drawableHeight) {
160        float scaleX = drawableWidth / VIEWBOX_WIDTH;
161        float scaleY = drawableHeight / VIEWBOX_HEIGHT;
162        float scale = Math.max(scaleX, scaleY);
163        scale = Math.min(MAX_CACHED_BITMAP_SCALE, scale);
164
165
166        int scaledWidth = (int) (VIEWBOX_WIDTH * scale);
167        int scaledHeight = (int) (VIEWBOX_HEIGHT * scale);
168
169        // Use ALPHA_8 mask to save memory, since the pattern is grayscale only anyway.
170        Bitmap bitmap = Bitmap.createBitmap(
171                scaledWidth,
172                scaledHeight,
173                Bitmap.Config.ALPHA_8);
174        Canvas bitmapCanvas = new Canvas(bitmap);
175        renderOnCanvas(bitmapCanvas, scale);
176        return bitmap;
177    }
178
179    private void renderOnCanvas(Canvas canvas, float scale) {
180        canvas.save();
181        canvas.scale(scale, scale);
182
183        mTempPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
184
185        // Draw the pattern by creating the paths, adjusting the colors and drawing them. The path
186        // values are extracted from the SVG of the pattern file.
187
188        if (sPatternPaths == null) {
189            sPatternPaths = new Path[NUM_PATHS];
190            // Lightness values of the pattern, range 0 - 255
191            sPatternLightness = new int[] { 10, 40, 51, 66, 91, 112, 130 };
192
193            Path p = sPatternPaths[0] = new Path();
194            p.moveTo(1029.4f, 357.5f);
195            p.lineTo(1366f, 759.1f);
196            p.lineTo(1366f, 0f);
197            p.lineTo(1137.7f, 0f);
198            p.close();
199
200            p = sPatternPaths[1] = new Path();
201            p.moveTo(1138.1f, 0f);
202            p.rLineTo(-144.8f, 768f);
203            p.rLineTo(372.7f, 0f);
204            p.rLineTo(0f, -524f);
205            p.cubicTo(1290.7f, 121.6f, 1219.2f, 41.1f, 1178.7f, 0f);
206            p.close();
207
208            p = sPatternPaths[2] = new Path();
209            p.moveTo(949.8f, 768f);
210            p.rCubicTo(92.6f, -170.6f, 213f, -440.3f, 269.4f, -768f);
211            p.lineTo(585f, 0f);
212            p.rLineTo(2.1f, 766f);
213            p.close();
214
215            p = sPatternPaths[3] = new Path();
216            p.moveTo(471.1f, 768f);
217            p.rMoveTo(704.5f, 0f);
218            p.cubicTo(1123.6f, 563.3f, 1027.4f, 275.2f, 856.2f, 0f);
219            p.lineTo(476.4f, 0f);
220            p.rLineTo(-5.3f, 768f);
221            p.close();
222
223            p = sPatternPaths[4] = new Path();
224            p.moveTo(323.1f, 768f);
225            p.moveTo(777.5f, 768f);
226            p.cubicTo(661.9f, 348.8f, 427.2f, 21.4f, 401.2f, 25.4f);
227            p.lineTo(323.1f, 768f);
228            p.close();
229
230            p = sPatternPaths[5] = new Path();
231            p.moveTo(178.44286f, 766.85714f);
232            p.lineTo(308.7f, 768f);
233            p.cubicTo(381.7f, 604.6f, 481.6f, 344.3f, 562.2f, 0f);
234            p.lineTo(0f, 0f);
235            p.close();
236
237            p = sPatternPaths[6] = new Path();
238            p.moveTo(146f, 0f);
239            p.lineTo(0f, 0f);
240            p.lineTo(0f, 768f);
241            p.lineTo(394.2f, 768f);
242            p.cubicTo(327.7f, 475.3f, 228.5f, 201f, 146f, 0f);
243            p.close();
244        }
245
246        for (int i = 0; i < NUM_PATHS; i++) {
247            // Color is 0xAARRGGBB, so alpha << 24 will create a color with (alpha)% black.
248            // Although the color components don't really matter, since the backing bitmap cache is
249            // ALPHA_8.
250            mTempPaint.setColor(sPatternLightness[i] << 24);
251            canvas.drawPath(sPatternPaths[i], mTempPaint);
252        }
253
254        canvas.restore();
255        mTempPaint.reset();
256    }
257
258    @VisibleForTesting
259    public void scaleCanvasToBounds(Canvas canvas, Bitmap bitmap, Rect drawableBounds) {
260        int bitmapWidth = bitmap.getWidth();
261        int bitmapHeight = bitmap.getHeight();
262        float scaleX = drawableBounds.width() / (float) bitmapWidth;
263        float scaleY = drawableBounds.height() / (float) bitmapHeight;
264
265        // First scale both sides to fit independently.
266        canvas.scale(scaleX, scaleY);
267        if (scaleY > scaleX) {
268            // Adjust x-scale to maintain aspect ratio using the pivot, so that more of the texture
269            // and less of the blank space on the left edge is seen.
270            canvas.scale(scaleY / scaleX, 1f, SCALE_FOCUS_X * bitmapWidth, 0f);
271        } else if (scaleX > scaleY) {
272            // Adjust y-scale to maintain aspect ratio using the pivot, so that an intersection of
273            // two "circles" can always be seen.
274            canvas.scale(1f, scaleX / scaleY, 0f, SCALE_FOCUS_Y * bitmapHeight);
275        }
276    }
277
278    @Override
279    public void setAlpha(int i) {
280        // Ignore
281    }
282
283    @Override
284    public void setColorFilter(ColorFilter colorFilter) {
285        // Ignore
286    }
287
288    @Override
289    public int getOpacity() {
290        return PixelFormat.UNKNOWN;
291    }
292
293    /**
294     * Sets the color used as the base color of this pattern drawable. The alpha component of the
295     * color will be ignored.
296     */
297    public void setColor(int color) {
298        final int r = Color.red(color);
299        final int g = Color.green(color);
300        final int b = Color.blue(color);
301        mColor = Color.argb(COLOR_ALPHA_INT, r, g, b);
302        mColorFilter = new ColorMatrixColorFilter(new float[] {
303                0, 0, 0, 1 - COLOR_ALPHA, r * COLOR_ALPHA,
304                0, 0, 0, 1 - COLOR_ALPHA, g * COLOR_ALPHA,
305                0, 0, 0, 1 - COLOR_ALPHA, b * COLOR_ALPHA,
306                0, 0, 0,               0,             255
307        });
308        invalidateSelf();
309    }
310
311    /**
312     * @return The color used as the base color of this pattern drawable. The alpha component of
313     * this is always 255.
314     */
315    public int getColor() {
316        return Color.argb(255, Color.red(mColor), Color.green(mColor), Color.blue(mColor));
317    }
318}
319