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