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.TargetApi; 20 import android.content.Context; 21 import android.content.res.ColorStateList; 22 import android.content.res.TypedArray; 23 import android.graphics.drawable.ColorDrawable; 24 import android.graphics.drawable.Drawable; 25 import android.os.Build; 26 import android.os.Build.VERSION_CODES; 27 import android.util.AttributeSet; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.ViewStub; 32 import android.widget.ProgressBar; 33 import android.widget.ScrollView; 34 import android.widget.TextView; 35 import androidx.annotation.ColorInt; 36 import androidx.annotation.LayoutRes; 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 import androidx.annotation.StringRes; 40 import com.google.android.setupcompat.PartnerCustomizationLayout; 41 import com.google.android.setupcompat.partnerconfig.PartnerConfig; 42 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper; 43 import com.google.android.setupcompat.template.StatusBarMixin; 44 import com.google.android.setupdesign.template.DescriptionMixin; 45 import com.google.android.setupdesign.template.HeaderMixin; 46 import com.google.android.setupdesign.template.IconMixin; 47 import com.google.android.setupdesign.template.IllustrationProgressMixin; 48 import com.google.android.setupdesign.template.ProfileMixin; 49 import com.google.android.setupdesign.template.ProgressBarMixin; 50 import com.google.android.setupdesign.template.RequireScrollMixin; 51 import com.google.android.setupdesign.template.ScrollViewScrollHandlingDelegate; 52 import com.google.android.setupdesign.util.DescriptionStyler; 53 import com.google.android.setupdesign.util.LayoutStyler; 54 55 /** 56 * Layout for the GLIF theme used in Setup Wizard for N. 57 * 58 * <p>Example usage: 59 * 60 * <pre>{@code 61 * <com.google.android.setupdesign.GlifLayout 62 * xmlns:android="http://schemas.android.com/apk/res/android" 63 * xmlns:app="http://schemas.android.com/apk/res-auto" 64 * android:layout_width="match_parent" 65 * android:layout_height="match_parent" 66 * android:icon="@drawable/my_icon" 67 * app:sucHeaderText="@string/my_title"> 68 * 69 * <!-- Content here --> 70 * 71 * </com.google.android.setupdesign.GlifLayout> 72 * }</pre> 73 */ 74 public class GlifLayout extends PartnerCustomizationLayout { 75 76 private ColorStateList primaryColor; 77 78 private boolean backgroundPatterned = true; 79 80 private boolean applyPartnerHeavyThemeResource = false; 81 82 /** The color of the background. If null, the color will inherit from primaryColor. */ 83 @Nullable private ColorStateList backgroundBaseColor; 84 GlifLayout(Context context)85 public GlifLayout(Context context) { 86 this(context, 0, 0); 87 } 88 GlifLayout(Context context, int template)89 public GlifLayout(Context context, int template) { 90 this(context, template, 0); 91 } 92 GlifLayout(Context context, int template, int containerId)93 public GlifLayout(Context context, int template, int containerId) { 94 super(context, template, containerId); 95 init(null, R.attr.sudLayoutTheme); 96 } 97 GlifLayout(Context context, AttributeSet attrs)98 public GlifLayout(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 init(attrs, R.attr.sudLayoutTheme); 101 } 102 103 @TargetApi(VERSION_CODES.HONEYCOMB) GlifLayout(Context context, AttributeSet attrs, int defStyleAttr)104 public GlifLayout(Context context, AttributeSet attrs, int defStyleAttr) { 105 super(context, attrs, defStyleAttr); 106 init(attrs, defStyleAttr); 107 } 108 109 // All the constructors delegate to this init method. The 3-argument constructor is not 110 // available in LinearLayout before v11, so call super with the exact same arguments. init(AttributeSet attrs, int defStyleAttr)111 private void init(AttributeSet attrs, int defStyleAttr) { 112 if (isInEditMode()) { 113 return; 114 } 115 116 TypedArray a = 117 getContext().obtainStyledAttributes(attrs, R.styleable.SudGlifLayout, defStyleAttr, 0); 118 boolean usePartnerHeavyTheme = 119 a.getBoolean(R.styleable.SudGlifLayout_sudUsePartnerHeavyTheme, false); 120 applyPartnerHeavyThemeResource = shouldApplyPartnerResource() && usePartnerHeavyTheme; 121 122 registerMixin(HeaderMixin.class, new HeaderMixin(this, attrs, defStyleAttr)); 123 registerMixin(DescriptionMixin.class, new DescriptionMixin(this, attrs, defStyleAttr)); 124 registerMixin(IconMixin.class, new IconMixin(this, attrs, defStyleAttr)); 125 registerMixin(ProfileMixin.class, new ProfileMixin(this, attrs, defStyleAttr)); 126 registerMixin(ProgressBarMixin.class, new ProgressBarMixin(this, attrs, defStyleAttr)); 127 registerMixin(IllustrationProgressMixin.class, new IllustrationProgressMixin(this)); 128 final RequireScrollMixin requireScrollMixin = new RequireScrollMixin(this); 129 registerMixin(RequireScrollMixin.class, requireScrollMixin); 130 131 final ScrollView scrollView = getScrollView(); 132 if (scrollView != null) { 133 requireScrollMixin.setScrollHandlingDelegate( 134 new ScrollViewScrollHandlingDelegate(requireScrollMixin, scrollView)); 135 } 136 137 ColorStateList primaryColor = a.getColorStateList(R.styleable.SudGlifLayout_sudColorPrimary); 138 if (primaryColor != null) { 139 setPrimaryColor(primaryColor); 140 } 141 if (shouldApplyPartnerHeavyThemeResource()) { 142 updateContentBackgroundColorWithPartnerConfig(); 143 } 144 145 View view = findManagedViewById(R.id.sud_layout_content); 146 if (view != null) { 147 if (shouldApplyPartnerResource()) { 148 // The margin of content is defined by @style/SudContentFrame. The Setupdesign library 149 // cannot obtain the content resource ID of the client, so the value of the content margin 150 // cannot be adjusted through GlifLayout. If the margin sides are changed through the 151 // partner config, it can only be based on the increased or decreased value to adjust the 152 // value of pading. In this way, the value of content margin plus padding will be equal to 153 // the value of partner config. 154 LayoutStyler.applyPartnerCustomizationExtraPaddingStyle(view); 155 } 156 157 // {@class GlifPreferenceLayout} Inherited from {@class GlifRecyclerLayout}. The API would 158 // be called twice from GlifRecyclerLayout and GlifLayout, so it should skip the API here 159 // when the instance is GlifPreferenceLayout. 160 if (!(this instanceof GlifPreferenceLayout)) { 161 tryApplyPartnerCustomizationContentPaddingTopStyle(view); 162 } 163 } 164 165 updateLandscapeMiddleHorizontalSpacing(); 166 167 ColorStateList backgroundColor = 168 a.getColorStateList(R.styleable.SudGlifLayout_sudBackgroundBaseColor); 169 setBackgroundBaseColor(backgroundColor); 170 171 boolean backgroundPatterned = 172 a.getBoolean(R.styleable.SudGlifLayout_sudBackgroundPatterned, true); 173 setBackgroundPatterned(backgroundPatterned); 174 175 final int stickyHeader = a.getResourceId(R.styleable.SudGlifLayout_sudStickyHeader, 0); 176 if (stickyHeader != 0) { 177 inflateStickyHeader(stickyHeader); 178 } 179 a.recycle(); 180 } 181 182 @Override onFinishInflate()183 protected void onFinishInflate() { 184 super.onFinishInflate(); 185 getMixin(IconMixin.class).tryApplyPartnerCustomizationStyle(); 186 getMixin(HeaderMixin.class).tryApplyPartnerCustomizationStyle(); 187 getMixin(DescriptionMixin.class).tryApplyPartnerCustomizationStyle(); 188 getMixin(ProgressBarMixin.class).tryApplyPartnerCustomizationStyle(); 189 getMixin(ProfileMixin.class).tryApplyPartnerCustomizationStyle(); 190 tryApplyPartnerCustomizationStyleToShortDescription(); 191 } 192 193 // TODO: remove when all sud_layout_description has migrated to 194 // DescriptionMixin(sud_layout_subtitle) tryApplyPartnerCustomizationStyleToShortDescription()195 private void tryApplyPartnerCustomizationStyleToShortDescription() { 196 TextView description = this.findManagedViewById(R.id.sud_layout_description); 197 if (description != null) { 198 if (applyPartnerHeavyThemeResource) { 199 DescriptionStyler.applyPartnerCustomizationHeavyStyle(description); 200 } else if (shouldApplyPartnerResource()) { 201 DescriptionStyler.applyPartnerCustomizationLightStyle(description); 202 } 203 } 204 } 205 updateLandscapeMiddleHorizontalSpacing()206 protected void updateLandscapeMiddleHorizontalSpacing() { 207 int horizontalSpacing = 208 getResources().getDimensionPixelSize(R.dimen.sud_glif_land_middle_horizontal_spacing); 209 if (shouldApplyPartnerResource() 210 && PartnerConfigHelper.get(getContext()) 211 .isPartnerConfigAvailable(PartnerConfig.CONFIG_LAND_MIDDLE_HORIZONTAL_SPACING)) { 212 horizontalSpacing = 213 (int) 214 PartnerConfigHelper.get(getContext()) 215 .getDimension(getContext(), PartnerConfig.CONFIG_LAND_MIDDLE_HORIZONTAL_SPACING); 216 } 217 218 View headerView = this.findManagedViewById(R.id.sud_landscape_header_area); 219 if (headerView != null) { 220 int layoutMarginEnd; 221 if (shouldApplyPartnerResource() 222 && PartnerConfigHelper.get(getContext()) 223 .isPartnerConfigAvailable(PartnerConfig.CONFIG_LAYOUT_MARGIN_END)) { 224 layoutMarginEnd = 225 (int) 226 PartnerConfigHelper.get(getContext()) 227 .getDimension(getContext(), PartnerConfig.CONFIG_LAYOUT_MARGIN_END); 228 } else { 229 TypedArray a = getContext().obtainStyledAttributes(new int[] {R.attr.sudMarginEnd}); 230 layoutMarginEnd = a.getDimensionPixelSize(0, 0); 231 a.recycle(); 232 } 233 int paddingEnd = (horizontalSpacing / 2) - layoutMarginEnd; 234 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 235 headerView.setPadding( 236 headerView.getPaddingStart(), 237 headerView.getPaddingTop(), 238 paddingEnd, 239 headerView.getPaddingBottom()); 240 } else { 241 headerView.setPadding( 242 headerView.getPaddingLeft(), 243 headerView.getPaddingTop(), 244 paddingEnd, 245 headerView.getPaddingBottom()); 246 } 247 } 248 249 View contentView = this.findManagedViewById(R.id.sud_landscape_content_area); 250 if (contentView != null) { 251 int layoutMarginStart; 252 if (shouldApplyPartnerResource() 253 && PartnerConfigHelper.get(getContext()) 254 .isPartnerConfigAvailable(PartnerConfig.CONFIG_LAYOUT_MARGIN_START)) { 255 layoutMarginStart = 256 (int) 257 PartnerConfigHelper.get(getContext()) 258 .getDimension(getContext(), PartnerConfig.CONFIG_LAYOUT_MARGIN_START); 259 } else { 260 TypedArray a = getContext().obtainStyledAttributes(new int[] {R.attr.sudMarginStart}); 261 layoutMarginStart = a.getDimensionPixelSize(0, 0); 262 a.recycle(); 263 } 264 int paddingStart = 0; 265 if (headerView != null) { 266 paddingStart = (horizontalSpacing / 2) - layoutMarginStart; 267 } 268 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 269 contentView.setPadding( 270 paddingStart, 271 contentView.getPaddingTop(), 272 contentView.getPaddingEnd(), 273 contentView.getPaddingBottom()); 274 } else { 275 contentView.setPadding( 276 paddingStart, 277 contentView.getPaddingTop(), 278 contentView.getPaddingRight(), 279 contentView.getPaddingBottom()); 280 } 281 } 282 } 283 284 @Override onInflateTemplate(LayoutInflater inflater, @LayoutRes int template)285 protected View onInflateTemplate(LayoutInflater inflater, @LayoutRes int template) { 286 if (template == 0) { 287 template = R.layout.sud_glif_template; 288 } 289 return inflateTemplate(inflater, R.style.SudThemeGlif_Light, template); 290 } 291 292 @Override findContainer(int containerId)293 protected ViewGroup findContainer(int containerId) { 294 if (containerId == 0) { 295 containerId = R.id.sud_layout_content; 296 } 297 return super.findContainer(containerId); 298 } 299 300 /** 301 * Sets the sticky header (i.e. header that doesn't scroll) of the layout, which is at the top of 302 * the content area outside of the scrolling container. The header can only be inflated once per 303 * instance of this layout. 304 * 305 * @param header The layout to be inflated as the header 306 * @return The root of the inflated header view 307 */ inflateStickyHeader(@ayoutRes int header)308 public View inflateStickyHeader(@LayoutRes int header) { 309 ViewStub stickyHeaderStub = findManagedViewById(R.id.sud_layout_sticky_header); 310 stickyHeaderStub.setLayoutResource(header); 311 return stickyHeaderStub.inflate(); 312 } 313 getScrollView()314 public ScrollView getScrollView() { 315 final View view = findManagedViewById(R.id.sud_scroll_view); 316 return view instanceof ScrollView ? (ScrollView) view : null; 317 } 318 getHeaderTextView()319 public TextView getHeaderTextView() { 320 return getMixin(HeaderMixin.class).getTextView(); 321 } 322 setHeaderText(int title)323 public void setHeaderText(int title) { 324 getMixin(HeaderMixin.class).setText(title); 325 } 326 setHeaderText(CharSequence title)327 public void setHeaderText(CharSequence title) { 328 getMixin(HeaderMixin.class).setText(title); 329 } 330 getHeaderText()331 public CharSequence getHeaderText() { 332 return getMixin(HeaderMixin.class).getText(); 333 } 334 getDescriptionTextView()335 public TextView getDescriptionTextView() { 336 return getMixin(DescriptionMixin.class).getTextView(); 337 } 338 339 /** 340 * Sets the description text and also sets the text visibility to visible. This can also be set 341 * via the XML attribute {@code app:sudDescriptionText}. 342 * 343 * @param title The resource ID of the text to be set as description 344 */ setDescriptionText(@tringRes int title)345 public void setDescriptionText(@StringRes int title) { 346 getMixin(DescriptionMixin.class).setText(title); 347 } 348 349 /** 350 * Sets the description text and also sets the text visibility to visible. This can also be set 351 * via the XML attribute {@code app:sudDescriptionText}. 352 * 353 * @param title The text to be set as description 354 */ setDescriptionText(CharSequence title)355 public void setDescriptionText(CharSequence title) { 356 getMixin(DescriptionMixin.class).setText(title); 357 } 358 359 /** Returns the current description text. */ getDescriptionText()360 public CharSequence getDescriptionText() { 361 return getMixin(DescriptionMixin.class).getText(); 362 } 363 setHeaderColor(ColorStateList color)364 public void setHeaderColor(ColorStateList color) { 365 getMixin(HeaderMixin.class).setTextColor(color); 366 } 367 getHeaderColor()368 public ColorStateList getHeaderColor() { 369 return getMixin(HeaderMixin.class).getTextColor(); 370 } 371 setIcon(Drawable icon)372 public void setIcon(Drawable icon) { 373 getMixin(IconMixin.class).setIcon(icon); 374 } 375 getIcon()376 public Drawable getIcon() { 377 return getMixin(IconMixin.class).getIcon(); 378 } 379 380 /** 381 * Sets the visibility of header area in landscape mode. These views inlcudes icon, header title 382 * and subtitle. It can make the content view become full screen when set false. 383 */ 384 @TargetApi(Build.VERSION_CODES.S) setLandscapeHeaderAreaVisible(boolean visible)385 public void setLandscapeHeaderAreaVisible(boolean visible) { 386 View view = this.findManagedViewById(R.id.sud_landscape_header_area); 387 if (view == null) { 388 return; 389 } 390 if (visible) { 391 view.setVisibility(View.VISIBLE); 392 } else { 393 view.setVisibility(View.GONE); 394 } 395 updateLandscapeMiddleHorizontalSpacing(); 396 } 397 398 /** 399 * Sets the primary color of this layout, which will be used to determine the color of the 400 * progress bar and the background pattern. 401 */ setPrimaryColor(@onNull ColorStateList color)402 public void setPrimaryColor(@NonNull ColorStateList color) { 403 primaryColor = color; 404 updateBackground(); 405 getMixin(ProgressBarMixin.class).setColor(color); 406 } 407 getPrimaryColor()408 public ColorStateList getPrimaryColor() { 409 return primaryColor; 410 } 411 412 /** 413 * Sets the base color of the background view, which is the status bar for phones and the full- 414 * screen background for tablets. If {@link #isBackgroundPatterned()} is true, the pattern will be 415 * drawn with this color. 416 * 417 * @param color The color to use as the base color of the background. If {@code null}, {@link 418 * #getPrimaryColor()} will be used 419 */ setBackgroundBaseColor(@ullable ColorStateList color)420 public void setBackgroundBaseColor(@Nullable ColorStateList color) { 421 backgroundBaseColor = color; 422 updateBackground(); 423 } 424 425 /** 426 * @return The base color of the background. {@code null} indicates the background will be drawn 427 * with {@link #getPrimaryColor()}. 428 */ 429 @Nullable getBackgroundBaseColor()430 public ColorStateList getBackgroundBaseColor() { 431 return backgroundBaseColor; 432 } 433 434 /** 435 * Sets whether the background should be {@link GlifPatternDrawable}. If {@code false}, the 436 * background will be a solid color. 437 */ setBackgroundPatterned(boolean patterned)438 public void setBackgroundPatterned(boolean patterned) { 439 backgroundPatterned = patterned; 440 updateBackground(); 441 } 442 443 /** @return True if this view uses {@link GlifPatternDrawable} as background. */ isBackgroundPatterned()444 public boolean isBackgroundPatterned() { 445 return backgroundPatterned; 446 } 447 updateBackground()448 private void updateBackground() { 449 final View patternBg = findManagedViewById(R.id.suc_layout_status); 450 if (patternBg != null) { 451 int backgroundColor = 0; 452 if (backgroundBaseColor != null) { 453 backgroundColor = backgroundBaseColor.getDefaultColor(); 454 } else if (primaryColor != null) { 455 backgroundColor = primaryColor.getDefaultColor(); 456 } 457 Drawable background = 458 backgroundPatterned 459 ? new GlifPatternDrawable(backgroundColor) 460 : new ColorDrawable(backgroundColor); 461 getMixin(StatusBarMixin.class).setStatusBarBackground(background); 462 } 463 } 464 isProgressBarShown()465 public boolean isProgressBarShown() { 466 return getMixin(ProgressBarMixin.class).isShown(); 467 } 468 setProgressBarShown(boolean shown)469 public void setProgressBarShown(boolean shown) { 470 getMixin(ProgressBarMixin.class).setShown(shown); 471 } 472 peekProgressBar()473 public ProgressBar peekProgressBar() { 474 return getMixin(ProgressBarMixin.class).peekProgressBar(); 475 } 476 477 /** 478 * Returns if the current layout/activity applies heavy partner customized configurations or not. 479 */ shouldApplyPartnerHeavyThemeResource()480 public boolean shouldApplyPartnerHeavyThemeResource() { 481 482 return applyPartnerHeavyThemeResource 483 || (shouldApplyPartnerResource() 484 && PartnerConfigHelper.shouldApplyExtendedPartnerConfig(getContext())); 485 } 486 487 /** Updates the background color of this layout with the partner-customizable background color. */ updateContentBackgroundColorWithPartnerConfig()488 private void updateContentBackgroundColorWithPartnerConfig() { 489 // If full dynamic color enabled which means this activity is running outside of setup 490 // flow, the colors should refer to R.style.SudFullDynamicColorThemeGlifV3. 491 if (useFullDynamicColor()) { 492 return; 493 } 494 495 @ColorInt 496 int color = 497 PartnerConfigHelper.get(getContext()) 498 .getColor(getContext(), PartnerConfig.CONFIG_LAYOUT_BACKGROUND_COLOR); 499 this.getRootView().setBackgroundColor(color); 500 } 501 502 @TargetApi(VERSION_CODES.JELLY_BEAN_MR1) tryApplyPartnerCustomizationContentPaddingTopStyle(View view)503 protected void tryApplyPartnerCustomizationContentPaddingTopStyle(View view) { 504 Context context = view.getContext(); 505 boolean partnerPaddingTopAvailable = 506 PartnerConfigHelper.get(context) 507 .isPartnerConfigAvailable(PartnerConfig.CONFIG_CONTENT_PADDING_TOP); 508 509 if (shouldApplyPartnerResource() && partnerPaddingTopAvailable) { 510 int paddingTop = 511 (int) 512 PartnerConfigHelper.get(context) 513 .getDimension(context, PartnerConfig.CONFIG_CONTENT_PADDING_TOP); 514 515 if (paddingTop != view.getPaddingTop()) { 516 view.setPadding( 517 view.getPaddingStart(), paddingTop, view.getPaddingEnd(), view.getPaddingBottom()); 518 } 519 } 520 } 521 } 522