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