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; 18 19 import android.annotation.SuppressLint; 20 import android.annotation.TargetApi; 21 import android.content.Context; 22 import android.content.res.ColorStateList; 23 import android.content.res.TypedArray; 24 import android.graphics.Shader.TileMode; 25 import android.graphics.drawable.BitmapDrawable; 26 import android.graphics.drawable.Drawable; 27 import android.graphics.drawable.LayerDrawable; 28 import android.os.Build.VERSION; 29 import android.os.Build.VERSION_CODES; 30 import android.os.Parcel; 31 import android.os.Parcelable; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.util.TypedValue; 35 import android.view.Gravity; 36 import android.view.LayoutInflater; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.widget.ScrollView; 40 import android.widget.TextView; 41 import com.google.android.setupcompat.internal.TemplateLayout; 42 import com.google.android.setupcompat.template.SystemNavBarMixin; 43 import com.google.android.setupdesign.template.DescriptionMixin; 44 import com.google.android.setupdesign.template.HeaderMixin; 45 import com.google.android.setupdesign.template.NavigationBarMixin; 46 import com.google.android.setupdesign.template.ProgressBarMixin; 47 import com.google.android.setupdesign.template.RequireScrollMixin; 48 import com.google.android.setupdesign.template.ScrollViewScrollHandlingDelegate; 49 import com.google.android.setupdesign.view.Illustration; 50 import com.google.android.setupdesign.view.NavigationBar; 51 52 public class SetupWizardLayout extends TemplateLayout { 53 54 private static final String TAG = "SetupWizardLayout"; 55 SetupWizardLayout(Context context)56 public SetupWizardLayout(Context context) { 57 super(context, 0, 0); 58 init(null, R.attr.sudLayoutTheme); 59 } 60 SetupWizardLayout(Context context, int template)61 public SetupWizardLayout(Context context, int template) { 62 this(context, template, 0); 63 } 64 SetupWizardLayout(Context context, int template, int containerId)65 public SetupWizardLayout(Context context, int template, int containerId) { 66 super(context, template, containerId); 67 init(null, R.attr.sudLayoutTheme); 68 } 69 SetupWizardLayout(Context context, AttributeSet attrs)70 public SetupWizardLayout(Context context, AttributeSet attrs) { 71 super(context, attrs); 72 init(attrs, R.attr.sudLayoutTheme); 73 } 74 75 @TargetApi(VERSION_CODES.HONEYCOMB) SetupWizardLayout(Context context, AttributeSet attrs, int defStyleAttr)76 public SetupWizardLayout(Context context, AttributeSet attrs, int defStyleAttr) { 77 super(context, attrs, defStyleAttr); 78 init(attrs, defStyleAttr); 79 } 80 81 // All the constructors delegate to this init method. The 3-argument constructor is not 82 // available in LinearLayout before v11, so call super with the exact same arguments. init(AttributeSet attrs, int defStyleAttr)83 private void init(AttributeSet attrs, int defStyleAttr) { 84 if (isInEditMode()) { 85 return; 86 } 87 88 registerMixin(SystemNavBarMixin.class, new SystemNavBarMixin(this, /* window= */ null)); 89 registerMixin( 90 HeaderMixin.class, 91 new HeaderMixin(this, attrs, defStyleAttr)); 92 registerMixin(DescriptionMixin.class, new DescriptionMixin(this, attrs, defStyleAttr)); 93 registerMixin(ProgressBarMixin.class, new ProgressBarMixin(this)); 94 registerMixin(NavigationBarMixin.class, new NavigationBarMixin(this)); 95 final RequireScrollMixin requireScrollMixin = new RequireScrollMixin(this); 96 registerMixin(RequireScrollMixin.class, requireScrollMixin); 97 98 final ScrollView scrollView = getScrollView(); 99 if (scrollView != null) { 100 requireScrollMixin.setScrollHandlingDelegate( 101 new ScrollViewScrollHandlingDelegate(requireScrollMixin, scrollView)); 102 } 103 104 final TypedArray a = 105 getContext() 106 .obtainStyledAttributes(attrs, R.styleable.SudSetupWizardLayout, defStyleAttr, 0); 107 108 // Set the background from XML, either directly or built from a bitmap tile 109 final Drawable background = a.getDrawable(R.styleable.SudSetupWizardLayout_sudBackground); 110 if (background != null) { 111 setLayoutBackground(background); 112 } else { 113 final Drawable backgroundTile = 114 a.getDrawable(R.styleable.SudSetupWizardLayout_sudBackgroundTile); 115 if (backgroundTile != null) { 116 setBackgroundTile(backgroundTile); 117 } 118 } 119 120 // Set the illustration from XML, either directly or built from image + horizontal tile 121 final Drawable illustration = a.getDrawable(R.styleable.SudSetupWizardLayout_sudIllustration); 122 if (illustration != null) { 123 setIllustration(illustration); 124 } else { 125 final Drawable illustrationImage = 126 a.getDrawable(R.styleable.SudSetupWizardLayout_sudIllustrationImage); 127 final Drawable horizontalTile = 128 a.getDrawable(R.styleable.SudSetupWizardLayout_sudIllustrationHorizontalTile); 129 if (illustrationImage != null && horizontalTile != null) { 130 setIllustration(illustrationImage, horizontalTile); 131 } 132 } 133 134 // Set the top padding of the illustration 135 int decorPaddingTop = 136 a.getDimensionPixelSize(R.styleable.SudSetupWizardLayout_sudDecorPaddingTop, -1); 137 if (decorPaddingTop == -1) { 138 decorPaddingTop = getResources().getDimensionPixelSize(R.dimen.sud_decor_padding_top); 139 } 140 setDecorPaddingTop(decorPaddingTop); 141 142 // Set the illustration aspect ratio. See Illustration.setAspectRatio(float). This will 143 // override sudDecorPaddingTop if its value is not 0. 144 float illustrationAspectRatio = 145 a.getFloat(R.styleable.SudSetupWizardLayout_sudIllustrationAspectRatio, -1f); 146 if (illustrationAspectRatio == -1f) { 147 final TypedValue out = new TypedValue(); 148 getResources().getValue(R.dimen.sud_illustration_aspect_ratio, out, true); 149 illustrationAspectRatio = out.getFloat(); 150 } 151 setIllustrationAspectRatio(illustrationAspectRatio); 152 153 a.recycle(); 154 } 155 156 @Override onSaveInstanceState()157 protected Parcelable onSaveInstanceState() { 158 final Parcelable parcelable = super.onSaveInstanceState(); 159 final SavedState ss = new SavedState(parcelable); 160 ss.isProgressBarShown = isProgressBarShown(); 161 return ss; 162 } 163 164 @Override onRestoreInstanceState(Parcelable state)165 protected void onRestoreInstanceState(Parcelable state) { 166 if (!(state instanceof SavedState)) { 167 Log.w(TAG, "Ignoring restore instance state " + state); 168 super.onRestoreInstanceState(state); 169 return; 170 } 171 172 final SavedState ss = (SavedState) state; 173 super.onRestoreInstanceState(ss.getSuperState()); 174 final boolean isProgressBarShown = ss.isProgressBarShown; 175 setProgressBarShown(isProgressBarShown); 176 } 177 178 @Override onInflateTemplate(LayoutInflater inflater, int template)179 protected View onInflateTemplate(LayoutInflater inflater, int template) { 180 if (template == 0) { 181 template = R.layout.sud_template; 182 } 183 return inflateTemplate(inflater, R.style.SudThemeMaterial_Light, template); 184 } 185 186 @Override findContainer(int containerId)187 protected ViewGroup findContainer(int containerId) { 188 if (containerId == 0) { 189 containerId = R.id.sud_layout_content; 190 } 191 return super.findContainer(containerId); 192 } 193 getNavigationBar()194 public NavigationBar getNavigationBar() { 195 return getMixin(NavigationBarMixin.class).getNavigationBar(); 196 } 197 getScrollView()198 public ScrollView getScrollView() { 199 final View view = findManagedViewById(R.id.sud_bottom_scroll_view); 200 return view instanceof ScrollView ? (ScrollView) view : null; 201 } 202 requireScrollToBottom()203 public void requireScrollToBottom() { 204 final RequireScrollMixin requireScrollMixin = getMixin(RequireScrollMixin.class); 205 final NavigationBar navigationBar = getNavigationBar(); 206 if (navigationBar != null) { 207 requireScrollMixin.requireScrollWithNavigationBar(navigationBar); 208 } else { 209 Log.e(TAG, "Cannot require scroll. Navigation bar is null."); 210 } 211 } 212 setHeaderText(int title)213 public void setHeaderText(int title) { 214 getMixin(HeaderMixin.class).setText(title); 215 } 216 setHeaderText(CharSequence title)217 public void setHeaderText(CharSequence title) { 218 getMixin(HeaderMixin.class).setText(title); 219 } 220 getHeaderText()221 public CharSequence getHeaderText() { 222 return getMixin(HeaderMixin.class).getText(); 223 } 224 getHeaderTextView()225 public TextView getHeaderTextView() { 226 return getMixin(HeaderMixin.class).getTextView(); 227 } 228 229 /** 230 * Set the illustration of the layout. The drawable will be applied as is, and the bounds will be 231 * set as implemented in {@link com.google.android.setupdesign.view.Illustration}. To create a 232 * suitable drawable from an asset and a horizontal repeating tile, use {@link 233 * #setIllustration(int, int)} instead. 234 * 235 * @param drawable The drawable specifying the illustration. 236 */ setIllustration(Drawable drawable)237 public void setIllustration(Drawable drawable) { 238 final View view = findManagedViewById(R.id.sud_layout_decor); 239 if (view instanceof Illustration) { 240 final Illustration illustration = (Illustration) view; 241 illustration.setIllustration(drawable); 242 } 243 } 244 245 /** 246 * Set the illustration of the layout, which will be created asset and the horizontal tile as 247 * suitable. On phone layouts (not sw600dp), the asset will be scaled, maintaining aspect ratio. 248 * On tablets (sw600dp), the assets will always have 256dp height and the rest of the illustration 249 * area that the asset doesn't fill will be covered by the horizontalTile. 250 * 251 * @param asset Resource ID of the illustration asset. 252 * @param horizontalTile Resource ID of the horizontally repeating tile for tablet layout. 253 */ setIllustration(int asset, int horizontalTile)254 public void setIllustration(int asset, int horizontalTile) { 255 final View view = findManagedViewById(R.id.sud_layout_decor); 256 if (view instanceof Illustration) { 257 final Illustration illustration = (Illustration) view; 258 final Drawable illustrationDrawable = getIllustration(asset, horizontalTile); 259 illustration.setIllustration(illustrationDrawable); 260 } 261 } 262 setIllustration(Drawable asset, Drawable horizontalTile)263 private void setIllustration(Drawable asset, Drawable horizontalTile) { 264 final View view = findManagedViewById(R.id.sud_layout_decor); 265 if (view instanceof Illustration) { 266 final Illustration illustration = (Illustration) view; 267 final Drawable illustrationDrawable = getIllustration(asset, horizontalTile); 268 illustration.setIllustration(illustrationDrawable); 269 } 270 } 271 272 /** 273 * Sets the aspect ratio of the illustration. This will be the space (padding top) reserved above 274 * the header text. This will override the padding top of the illustration. 275 * 276 * @param aspectRatio The aspect ratio 277 * @see com.google.android.setupdesign.view.Illustration#setAspectRatio(float) 278 */ setIllustrationAspectRatio(float aspectRatio)279 public void setIllustrationAspectRatio(float aspectRatio) { 280 final View view = findManagedViewById(R.id.sud_layout_decor); 281 if (view instanceof Illustration) { 282 final Illustration illustration = (Illustration) view; 283 illustration.setAspectRatio(aspectRatio); 284 } 285 } 286 287 /** 288 * Set the top padding of the decor view. If the decor is an Illustration and the aspect ratio is 289 * set, this value will be overridden. 290 * 291 * <p>Note: Currently the default top padding for tablet landscape is 128dp, which is the offset 292 * of the card from the top. This is likely to change in future versions so this value aligns with 293 * the height of the illustration instead. 294 * 295 * @param paddingTop The top padding in pixels. 296 */ setDecorPaddingTop(int paddingTop)297 public void setDecorPaddingTop(int paddingTop) { 298 final View view = findManagedViewById(R.id.sud_layout_decor); 299 if (view != null) { 300 view.setPadding( 301 view.getPaddingLeft(), paddingTop, view.getPaddingRight(), view.getPaddingBottom()); 302 } 303 } 304 305 /** 306 * Set the background of the layout, which is expected to be able to extend infinitely. If it is a 307 * bitmap tile and you want it to repeat, use {@link #setBackgroundTile(int)} instead. 308 */ setLayoutBackground(Drawable background)309 public void setLayoutBackground(Drawable background) { 310 final View view = findManagedViewById(R.id.sud_layout_decor); 311 if (view != null) { 312 //noinspection deprecation 313 view.setBackgroundDrawable(background); 314 } 315 } 316 317 /** 318 * Set the background of the layout to a repeating bitmap tile. To use a different kind of 319 * drawable, use {@link #setLayoutBackground(android.graphics.drawable.Drawable)} instead. 320 */ setBackgroundTile(int backgroundTile)321 public void setBackgroundTile(int backgroundTile) { 322 final Drawable backgroundTileDrawable = getContext().getResources().getDrawable(backgroundTile); 323 setBackgroundTile(backgroundTileDrawable); 324 } 325 setBackgroundTile(Drawable backgroundTile)326 private void setBackgroundTile(Drawable backgroundTile) { 327 if (backgroundTile instanceof BitmapDrawable) { 328 ((BitmapDrawable) backgroundTile).setTileModeXY(TileMode.REPEAT, TileMode.REPEAT); 329 } 330 setLayoutBackground(backgroundTile); 331 } 332 getIllustration(int asset, int horizontalTile)333 private Drawable getIllustration(int asset, int horizontalTile) { 334 final Context context = getContext(); 335 final Drawable assetDrawable = context.getResources().getDrawable(asset); 336 final Drawable tile = context.getResources().getDrawable(horizontalTile); 337 return getIllustration(assetDrawable, tile); 338 } 339 340 @SuppressLint("RtlHardcoded") getIllustration(Drawable asset, Drawable horizontalTile)341 private Drawable getIllustration(Drawable asset, Drawable horizontalTile) { 342 final Context context = getContext(); 343 if (context.getResources().getBoolean(R.bool.sudUseTabletLayout)) { 344 // If it is a "tablet" (sw600dp), create a LayerDrawable with the horizontal tile. 345 if (horizontalTile instanceof BitmapDrawable) { 346 ((BitmapDrawable) horizontalTile).setTileModeX(TileMode.REPEAT); 347 ((BitmapDrawable) horizontalTile).setGravity(Gravity.TOP); 348 } 349 if (asset instanceof BitmapDrawable) { 350 // Always specify TOP | LEFT, Illustration will flip the entire LayerDrawable. 351 ((BitmapDrawable) asset).setGravity(Gravity.TOP | Gravity.LEFT); 352 } 353 final LayerDrawable layers = new LayerDrawable(new Drawable[] {horizontalTile, asset}); 354 if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { 355 layers.setAutoMirrored(true); 356 } 357 return layers; 358 } else { 359 // If it is a "phone" (not sw600dp), simply return the illustration 360 if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { 361 asset.setAutoMirrored(true); 362 } 363 return asset; 364 } 365 } 366 isProgressBarShown()367 public boolean isProgressBarShown() { 368 return getMixin(ProgressBarMixin.class).isShown(); 369 } 370 371 /** 372 * Sets whether the progress bar below the header text is shown or not. The progress bar is a 373 * lazily inflated ViewStub, which means the progress bar will not actually be part of the view 374 * hierarchy until the first time this is set to {@code true}. 375 */ setProgressBarShown(boolean shown)376 public void setProgressBarShown(boolean shown) { 377 getMixin(ProgressBarMixin.class).setShown(shown); 378 } 379 380 /** @deprecated Use {@link #setProgressBarShown(boolean)} */ 381 @Deprecated showProgressBar()382 public void showProgressBar() { 383 setProgressBarShown(true); 384 } 385 386 /** @deprecated Use {@link #setProgressBarShown(boolean)} */ 387 @Deprecated hideProgressBar()388 public void hideProgressBar() { 389 setProgressBarShown(false); 390 } 391 setProgressBarColor(ColorStateList color)392 public void setProgressBarColor(ColorStateList color) { 393 getMixin(ProgressBarMixin.class).setColor(color); 394 } 395 getProgressBarColor()396 public ColorStateList getProgressBarColor() { 397 return getMixin(ProgressBarMixin.class).getColor(); 398 } 399 400 /* Misc */ 401 402 protected static class SavedState extends BaseSavedState { 403 404 boolean isProgressBarShown = false; 405 SavedState(Parcelable parcelable)406 public SavedState(Parcelable parcelable) { 407 super(parcelable); 408 } 409 SavedState(Parcel source)410 public SavedState(Parcel source) { 411 super(source); 412 isProgressBarShown = source.readInt() != 0; 413 } 414 415 @Override writeToParcel(Parcel dest, int flags)416 public void writeToParcel(Parcel dest, int flags) { 417 super.writeToParcel(dest, flags); 418 dest.writeInt(isProgressBarShown ? 1 : 0); 419 } 420 421 public static final Parcelable.Creator<SavedState> CREATOR = 422 new Parcelable.Creator<SavedState>() { 423 424 @Override 425 public SavedState createFromParcel(Parcel parcel) { 426 return new SavedState(parcel); 427 } 428 429 @Override 430 public SavedState[] newArray(int size) { 431 return new SavedState[size]; 432 } 433 }; 434 } 435 } 436