• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 package com.android.car.setupwizardlib;
17 
18 import android.animation.ValueAnimator;
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.Rect;
23 import android.support.annotation.VisibleForTesting;
24 import android.text.TextUtils;
25 import android.util.AttributeSet;
26 import android.view.LayoutInflater;
27 import android.view.TouchDelegate;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.ViewStub;
31 import android.widget.Button;
32 import android.widget.LinearLayout;
33 import android.widget.ProgressBar;
34 import android.widget.TextView;
35 
36 
37 import java.util.Locale;
38 
39 /**
40  * Custom layout for the Car Setup Wizard. Provides accessors for modifying elements such as buttons
41  * and progress bars. Any modifications to elements built by
42  * the CarSetupWizardLayout should be done through methods provided by this class unless that is
43  * not possible so as to keep the state internally consistent.
44  */
45 public class CarSetupWizardLayout extends LinearLayout {
46     private static final int ANIMATION_DURATION_MS = 100;
47 
48     private View mBackButton;
49     private View mTitleBar;
50     private Float mTitleBarElevation;
51     private TextView mToolbarTitle;
52 
53     /* <p>The Primary Toolbar Button should always be used when there is only a single action that
54      * moves the wizard to the next screen (e.g. Only need a 'Skip' button).
55      *
56      * When there are two actions that can move the wizard to the next screen (e.g. either 'Skip'
57      * or 'Let's Go' are the two options), then the Primary is used for the positive action
58      * while the Secondary is used for the negative action.</p>
59      */
60     private Button mPrimaryToolbarButton;
61 
62     /*
63      * Flag to track the primary toolbar button flat state.
64      */
65     private boolean mPrimaryToolbarButtonFlat;
66     private View.OnClickListener mPrimaryToolbarButtonOnClick;
67     private Button mSecondaryToolbarButton;
68     private ProgressBar mProgressBar;
69 
CarSetupWizardLayout(Context context)70     public CarSetupWizardLayout(Context context) {
71         this(context, null);
72     }
73 
CarSetupWizardLayout(Context context, @Nullable AttributeSet attrs)74     public CarSetupWizardLayout(Context context, @Nullable AttributeSet attrs) {
75         this(context, attrs, 0);
76     }
77 
CarSetupWizardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr)78     public CarSetupWizardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
79         this(context, attrs, defStyleAttr, 0);
80     }
81 
82     /**
83      * On initialization, the layout gets all of the custom attributes and initializes
84      * the custom views that can be set by the user (e.g. back button, continue button).
85      */
CarSetupWizardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)86     public CarSetupWizardLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
87             int defStyleRes) {
88         super(context, attrs, defStyleAttr, defStyleRes);
89 
90         TypedArray attrArray = context.getTheme().obtainStyledAttributes(
91                 attrs,
92                 R.styleable.CarSetupWizardLayout,
93                 0, 0);
94 
95         init(attrArray);
96     }
97 
98     /**
99      * Inflates the layout and sets the custom views (e.g. back button, continue button).
100      */
init(TypedArray attrArray)101     private void init(TypedArray attrArray) {
102         boolean showBackButton;
103 
104         boolean showToolbarTitle;
105         String toolbarTitleText;
106 
107         boolean showPrimaryToolbarButton;
108         String primaryToolbarButtonText;
109         boolean primaryToolbarButtonEnabled;
110 
111         boolean showSecondaryToolbarButton;
112         String secondaryToolbarButtonText;
113         boolean secondaryToolbarButtonEnabled;
114 
115         boolean showProgressBar;
116         boolean indeterminateProgressBar;
117 
118         try {
119             showBackButton = attrArray.getBoolean(
120                     R.styleable.CarSetupWizardLayout_showBackButton, true);
121             showToolbarTitle = attrArray.getBoolean(
122                     R.styleable.CarSetupWizardLayout_showToolbarTitle, false);
123             toolbarTitleText = attrArray.getString(
124                     R.styleable.CarSetupWizardLayout_toolbarTitleText);
125             showPrimaryToolbarButton = attrArray.getBoolean(
126                     R.styleable.CarSetupWizardLayout_showPrimaryToolbarButton, true);
127             primaryToolbarButtonText = attrArray.getString(
128                     R.styleable.CarSetupWizardLayout_primaryToolbarButtonText);
129             primaryToolbarButtonEnabled = attrArray.getBoolean(
130                     R.styleable.CarSetupWizardLayout_primaryToolbarButtonEnabled, true);
131             mPrimaryToolbarButtonFlat = attrArray.getBoolean(
132                     R.styleable.CarSetupWizardLayout_primaryToolbarButtonFlat, false);
133             showSecondaryToolbarButton = attrArray.getBoolean(
134                     R.styleable.CarSetupWizardLayout_showSecondaryToolbarButton, false);
135             secondaryToolbarButtonText = attrArray.getString(
136                     R.styleable.CarSetupWizardLayout_secondaryToolbarButtonText);
137             secondaryToolbarButtonEnabled = attrArray.getBoolean(
138                     R.styleable.CarSetupWizardLayout_secondaryToolbarButtonEnabled, true);
139             showProgressBar = attrArray.getBoolean(
140                     R.styleable.CarSetupWizardLayout_showProgressBar, false);
141             indeterminateProgressBar = attrArray.getBoolean(
142                     R.styleable.CarSetupWizardLayout_indeterminateProgressBar, true);
143         } finally {
144             attrArray.recycle();
145         }
146 
147         LayoutInflater inflater = LayoutInflater.from(getContext());
148         inflater.inflate(R.layout.car_setup_wizard_layout, this);
149 
150         // Set the back button visibility based on the custom attribute.
151         setBackButton(findViewById(R.id.back_button));
152         setBackButtonVisible(showBackButton);
153 
154         // Se the title bar.
155         setTitleBar(findViewById(R.id.application_bar));
156         mTitleBarElevation =
157                 getContext().getResources().getDimension(R.dimen.title_bar_drop_shadow_elevation);
158 
159         // Set the toolbar title visibility and text based on the custom attributes.
160         setToolbarTitle(findViewById(R.id.toolbar_title));
161         if (showToolbarTitle) {
162             setToolbarTitleText(toolbarTitleText);
163         } else {
164             setToolbarTitleVisible(false);
165         }
166 
167         // Set the primary continue button visibility and text based on the custom attributes.
168         ViewStub primaryToolbarButtonStub =
169                 (ViewStub) findViewById(R.id.primary_toolbar_button_stub);
170         // Set the button layout to flat if that attribute was set.
171         if (mPrimaryToolbarButtonFlat) {
172             primaryToolbarButtonStub.setLayoutResource(R.layout.flat_button);
173         }
174         primaryToolbarButtonStub.inflate();
175         setPrimaryToolbarButton(findViewById(R.id.primary_toolbar_button));
176         if (showPrimaryToolbarButton) {
177             setPrimaryToolbarButtonText(primaryToolbarButtonText);
178             setPrimaryToolbarButtonEnabled(primaryToolbarButtonEnabled);
179         } else {
180             setPrimaryToolbarButtonVisible(false);
181         }
182 
183         // Set the secondary continue button visibility and text based on the custom attributes.
184         ViewStub secondaryToolbarButtonStub =
185                 (ViewStub) findViewById(R.id.secondary_toolbar_button_stub);
186         if (showSecondaryToolbarButton || !TextUtils.isEmpty(secondaryToolbarButtonText)) {
187             secondaryToolbarButtonStub.inflate();
188             mSecondaryToolbarButton = findViewById(R.id.secondary_toolbar_button);
189             setSecondaryToolbarButtonText(secondaryToolbarButtonText);
190             setSecondaryToolbarButtonEnabled(secondaryToolbarButtonEnabled);
191             setSecondaryToolbarButtonVisible(showSecondaryToolbarButton);
192         }
193 
194         mProgressBar = findViewById(R.id.progress_bar);
195         setProgressBarVisible(showProgressBar);
196         setProgressBarIndeterminate(indeterminateProgressBar);
197 
198         // Set orientation programmatically since the inflated layout uses <merge>
199         setOrientation(LinearLayout.VERTICAL);
200     }
201 
202     /**
203      * Set a given view's visibility.
204      */
205     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
setViewVisible(View view, boolean visible)206     void setViewVisible(View view, boolean visible) {
207         view.setVisibility(visible ? View.VISIBLE : View.GONE);
208     }
209 
210     // Add or remove the back button touch delegate depending on whether it is visible.
211     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
updateBackButtonTouchDelegate(boolean visible)212     void updateBackButtonTouchDelegate(boolean visible) {
213         if (visible) {
214             // Post this action in the parent's message queue to make sure the parent
215             // lays out its children before getHitRect() is called
216             this.post(() -> {
217                 Rect delegateArea = new Rect();
218 
219                 mBackButton.getHitRect(delegateArea);
220 
221                 /*
222                  * Update the delegate area based on the difference between the current size and
223                  * the touch target size
224                  */
225                 float touchTargetSize = getResources().getDimension(
226                         R.dimen.car_touch_target_size);
227                 float primaryIconSize = getResources().getDimension(
228                         R.dimen.car_primary_icon_size);
229 
230                 int sizeDifference = (int) ((touchTargetSize - primaryIconSize) / 2);
231 
232                 delegateArea.right += sizeDifference;
233                 delegateArea.bottom += sizeDifference;
234                 delegateArea.left -= sizeDifference;
235                 delegateArea.top -= sizeDifference;
236 
237                 // Set the TouchDelegate on the parent view
238                 TouchDelegate touchDelegate = new TouchDelegate(delegateArea,
239                         mBackButton);
240 
241                 if (View.class.isInstance(mBackButton.getParent())) {
242                     ((View) mBackButton.getParent()).setTouchDelegate(touchDelegate);
243                 }
244             });
245         } else {
246             // Set the TouchDelegate to null if the back button is not visible.
247             if (View.class.isInstance(mBackButton.getParent())) {
248                 ((View) mBackButton.getParent()).setTouchDelegate(null);
249             }
250         }
251     }
252 
253     /**
254      * Gets the back button.
255      */
getBackButton()256     public View getBackButton() {
257         return mBackButton;
258     }
259 
260     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
setBackButton(View backButton)261     final void setBackButton(View backButton) {
262         mBackButton = backButton;
263     }
264 
265     /**
266      * Set the back button onClickListener to given listener. Can be null if the listener should
267      * be overridden so no callback is made.
268      */
setBackButtonListener(@ullable View.OnClickListener listener)269     public void setBackButtonListener(@Nullable View.OnClickListener listener) {
270         mBackButton.setOnClickListener(listener);
271     }
272 
273     /**
274      * Set the back button visibility to the given visibility.
275      */
setBackButtonVisible(boolean visible)276     public void setBackButtonVisible(boolean visible) {
277         setViewVisible(mBackButton, visible);
278         updateBackButtonTouchDelegate(visible);
279     }
280 
281     /**
282      * Sets the title bar view.
283      */
setTitleBar(View titleBar)284     private void setTitleBar(View titleBar) {
285         mTitleBar = titleBar;
286     }
287 
288     /**
289      * Gets the toolbar title.
290      */
getToolbarTitle()291     public TextView getToolbarTitle() {
292         return mToolbarTitle;
293     }
294 
295     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
setToolbarTitle(TextView toolbarTitle)296     final void setToolbarTitle(TextView toolbarTitle) {
297         mToolbarTitle = toolbarTitle;
298     }
299 
300     /**
301      * Sets the header title visibility to given value.
302      */
setToolbarTitleVisible(boolean visible)303     public void setToolbarTitleVisible(boolean visible) {
304         setViewVisible(mToolbarTitle, visible);
305     }
306 
307     /**
308      * Sets the header title text to the provided text.
309      */
setToolbarTitleText(String text)310     public void setToolbarTitleText(String text) {
311         mToolbarTitle.setText(text);
312     }
313 
314     /**
315      * Gets the primary toolbar button.
316      */
getPrimaryToolbarButton()317     public Button getPrimaryToolbarButton() {
318         return mPrimaryToolbarButton;
319     }
320 
321     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
setPrimaryToolbarButton(Button primaryToolbarButton)322     final void setPrimaryToolbarButton(Button primaryToolbarButton) {
323         mPrimaryToolbarButton = primaryToolbarButton;
324     }
325 
326     /**
327      * Set the primary continue button visibility to the given visibility.
328      */
setPrimaryToolbarButtonVisible(boolean visible)329     public void setPrimaryToolbarButtonVisible(boolean visible) {
330         setViewVisible(mPrimaryToolbarButton, visible);
331     }
332 
333     /**
334      * Set whether the primary continue button is enabled.
335      */
setPrimaryToolbarButtonEnabled(boolean enabled)336     public void setPrimaryToolbarButtonEnabled(boolean enabled) {
337         mPrimaryToolbarButton.setEnabled(enabled);
338     }
339 
340     /**
341      * Set the primary continue button text to the given text.
342      */
setPrimaryToolbarButtonText(String text)343     public void setPrimaryToolbarButtonText(String text) {
344         mPrimaryToolbarButton.setText(text);
345     }
346 
347     /**
348      * Set the primary continue button onClickListener to the given listener. Can be null if the
349      * listener should be overridden so no callback is made. All changes to primary toolbar
350      * button's onClickListener should be made here so they can be stored through changes to the
351      * button.
352      */
setPrimaryToolbarButtonListener(@ullable View.OnClickListener listener)353     public void setPrimaryToolbarButtonListener(@Nullable View.OnClickListener listener) {
354         mPrimaryToolbarButtonOnClick = listener;
355         mPrimaryToolbarButton.setOnClickListener(listener);
356     }
357 
358     /**
359      * Getter for the flatness of the primary toolbar button.
360      */
getPrimaryToolbarButtonFlat()361     public boolean getPrimaryToolbarButtonFlat() {
362         return mPrimaryToolbarButtonFlat;
363     }
364 
365     /**
366      * Changes the button in the primary slot to a flat theme, maintaining the text, visibility,
367      * whether it is enabled, and id.
368      * <p>NOTE: that other attributes set manually on the primaryToolbarButton will be lost on calls
369      * to this method as the button will be replaced.</p>
370      */
setPrimaryToolbarButtonFlat(boolean isFlat)371     public void setPrimaryToolbarButtonFlat(boolean isFlat) {
372         // Do nothing if the state isn't changing.
373         if (isFlat == mPrimaryToolbarButtonFlat) {
374             return;
375         }
376         Button newPrimaryButton = createPrimaryToolbarButton(isFlat);
377 
378         ViewGroup parent = (ViewGroup) findViewById(R.id.button_container);
379         int buttonIndex = parent.indexOfChild(mPrimaryToolbarButton);
380         parent.removeViewAt(buttonIndex);
381         parent.addView(newPrimaryButton, buttonIndex);
382 
383         // Update state of layout
384         setPrimaryToolbarButton(newPrimaryButton);
385         mPrimaryToolbarButtonFlat = isFlat;
386     }
387 
388     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
createPrimaryToolbarButton(boolean isFlat)389     Button createPrimaryToolbarButton(boolean isFlat) {
390         int layoutId = isFlat ? R.layout.flat_button : R.layout.primary_button;
391         Button newPrimaryButton = (Button) inflate(getContext(), layoutId, null);
392         newPrimaryButton.setId(mPrimaryToolbarButton.getId());
393         newPrimaryButton.setVisibility(mPrimaryToolbarButton.getVisibility());
394         newPrimaryButton.setEnabled(mPrimaryToolbarButton.isEnabled());
395         newPrimaryButton.setText(mPrimaryToolbarButton.getText());
396         newPrimaryButton.setOnClickListener(mPrimaryToolbarButtonOnClick);
397         newPrimaryButton.setLayoutParams(mPrimaryToolbarButton.getLayoutParams());
398 
399         return newPrimaryButton;
400     }
401 
402     /**
403      * Gets the secondary toolbar button.
404      */
getSecondaryToolbarButton()405     public Button getSecondaryToolbarButton() {
406         return mSecondaryToolbarButton;
407     }
408 
409     /**
410      * Set the secondary continue button visibility to the given visibility.
411      */
setSecondaryToolbarButtonVisible(boolean visible)412     public void setSecondaryToolbarButtonVisible(boolean visible) {
413         // If not setting it visible and it hasn't been inflated yet then don't inflate.
414         if (!visible && mSecondaryToolbarButton == null) {
415             return;
416         }
417         maybeInflateSecondaryToolbarButton();
418         setViewVisible(mSecondaryToolbarButton, visible);
419     }
420 
421     /**
422      * Sets whether the secondary continue button is enabled.
423      */
setSecondaryToolbarButtonEnabled(boolean enabled)424     public void setSecondaryToolbarButtonEnabled(boolean enabled) {
425         maybeInflateSecondaryToolbarButton();
426         mSecondaryToolbarButton.setEnabled(enabled);
427     }
428 
429     /**
430      * Sets the secondary continue button text to the given text.
431      */
setSecondaryToolbarButtonText(String text)432     public void setSecondaryToolbarButtonText(String text) {
433         maybeInflateSecondaryToolbarButton();
434         mSecondaryToolbarButton.setText(text);
435     }
436 
437     /**
438      * Sets the secondary continue button onClickListener to the given listener. Can be null if the
439      * listener should be overridden so no callback is made.
440      */
setSecondaryToolbarButtonListener(@ullable View.OnClickListener listener)441     public void setSecondaryToolbarButtonListener(@Nullable View.OnClickListener listener) {
442         maybeInflateSecondaryToolbarButton();
443         mSecondaryToolbarButton.setOnClickListener(listener);
444     }
445 
446     /**
447      * A method that will inflate the SecondaryToolbarButton if it is has not already been
448      * inflated. If it has been inflated already this method will do nothing.
449      */
maybeInflateSecondaryToolbarButton()450     private void maybeInflateSecondaryToolbarButton() {
451         ViewStub secondaryToolbarButtonStub = findViewById(R.id.secondary_toolbar_button_stub);
452         // If the secondaryToolbarButtonStub is null then the stub has been inflated so there is
453         // nothing to do.
454         if (secondaryToolbarButtonStub != null) {
455             secondaryToolbarButtonStub.inflate();
456             mSecondaryToolbarButton = findViewById(R.id.secondary_toolbar_button);
457             setSecondaryToolbarButtonVisible(false);
458         }
459 
460     }
461 
462     /**
463      * Gets the progress bar.
464      */
getProgressBar()465     public ProgressBar getProgressBar() {
466         return mProgressBar;
467     }
468 
469     /**
470      * Sets the progress bar visibility to the given visibility.
471      */
setProgressBarVisible(boolean visible)472     public void setProgressBarVisible(boolean visible) {
473         setViewVisible(mProgressBar, visible);
474     }
475 
476     /**
477      * Sets the progress bar indeterminate/determinate state.
478      */
setProgressBarIndeterminate(boolean indeterminate)479     public void setProgressBarIndeterminate(boolean indeterminate) {
480         mProgressBar.setIndeterminate(indeterminate);
481     }
482 
483     /**
484      * Sets the progress bar's progress.
485      */
setProgressBarProgress(int progress)486     public void setProgressBarProgress(int progress) {
487         setProgressBarIndeterminate(false);
488         mProgressBar.setProgress(progress);
489     }
490 
491     /**
492      * Sets the locale to be used for rendering.
493      */
applyLocale(Locale locale)494     public void applyLocale(Locale locale) {
495         if (locale == null) {
496             return;
497         }
498         int direction = TextUtils.getLayoutDirectionFromLocale(locale);
499         setLayoutDirection(direction);
500 
501         mToolbarTitle.setTextLocale(locale);
502         mToolbarTitle.setLayoutDirection(direction);
503 
504         mPrimaryToolbarButton.setTextLocale(locale);
505         mPrimaryToolbarButton.setLayoutDirection(direction);
506 
507         mSecondaryToolbarButton.setTextLocale(locale);
508         mSecondaryToolbarButton.setLayoutDirection(direction);
509     }
510 
511     /**
512      * Adds elevation to the title bar in order to produce a drop shadow. An animation can be used
513      * in cases where a direct elevation changes would be too jarring.
514      *
515      * @param animate True when a smooth animation is wanted for the adding of the elevation.
516      */
addElevationToTitleBar(boolean animate)517     public void addElevationToTitleBar(boolean animate) {
518         if (animate) {
519             ValueAnimator elevationAnimator =
520                     ValueAnimator.ofFloat(mTitleBar.getElevation(), mTitleBarElevation);
521             elevationAnimator
522                     .setDuration(ANIMATION_DURATION_MS)
523                     .addUpdateListener(
524                             animation -> mTitleBar.setElevation(
525                                     (float) animation.getAnimatedValue()));
526             elevationAnimator.start();
527         } else {
528             mTitleBar.setElevation(mTitleBarElevation);
529         }
530     }
531 
532     /**
533      * Removes the elevation from the title bar, an animation can be used in cases where a direct
534      * elevation changes would be too jarring.
535      *
536      * @param animate True when a smooth animation is wanted for the removal of the elevation.
537      */
removeElevationFromTitleBar(boolean animate)538     public void removeElevationFromTitleBar(boolean animate) {
539         if (animate) {
540             ValueAnimator elevationAnimator =
541                     ValueAnimator.ofFloat(mTitleBar.getElevation(), 0f);
542             elevationAnimator
543                     .setDuration(ANIMATION_DURATION_MS)
544                     .addUpdateListener(
545                             animation -> mTitleBar.setElevation(
546                                     (float) animation.getAnimatedValue()));
547             elevationAnimator.start();
548         } else {
549             mTitleBar.setElevation(0f);
550         }
551     }
552 }
553