Home | History | Annotate | Download | only in setupwizardlib
      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 
     70     public CarSetupWizardLayout(Context context) {
     71         this(context, null);
     72     }
     73 
     74     public CarSetupWizardLayout(Context context, @Nullable AttributeSet attrs) {
     75         this(context, attrs, 0);
     76     }
     77 
     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      */
     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      */
    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)
    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)
    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      */
    256     public View getBackButton() {
    257         return mBackButton;
    258     }
    259 
    260     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    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      */
    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      */
    276     public void setBackButtonVisible(boolean visible) {
    277         setViewVisible(mBackButton, visible);
    278         updateBackButtonTouchDelegate(visible);
    279     }
    280 
    281     /**
    282      * Sets the title bar view.
    283      */
    284     private void setTitleBar(View titleBar) {
    285         mTitleBar = titleBar;
    286     }
    287 
    288     /**
    289      * Gets the toolbar title.
    290      */
    291     public TextView getToolbarTitle() {
    292         return mToolbarTitle;
    293     }
    294 
    295     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    296     final void setToolbarTitle(TextView toolbarTitle) {
    297         mToolbarTitle = toolbarTitle;
    298     }
    299 
    300     /**
    301      * Sets the header title visibility to given value.
    302      */
    303     public void setToolbarTitleVisible(boolean visible) {
    304         setViewVisible(mToolbarTitle, visible);
    305     }
    306 
    307     /**
    308      * Sets the header title text to the provided text.
    309      */
    310     public void setToolbarTitleText(String text) {
    311         mToolbarTitle.setText(text);
    312     }
    313 
    314     /**
    315      * Gets the primary toolbar button.
    316      */
    317     public Button getPrimaryToolbarButton() {
    318         return mPrimaryToolbarButton;
    319     }
    320 
    321     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    322     final void setPrimaryToolbarButton(Button primaryToolbarButton) {
    323         mPrimaryToolbarButton = primaryToolbarButton;
    324     }
    325 
    326     /**
    327      * Set the primary continue button visibility to the given visibility.
    328      */
    329     public void setPrimaryToolbarButtonVisible(boolean visible) {
    330         setViewVisible(mPrimaryToolbarButton, visible);
    331     }
    332 
    333     /**
    334      * Set whether the primary continue button is enabled.
    335      */
    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      */
    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      */
    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      */
    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      */
    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)
    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      */
    405     public Button getSecondaryToolbarButton() {
    406         return mSecondaryToolbarButton;
    407     }
    408 
    409     /**
    410      * Set the secondary continue button visibility to the given visibility.
    411      */
    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      */
    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      */
    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      */
    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      */
    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      */
    465     public ProgressBar getProgressBar() {
    466         return mProgressBar;
    467     }
    468 
    469     /**
    470      * Sets the progress bar visibility to the given visibility.
    471      */
    472     public void setProgressBarVisible(boolean visible) {
    473         setViewVisible(mProgressBar, visible);
    474     }
    475 
    476     /**
    477      * Sets the progress bar indeterminate/determinate state.
    478      */
    479     public void setProgressBarIndeterminate(boolean indeterminate) {
    480         mProgressBar.setIndeterminate(indeterminate);
    481     }
    482 
    483     /**
    484      * Sets the progress bar's progress.
    485      */
    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      */
    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      */
    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      */
    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