Home | History | Annotate | Download | only in setupwizardlib
      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.ColorMatrixColorFilter;
     27 import android.graphics.Paint;
     28 import android.graphics.Path;
     29 import android.graphics.PixelFormat;
     30 import android.graphics.PorterDuff;
     31 import android.graphics.PorterDuffXfermode;
     32 import android.graphics.Rect;
     33 import android.graphics.drawable.Drawable;
     34 import android.os.Build;
     35 import android.support.annotation.NonNull;
     36 import android.support.annotation.VisibleForTesting;
     37 
     38 import 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  */
     44 public 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(@NonNull 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