| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.setupwizardlib; |
| |
| import android.annotation.SuppressLint; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.ColorFilter; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.PixelFormat; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffXfermode; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.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 */ |
| |
| @SuppressLint("InlinedApi") |
| private static final int[] ATTRS_PRIMARY_COLOR = new int[] {android.R.attr.colorPrimary}; |
| |
| private static final float VIEWBOX_HEIGHT = 768f; |
| private static final float VIEWBOX_WIDTH = 1366f; |
| // X coordinate of scale focus, as a fraction of the width. (Range is 0 - 1) |
| private static final float SCALE_FOCUS_X = .146f; |
| // Y coordinate of scale focus, as a fraction 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> bitmapCache; |
| private static Path[] patternPaths; |
| private static int[] patternLightness; |
| |
| public static GlifPatternDrawable getDefault(Context context) { |
| int colorPrimary = 0; |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
| final TypedArray a = context.obtainStyledAttributes(ATTRS_PRIMARY_COLOR); |
| colorPrimary = a.getColor(0, Color.BLACK); |
| a.recycle(); |
| } |
| return new GlifPatternDrawable(colorPrimary); |
| } |
| |
| @VisibleForTesting |
| public static void invalidatePattern() { |
| bitmapCache = null; |
| } |
| |
| /* non-static section */ |
| |
| private int color; |
| private final Paint tempPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| |
| public GlifPatternDrawable(int color) { |
| setColor(color); |
| } |
| |
| @Override |
| public void draw(@NonNull Canvas canvas) { |
| final Rect bounds = getBounds(); |
| int drawableWidth = bounds.width(); |
| int drawableHeight = bounds.height(); |
| Bitmap bitmap = null; |
| if (bitmapCache != null) { |
| bitmap = bitmapCache.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 |
| tempPaint.reset(); |
| |
| bitmap = createBitmapCache(drawableWidth, drawableHeight); |
| bitmapCache = new SoftReference<>(bitmap); |
| |
| // Reset the paint to so it can be used to draw the bitmap |
| tempPaint.reset(); |
| } |
| |
| canvas.save(); |
| canvas.clipRect(bounds); |
| |
| scaleCanvasToBounds(canvas, bitmap, bounds); |
| canvas.drawColor(Color.BLACK); |
| tempPaint.setColor(Color.WHITE); |
| canvas.drawBitmap(bitmap, 0, 0, tempPaint); |
| canvas.drawColor(color); |
| |
| canvas.restore(); |
| } |
| |
| @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.scale(scale, scale); |
| |
| tempPaint.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. |
| |
| if (patternPaths == null) { |
| patternPaths = new Path[NUM_PATHS]; |
| // Lightness values of the pattern, range 0 - 255 |
| patternLightness = new int[] {10, 40, 51, 66, 91, 112, 130}; |
| |
| Path p = patternPaths[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 = patternPaths[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 = patternPaths[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 = patternPaths[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 = patternPaths[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 = patternPaths[5] = new Path(); |
| p.moveTo(178.44286f, 766.8571f); |
| 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 = patternPaths[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. |
| tempPaint.setColor(patternLightness[i] << 24); |
| canvas.drawPath(patternPaths[i], tempPaint); |
| } |
| |
| canvas.restore(); |
| tempPaint.reset(); |
| } |
| |
| @VisibleForTesting |
| 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; |
| |
| // First scale both sides to fit independently. |
| canvas.scale(scaleX, scaleY); |
| if (scaleY > scaleX) { |
| // 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); |
| } |
| } |
| |
| @Override |
| public void setAlpha(int i) { |
| // Ignore |
| } |
| |
| @Override |
| public void setColorFilter(ColorFilter colorFilter) { |
| // Ignore |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.UNKNOWN; |
| } |
| |
| /** |
| * 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) { |
| final int r = Color.red(color); |
| final int g = Color.green(color); |
| final int b = Color.blue(color); |
| this.color = Color.argb(COLOR_ALPHA_INT, r, g, b); |
| 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 Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); |
| } |
| } |