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.google.android.setupdesign.view; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.content.pm.ApplicationInfo; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.Rect; 25 import android.graphics.drawable.Drawable; 26 import android.os.Build.VERSION; 27 import android.os.Build.VERSION_CODES; 28 import android.util.AttributeSet; 29 import android.util.LayoutDirection; 30 import android.view.Gravity; 31 import android.view.ViewOutlineProvider; 32 import android.widget.FrameLayout; 33 import com.google.android.setupdesign.R; 34 35 /** 36 * Class to draw the illustration of setup wizard. The {@code aspectRatio} attribute determines the 37 * aspect ratio of the top padding, which leaves space for the illustration. Draws the illustration 38 * drawable to fit the width of the view and fills the rest with the background. 39 * 40 * <p>If an aspect ratio is set, then the aspect ratio of the source drawable is maintained. 41 * Otherwise the aspect ratio will be ignored, only increasing the width of the illustration. 42 */ 43 public class Illustration extends FrameLayout { 44 45 // Size of the baseline grid in pixels 46 private float baselineGridSize; 47 private Drawable background; 48 private Drawable illustration; 49 private final Rect viewBounds = new Rect(); 50 private final Rect illustrationBounds = new Rect(); 51 private float scale = 1.0f; 52 private float aspectRatio = 0.0f; 53 Illustration(Context context)54 public Illustration(Context context) { 55 super(context); 56 init(null, 0); 57 } 58 Illustration(Context context, AttributeSet attrs)59 public Illustration(Context context, AttributeSet attrs) { 60 super(context, attrs); 61 init(attrs, 0); 62 } 63 64 @TargetApi(VERSION_CODES.HONEYCOMB) Illustration(Context context, AttributeSet attrs, int defStyleAttr)65 public Illustration(Context context, AttributeSet attrs, int defStyleAttr) { 66 super(context, attrs, defStyleAttr); 67 init(attrs, defStyleAttr); 68 } 69 70 // All the constructors delegate to this init method. The 3-argument constructor is not 71 // available in FrameLayout before v11, so call super with the exact same arguments. init(AttributeSet attrs, int defStyleAttr)72 private void init(AttributeSet attrs, int defStyleAttr) { 73 if (attrs != null) { 74 TypedArray a = 75 getContext().obtainStyledAttributes(attrs, R.styleable.SudIllustration, defStyleAttr, 0); 76 aspectRatio = a.getFloat(R.styleable.SudIllustration_sudAspectRatio, 0.0f); 77 a.recycle(); 78 } 79 // Number of pixels of the 8dp baseline grid as defined in material design specs 80 baselineGridSize = getResources().getDisplayMetrics().density * 8; 81 setWillNotDraw(false); 82 } 83 84 /** 85 * The background will be drawn to fill up the rest of the view. It will also be scaled by the 86 * same amount as the foreground so their textures look the same. 87 */ 88 // Override the deprecated setBackgroundDrawable method to support API < 16. View.setBackground 89 // forwards to setBackgroundDrawable in the framework implementation. 90 @SuppressWarnings("deprecation") 91 @Override setBackgroundDrawable(Drawable background)92 public void setBackgroundDrawable(Drawable background) { 93 if (background == this.background) { 94 return; 95 } 96 this.background = background; 97 invalidate(); 98 requestLayout(); 99 } 100 101 /** 102 * Sets the drawable used as the illustration. The drawable is expected to have intrinsic width 103 * and height defined and will be scaled to fit the width of the view. 104 */ setIllustration(Drawable illustration)105 public void setIllustration(Drawable illustration) { 106 if (illustration == this.illustration) { 107 return; 108 } 109 this.illustration = illustration; 110 invalidate(); 111 requestLayout(); 112 } 113 114 /** 115 * Set the aspect ratio reserved for the illustration. This overrides the top padding of the view 116 * according to the width of this view and the aspect ratio. Children views will start being laid 117 * out below this aspect ratio. 118 * 119 * @param aspectRatio A float value specifying the aspect ratio (= width / height). 0 to not 120 * override the top padding. 121 */ setAspectRatio(float aspectRatio)122 public void setAspectRatio(float aspectRatio) { 123 this.aspectRatio = aspectRatio; 124 invalidate(); 125 requestLayout(); 126 } 127 128 @Override 129 @Deprecated setForeground(Drawable d)130 public void setForeground(Drawable d) { 131 setIllustration(d); 132 } 133 134 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)135 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 136 if (aspectRatio != 0.0f) { 137 int parentWidth = MeasureSpec.getSize(widthMeasureSpec); 138 int illustrationHeight = (int) (parentWidth / aspectRatio); 139 illustrationHeight = (int) (illustrationHeight - (illustrationHeight % baselineGridSize)); 140 setPadding(0, illustrationHeight, 0, 0); 141 } 142 if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 143 //noinspection AndroidLintInlinedApi 144 setOutlineProvider(ViewOutlineProvider.BOUNDS); 145 } 146 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 147 } 148 149 @Override onLayout(boolean changed, int left, int top, int right, int bottom)150 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 151 final int layoutWidth = right - left; 152 final int layoutHeight = bottom - top; 153 if (illustration != null) { 154 int intrinsicWidth = illustration.getIntrinsicWidth(); 155 int intrinsicHeight = illustration.getIntrinsicHeight(); 156 157 viewBounds.set(0, 0, layoutWidth, layoutHeight); 158 if (aspectRatio != 0f) { 159 scale = layoutWidth / (float) intrinsicWidth; 160 intrinsicWidth = layoutWidth; 161 intrinsicHeight = (int) (intrinsicHeight * scale); 162 } 163 Gravity.apply( 164 Gravity.FILL_HORIZONTAL | Gravity.TOP, 165 intrinsicWidth, 166 intrinsicHeight, 167 viewBounds, 168 illustrationBounds); 169 illustration.setBounds(illustrationBounds); 170 } 171 if (background != null) { 172 // Scale the background bounds by the same scale to compensate for the scale done to the 173 // canvas in onDraw. 174 background.setBounds( 175 0, 176 0, 177 (int) Math.ceil(layoutWidth / scale), 178 (int) Math.ceil((layoutHeight - illustrationBounds.height()) / scale)); 179 } 180 super.onLayout(changed, left, top, right, bottom); 181 } 182 183 @Override onDraw(Canvas canvas)184 public void onDraw(Canvas canvas) { 185 if (background != null) { 186 // Draw the background filling parts not covered by the illustration 187 canvas.save(); 188 canvas.translate(0, illustrationBounds.height()); 189 // Scale the background so its size matches the foreground 190 canvas.scale(scale, scale, 0, 0); 191 if (VERSION.SDK_INT > VERSION_CODES.JELLY_BEAN_MR1 192 && shouldMirrorDrawable(background, getLayoutDirection())) { 193 // Flip the illustration for RTL layouts 194 canvas.scale(-1, 1); 195 canvas.translate(-background.getBounds().width(), 0); 196 } 197 background.draw(canvas); 198 canvas.restore(); 199 } 200 if (illustration != null) { 201 canvas.save(); 202 if (VERSION.SDK_INT > VERSION_CODES.JELLY_BEAN_MR1 203 && shouldMirrorDrawable(illustration, getLayoutDirection())) { 204 // Flip the illustration for RTL layouts 205 canvas.scale(-1, 1); 206 canvas.translate(-illustrationBounds.width(), 0); 207 } 208 // Draw the illustration 209 illustration.draw(canvas); 210 canvas.restore(); 211 } 212 super.onDraw(canvas); 213 } 214 shouldMirrorDrawable(Drawable drawable, int layoutDirection)215 private boolean shouldMirrorDrawable(Drawable drawable, int layoutDirection) { 216 if (layoutDirection == LayoutDirection.RTL) { 217 if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { 218 return drawable.isAutoMirrored(); 219 } else if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { 220 final int flags = getContext().getApplicationInfo().flags; 221 //noinspection AndroidLintInlinedApi 222 return (flags & ApplicationInfo.FLAG_SUPPORTS_RTL) != 0; 223 } 224 } 225 return false; 226 } 227 } 228