• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  * &lt;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  *     &lt;!-- Content here -->
71  *
72  * &lt;/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