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