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