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