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