TextListItem.java revision 52a80988da3d27f591abd43faa27eae120996ec2
1/* 2 * Copyright 2018 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 17package androidx.car.widget; 18 19import static java.lang.annotation.RetentionPolicy.SOURCE; 20 21import android.car.drivingstate.CarUxRestrictions; 22import android.content.Context; 23import android.content.res.Resources; 24import android.graphics.drawable.Drawable; 25import android.text.TextUtils; 26import android.view.View; 27import android.view.ViewGroup; 28import android.widget.Button; 29import android.widget.CompoundButton; 30import android.widget.ImageView; 31import android.widget.RelativeLayout; 32import android.widget.Switch; 33import android.widget.TextView; 34 35import androidx.annotation.DrawableRes; 36import androidx.annotation.IdRes; 37import androidx.annotation.IntDef; 38import androidx.annotation.StyleRes; 39import androidx.car.R; 40import androidx.car.utils.CarUxRestrictionsUtils; 41import androidx.recyclerview.widget.RecyclerView; 42 43import java.lang.annotation.Retention; 44import java.util.ArrayList; 45import java.util.List; 46 47/** 48 * Class to build a list item of text. 49 * 50 * <p>An item supports primary action and supplemental action(s). 51 * 52 * <p>An item visually composes of 3 parts; each part may contain multiple views. 53 * <ul> 54 * <li>{@code Primary Action}: represented by an icon of following types. 55 * <ul> 56 * <li>Primary Icon - icon size could be large or small. 57 * <li>No Icon - no icon is shown. 58 * <li>Empty Icon - {@code Text} offsets start space as if there was an icon. 59 * </ul> 60 * <li>{@code Text}: supports any combination of the following text views. 61 * <ul> 62 * <li>Title 63 * <li>Body 64 * </ul> 65 * <li>{@code Supplemental Action}: represented by one of the following types; aligned toward 66 * the end of item. 67 * <ul> 68 * <li>Supplemental Icon 69 * <li>One Action Button 70 * <li>Two Action Buttons 71 * <li>Switch 72 * </ul> 73 * </ul> 74 * 75 * <p>{@code TextListItem} binds data to {@link ViewHolder} based on components selected. 76 * 77 * <p>When conflicting setter methods are called (e.g. setting primary action to both primary icon 78 * and no icon), the last called method wins. 79 */ 80public class TextListItem extends ListItem<TextListItem.ViewHolder> { 81 82 @Retention(SOURCE) 83 @IntDef({ 84 PRIMARY_ACTION_TYPE_NO_ICON, PRIMARY_ACTION_TYPE_EMPTY_ICON, 85 PRIMARY_ACTION_TYPE_LARGE_ICON, PRIMARY_ACTION_TYPE_SMALL_ICON}) 86 private @interface PrimaryActionType {} 87 88 private static final int PRIMARY_ACTION_TYPE_NO_ICON = 0; 89 private static final int PRIMARY_ACTION_TYPE_EMPTY_ICON = 1; 90 private static final int PRIMARY_ACTION_TYPE_LARGE_ICON = 2; 91 private static final int PRIMARY_ACTION_TYPE_SMALL_ICON = 3; 92 93 @Retention(SOURCE) 94 @IntDef({SUPPLEMENTAL_ACTION_NO_ACTION, SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON, 95 SUPPLEMENTAL_ACTION_ONE_ACTION, SUPPLEMENTAL_ACTION_TWO_ACTIONS, 96 SUPPLEMENTAL_ACTION_SWITCH}) 97 private @interface SupplementalActionType {} 98 99 private static final int SUPPLEMENTAL_ACTION_NO_ACTION = 0; 100 private static final int SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON = 1; 101 private static final int SUPPLEMENTAL_ACTION_ONE_ACTION = 2; 102 private static final int SUPPLEMENTAL_ACTION_TWO_ACTIONS = 3; 103 private static final int SUPPLEMENTAL_ACTION_SWITCH = 4; 104 105 private final Context mContext; 106 107 private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>(); 108 109 private View.OnClickListener mOnClickListener; 110 111 @PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON; 112 private Drawable mPrimaryActionIconDrawable; 113 114 private String mTitle; 115 private String mBody; 116 private boolean mIsBodyPrimary; 117 118 @SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION; 119 private Drawable mSupplementalIconDrawable; 120 private View.OnClickListener mSupplementalIconOnClickListener; 121 private boolean mShowSupplementalIconDivider; 122 123 private boolean mSwitchChecked; 124 private boolean mShowSwitchDivider; 125 private CompoundButton.OnCheckedChangeListener mSwitchOnCheckedChangeListener; 126 127 private String mAction1Text; 128 private View.OnClickListener mAction1OnClickListener; 129 private boolean mShowAction1Divider; 130 private String mAction2Text; 131 private View.OnClickListener mAction2OnClickListener; 132 private boolean mShowAction2Divider; 133 134 /** 135 * Creates a {@link TextListItem.ViewHolder}. 136 */ 137 public static ViewHolder createViewHolder(View itemView) { 138 return new ViewHolder(itemView); 139 } 140 141 public TextListItem(Context context) { 142 mContext = context; 143 markDirty(); 144 } 145 146 /** 147 * Used by {@link ListItemAdapter} to choose layout to inflate for view holder. 148 */ 149 @Override 150 public int getViewType() { 151 return ListItemAdapter.LIST_ITEM_TYPE_TEXT; 152 } 153 154 /** 155 * Calculates the layout params for views in {@link ViewHolder}. 156 */ 157 @Override 158 protected void resolveDirtyState() { 159 mBinders.clear(); 160 161 // Create binders that adjust layout params of each view. 162 setItemLayoutHeight(); 163 setPrimaryAction(); 164 setText(); 165 setSupplementalActions(); 166 setOnClickListener(); 167 } 168 169 /** 170 * Hides all views in {@link ViewHolder} then applies ViewBinders to adjust view layout params. 171 */ 172 @Override 173 public void onBind(ViewHolder viewHolder) { 174 hideSubViews(viewHolder); 175 for (ViewBinder binder : mBinders) { 176 binder.bind(viewHolder); 177 } 178 } 179 180 /** Sets the title text appearance from the specified style resource. */ 181 @Override 182 void setTitleTextAppearance(@StyleRes int titleTextAppearance) { 183 super.setTitleTextAppearance(titleTextAppearance); 184 setTextContent(); 185 } 186 187 /** Sets the body text appearance from the specified style resource. */ 188 @Override 189 void setBodyTextAppearance(@StyleRes int bodyTextAppearance) { 190 super.setBodyTextAppearance(bodyTextAppearance); 191 setTextContent(); 192 } 193 194 private void hideSubViews(ViewHolder vh) { 195 View[] subviews = new View[] { 196 vh.getPrimaryIcon(), 197 vh.getTitle(), vh.getBody(), 198 vh.getSupplementalIcon(), vh.getSupplementalIconDivider(), 199 vh.getSwitch(), vh.getSwitchDivider(), 200 vh.getAction1(), vh.getAction1Divider(), vh.getAction2(), vh.getAction2Divider()}; 201 for (View v : subviews) { 202 v.setVisibility(View.GONE); 203 } 204 } 205 206 /** 207 * Sets the height of item depending on which text field is set. 208 */ 209 private void setItemLayoutHeight() { 210 if (TextUtils.isEmpty(mBody)) { 211 // If the item only has title or no text, it uses fixed-height as single line. 212 int height = mContext.getResources().getDimensionPixelSize( 213 R.dimen.car_single_line_list_item_height); 214 mBinders.add(vh -> { 215 ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams(); 216 layoutParams.height = height; 217 vh.itemView.requestLayout(); 218 }); 219 } else { 220 // If body is present, the item should be at least as tall as min height, and wraps 221 // content. 222 int minHeight = mContext.getResources().getDimensionPixelSize( 223 R.dimen.car_double_line_list_item_height); 224 mBinders.add(vh -> { 225 vh.itemView.setMinimumHeight(minHeight); 226 vh.getContainerLayout().setMinimumHeight(minHeight); 227 228 ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams(); 229 layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT; 230 vh.itemView.requestLayout(); 231 }); 232 } 233 } 234 235 private void setPrimaryAction() { 236 setPrimaryIconContent(); 237 setPrimaryIconLayout(); 238 } 239 240 private void setText() { 241 setTextContent(); 242 setTextVerticalMargin(); 243 // Only set start margin because text end is relative to the start of supplemental actions. 244 setTextStartMargin(); 245 setTextEndLayout(); 246 } 247 248 private void setOnClickListener() { 249 mBinders.add(vh -> vh.itemView.setOnClickListener(mOnClickListener)); 250 } 251 252 private void setPrimaryIconContent() { 253 switch (mPrimaryActionType) { 254 case PRIMARY_ACTION_TYPE_SMALL_ICON: 255 case PRIMARY_ACTION_TYPE_LARGE_ICON: 256 mBinders.add(vh -> { 257 vh.getPrimaryIcon().setVisibility(View.VISIBLE); 258 259 vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable); 260 }); 261 break; 262 case PRIMARY_ACTION_TYPE_EMPTY_ICON: 263 case PRIMARY_ACTION_TYPE_NO_ICON: 264 // Do nothing. 265 break; 266 default: 267 throw new IllegalStateException("Unknown primary action type."); 268 } 269 } 270 271 /** 272 * Sets layout params of primary icon. 273 * 274 * <p>Large icon will have no start margin, and always align center vertically. 275 * 276 * <p>Small icon will have start margin. When body text is present small icon uses a top 277 * margin otherwise align center vertically. 278 */ 279 private void setPrimaryIconLayout() { 280 // Set all relevant fields in layout params to avoid carried over params when the item 281 // gets bound to a recycled view holder. 282 switch (mPrimaryActionType) { 283 case PRIMARY_ACTION_TYPE_SMALL_ICON: 284 mBinders.add(vh -> { 285 int iconSize = mContext.getResources().getDimensionPixelSize( 286 R.dimen.car_primary_icon_size); 287 // Icon size. 288 RelativeLayout.LayoutParams layoutParams = 289 (RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams(); 290 layoutParams.height = layoutParams.width = iconSize; 291 292 // Start margin. 293 layoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize( 294 R.dimen.car_keyline_1)); 295 296 if (!TextUtils.isEmpty(mBody)) { 297 // Set icon top margin so that the icon remains in the same position it 298 // would've been in for non-long-text item, namely so that the center 299 // line of icon matches that of line item. 300 layoutParams.removeRule(RelativeLayout.CENTER_VERTICAL); 301 int itemHeight = mContext.getResources().getDimensionPixelSize( 302 R.dimen.car_double_line_list_item_height); 303 layoutParams.topMargin = (itemHeight - iconSize) / 2; 304 } else { 305 // If the icon can be centered vertically, leave the work for framework. 306 layoutParams.addRule(RelativeLayout.CENTER_VERTICAL); 307 layoutParams.topMargin = 0; 308 } 309 vh.getPrimaryIcon().requestLayout(); 310 }); 311 break; 312 case PRIMARY_ACTION_TYPE_LARGE_ICON: 313 mBinders.add(vh -> { 314 int iconSize = mContext.getResources().getDimensionPixelSize( 315 R.dimen.car_single_line_list_item_height); 316 // Icon size. 317 RelativeLayout.LayoutParams layoutParams = 318 (RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams(); 319 layoutParams.height = layoutParams.width = iconSize; 320 321 // No start margin. 322 layoutParams.setMarginStart(0); 323 324 // Always centered vertically. 325 layoutParams.addRule(RelativeLayout.CENTER_VERTICAL); 326 layoutParams.topMargin = 0; 327 328 vh.getPrimaryIcon().requestLayout(); 329 }); 330 break; 331 case PRIMARY_ACTION_TYPE_EMPTY_ICON: 332 case PRIMARY_ACTION_TYPE_NO_ICON: 333 // Do nothing. 334 break; 335 default: 336 throw new IllegalStateException("Unknown primary action type."); 337 } 338 } 339 340 private void setTextContent() { 341 if (!TextUtils.isEmpty(mTitle)) { 342 mBinders.add(vh -> { 343 vh.getTitle().setVisibility(View.VISIBLE); 344 vh.getTitle().setText(mTitle); 345 }); 346 } 347 if (!TextUtils.isEmpty(mBody)) { 348 mBinders.add(vh -> { 349 vh.getBody().setVisibility(View.VISIBLE); 350 vh.getBody().setText(mBody); 351 }); 352 } 353 354 if (mIsBodyPrimary) { 355 mBinders.add(vh -> { 356 vh.getTitle().setTextAppearance(getBodyTextAppearance()); 357 vh.getBody().setTextAppearance(getTitleTextAppearance()); 358 }); 359 } else { 360 mBinders.add(vh -> { 361 vh.getTitle().setTextAppearance(getTitleTextAppearance()); 362 vh.getBody().setTextAppearance(getBodyTextAppearance()); 363 }); 364 } 365 } 366 367 /** 368 * Sets start margin of text view depending on icon type. 369 */ 370 private void setTextStartMargin() { 371 final int startMarginResId; 372 switch (mPrimaryActionType) { 373 case PRIMARY_ACTION_TYPE_NO_ICON: 374 startMarginResId = R.dimen.car_keyline_1; 375 break; 376 case PRIMARY_ACTION_TYPE_EMPTY_ICON: 377 startMarginResId = R.dimen.car_keyline_3; 378 break; 379 case PRIMARY_ACTION_TYPE_SMALL_ICON: 380 startMarginResId = R.dimen.car_keyline_3; 381 break; 382 case PRIMARY_ACTION_TYPE_LARGE_ICON: 383 startMarginResId = R.dimen.car_keyline_4; 384 break; 385 default: 386 throw new IllegalStateException("Unknown primary action type."); 387 } 388 int startMargin = mContext.getResources().getDimensionPixelSize(startMarginResId); 389 mBinders.add(vh -> { 390 RelativeLayout.LayoutParams titleLayoutParams = 391 (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams(); 392 titleLayoutParams.setMarginStart(startMargin); 393 vh.getTitle().requestLayout(); 394 395 RelativeLayout.LayoutParams bodyLayoutParams = 396 (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams(); 397 bodyLayoutParams.setMarginStart(startMargin); 398 vh.getBody().requestLayout(); 399 }); 400 } 401 402 /** 403 * Sets top/bottom margins of {@code Title} and {@code Body}. 404 */ 405 private void setTextVerticalMargin() { 406 // Set all relevant fields in layout params to avoid carried over params when the item 407 // gets bound to a recycled view holder. 408 if (!TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mBody)) { 409 // Title only - view is aligned center vertically by itself. 410 mBinders.add(vh -> { 411 RelativeLayout.LayoutParams layoutParams = 412 (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams(); 413 layoutParams.addRule(RelativeLayout.CENTER_VERTICAL); 414 layoutParams.topMargin = 0; 415 vh.getTitle().requestLayout(); 416 }); 417 } else if (TextUtils.isEmpty(mTitle) && !TextUtils.isEmpty(mBody)) { 418 mBinders.add(vh -> { 419 // Body uses top and bottom margin. 420 int margin = mContext.getResources().getDimensionPixelSize( 421 R.dimen.car_padding_3); 422 RelativeLayout.LayoutParams layoutParams = 423 (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams(); 424 layoutParams.addRule(RelativeLayout.CENTER_VERTICAL); 425 layoutParams.removeRule(RelativeLayout.BELOW); 426 layoutParams.topMargin = margin; 427 layoutParams.bottomMargin = margin; 428 vh.getBody().requestLayout(); 429 }); 430 } else { 431 mBinders.add(vh -> { 432 // Title has a top margin 433 Resources resources = mContext.getResources(); 434 int padding1 = resources.getDimensionPixelSize(R.dimen.car_padding_1); 435 int padding3 = resources.getDimensionPixelSize(R.dimen.car_padding_3); 436 437 RelativeLayout.LayoutParams titleLayoutParams = 438 (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams(); 439 titleLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL); 440 titleLayoutParams.topMargin = padding3; 441 vh.getTitle().requestLayout(); 442 // Body is below title with a margin, and has bottom margin. 443 RelativeLayout.LayoutParams bodyLayoutParams = 444 (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams(); 445 bodyLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL); 446 bodyLayoutParams.addRule(RelativeLayout.BELOW, R.id.title); 447 bodyLayoutParams.topMargin = padding1; 448 bodyLayoutParams.bottomMargin = padding3; 449 vh.getBody().requestLayout(); 450 }); 451 } 452 } 453 454 /** 455 * Returns the id of the leading (left most in LTR) view of supplemental actions. 456 * The view could be one of the supplemental actions (icon, button, switch), or their divider. 457 * Returns 0 if none is enabled. 458 */ 459 @IdRes 460 private int getSupplementalActionLeadingView() { 461 int leadingViewId; 462 switch (mSupplementalActionType) { 463 case SUPPLEMENTAL_ACTION_NO_ACTION: 464 leadingViewId = 0; 465 break; 466 case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON: 467 leadingViewId = mShowSupplementalIconDivider 468 ? R.id.supplemental_icon_divider : R.id.supplemental_icon; 469 break; 470 case SUPPLEMENTAL_ACTION_ONE_ACTION: 471 leadingViewId = mShowAction1Divider ? R.id.action1_divider : R.id.action1; 472 break; 473 case SUPPLEMENTAL_ACTION_TWO_ACTIONS: 474 leadingViewId = mShowAction2Divider ? R.id.action2_divider : R.id.action2; 475 break; 476 case SUPPLEMENTAL_ACTION_SWITCH: 477 leadingViewId = mShowSwitchDivider ? R.id.switch_divider : R.id.switch_widget; 478 break; 479 default: 480 throw new IllegalStateException("Unknown supplemental action type."); 481 } 482 return leadingViewId; 483 } 484 485 private void setTextEndLayout() { 486 // Figure out which view the text should align to. 487 @IdRes int leadingViewId = getSupplementalActionLeadingView(); 488 489 if (leadingViewId == 0) { 490 // There is no supplemental action. Text should align to parent end with KL1 padding. 491 mBinders.add(vh -> { 492 Resources resources = mContext.getResources(); 493 int padding = resources.getDimensionPixelSize(R.dimen.car_keyline_1); 494 495 RelativeLayout.LayoutParams titleLayoutParams = 496 (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams(); 497 titleLayoutParams.setMarginEnd(padding); 498 titleLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_END); 499 titleLayoutParams.removeRule(RelativeLayout.START_OF); 500 501 RelativeLayout.LayoutParams bodyLayoutParams = 502 (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams(); 503 bodyLayoutParams.setMarginEnd(padding); 504 bodyLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_END); 505 bodyLayoutParams.removeRule(RelativeLayout.START_OF); 506 }); 507 } else { 508 // Text align to start of leading supplemental view with padding. 509 mBinders.add(vh -> { 510 Resources resources = mContext.getResources(); 511 int padding = resources.getDimensionPixelSize(R.dimen.car_padding_4); 512 513 RelativeLayout.LayoutParams titleLayoutParams = 514 (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams(); 515 titleLayoutParams.setMarginEnd(padding); 516 titleLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_END); 517 titleLayoutParams.addRule(RelativeLayout.START_OF, leadingViewId); 518 vh.getTitle().requestLayout(); 519 520 RelativeLayout.LayoutParams bodyLayoutParams = 521 (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams(); 522 bodyLayoutParams.setMarginEnd(padding); 523 bodyLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_END); 524 bodyLayoutParams.addRule(RelativeLayout.START_OF, leadingViewId); 525 vh.getBody().requestLayout(); 526 }); 527 } 528 } 529 530 /** 531 * Sets up view(s) for supplemental action. 532 */ 533 private void setSupplementalActions() { 534 switch (mSupplementalActionType) { 535 case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON: 536 mBinders.add(vh -> { 537 vh.getSupplementalIcon().setVisibility(View.VISIBLE); 538 if (mShowSupplementalIconDivider) { 539 vh.getSupplementalIconDivider().setVisibility(View.VISIBLE); 540 } 541 542 vh.getSupplementalIcon().setImageDrawable(mSupplementalIconDrawable); 543 vh.getSupplementalIcon().setOnClickListener( 544 mSupplementalIconOnClickListener); 545 vh.getSupplementalIcon().setClickable( 546 mSupplementalIconOnClickListener != null); 547 }); 548 break; 549 case SUPPLEMENTAL_ACTION_TWO_ACTIONS: 550 mBinders.add(vh -> { 551 vh.getAction2().setVisibility(View.VISIBLE); 552 if (mShowAction2Divider) { 553 vh.getAction2Divider().setVisibility(View.VISIBLE); 554 } 555 556 vh.getAction2().setText(mAction2Text); 557 vh.getAction2().setOnClickListener(mAction2OnClickListener); 558 }); 559 // Fall through 560 case SUPPLEMENTAL_ACTION_ONE_ACTION: 561 mBinders.add(vh -> { 562 vh.getAction1().setVisibility(View.VISIBLE); 563 if (mShowAction1Divider) { 564 vh.getAction1Divider().setVisibility(View.VISIBLE); 565 } 566 567 vh.getAction1().setText(mAction1Text); 568 vh.getAction1().setOnClickListener(mAction1OnClickListener); 569 }); 570 break; 571 case SUPPLEMENTAL_ACTION_NO_ACTION: 572 // Do nothing 573 break; 574 case SUPPLEMENTAL_ACTION_SWITCH: 575 mBinders.add(vh -> { 576 vh.getSwitch().setVisibility(View.VISIBLE); 577 vh.getSwitch().setChecked(mSwitchChecked); 578 vh.getSwitch().setOnCheckedChangeListener(mSwitchOnCheckedChangeListener); 579 if (mShowSwitchDivider) { 580 vh.getSwitchDivider().setVisibility(View.VISIBLE); 581 } 582 }); 583 break; 584 default: 585 throw new IllegalStateException("Unknown supplemental action type."); 586 } 587 } 588 589 /** 590 * Sets {@link View.OnClickListener} of {@code TextListItem}. 591 */ 592 public void setOnClickListener(View.OnClickListener listener) { 593 mOnClickListener = listener; 594 markDirty(); 595 } 596 597 /** 598 * Sets {@code Primary Action} to be represented by an icon. 599 * 600 * @param iconResId the resource identifier of the drawable. 601 * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item. 602 */ 603 public void setPrimaryActionIcon(@DrawableRes int iconResId, boolean useLargeIcon) { 604 setPrimaryActionIcon(mContext.getDrawable(iconResId), useLargeIcon); 605 } 606 607 /** 608 * Sets {@code Primary Action} to be represented by an icon. 609 * 610 * @param drawable the Drawable to set, or null to clear the content. 611 * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item. 612 */ 613 public void setPrimaryActionIcon(Drawable drawable, boolean useLargeIcon) { 614 mPrimaryActionType = useLargeIcon 615 ? PRIMARY_ACTION_TYPE_LARGE_ICON 616 : PRIMARY_ACTION_TYPE_SMALL_ICON; 617 mPrimaryActionIconDrawable = drawable; 618 619 markDirty(); 620 } 621 622 /** 623 * Sets {@code Primary Action} to be empty icon. 624 * 625 * <p>{@code Text} would have a start margin as if {@code Primary Action} were set to primary 626 * icon. 627 */ 628 public void setPrimaryActionEmptyIcon() { 629 mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON; 630 markDirty(); 631 } 632 633 /** 634 * Sets {@code Primary Action} to have no icon. Text would align to the start of item. 635 */ 636 public void setPrimaryActionNoIcon() { 637 mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON; 638 markDirty(); 639 } 640 641 /** 642 * Sets the title of item. 643 * 644 * <p>Primary text is {@code Title} by default. It can be set by 645 * {@link #setBody(String, boolean)} 646 * 647 * <p>{@code Title} text is limited to one line, and ellipses at the end. 648 * 649 * @param title text to display as title. 650 */ 651 public void setTitle(String title) { 652 mTitle = title; 653 markDirty(); 654 } 655 656 /** 657 * Sets the body text of item. 658 * 659 * <p>Text beyond length required by regulation will be truncated. Defaults {@code Title} 660 * text as the primary. 661 * @param body text to be displayed. 662 */ 663 public void setBody(String body) { 664 setBody(body, false); 665 } 666 667 /** 668 * Sets the body text of item. 669 * 670 * @param body text to be displayed. 671 * @param asPrimary sets {@code Body Text} as primary text of item. 672 */ 673 public void setBody(String body, boolean asPrimary) { 674 mBody = body; 675 mIsBodyPrimary = asPrimary; 676 677 markDirty(); 678 } 679 680 /** 681 * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}. 682 * 683 * @param iconResId drawable resource id. 684 * @param showDivider whether to display a vertical bar that separates {@code text} and 685 * {@code Supplemental Icon}. 686 */ 687 public void setSupplementalIcon(int iconResId, boolean showDivider) { 688 setSupplementalIcon(mContext.getDrawable(iconResId), showDivider, null); 689 } 690 691 /** 692 * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}. 693 * 694 * @param drawable the Drawable to set, or null to clear the content. 695 * @param showDivider whether to display a vertical bar that separates {@code text} and 696 * {@code Supplemental Icon}. 697 */ 698 public void setSupplementalIcon(Drawable drawable, boolean showDivider) { 699 setSupplementalIcon(drawable, showDivider, null); 700 } 701 702 /** 703 * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}. 704 * 705 * @param iconResId drawable resource id. 706 * @param showDivider whether to display a vertical bar that separates {@code text} and 707 * {@code Supplemental Icon}. 708 * @param listener the callback that will run when icon is clicked. 709 */ 710 public void setSupplementalIcon(int iconResId, boolean showDivider, 711 View.OnClickListener listener) { 712 setSupplementalIcon(mContext.getDrawable(iconResId), showDivider, listener); 713 } 714 715 /** 716 * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}. 717 * 718 * @param drawable the Drawable to set, or null to clear the content. 719 * @param showDivider whether to display a vertical bar that separates {@code text} and 720 * {@code Supplemental Icon}. 721 * @param listener the callback that will run when icon is clicked. 722 */ 723 public void setSupplementalIcon(Drawable drawable, boolean showDivider, 724 View.OnClickListener listener) { 725 mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON; 726 727 mSupplementalIconDrawable = drawable; 728 mSupplementalIconOnClickListener = listener; 729 mShowSupplementalIconDivider = showDivider; 730 markDirty(); 731 } 732 733 /** 734 * Sets {@code Supplemental Action} to be represented by an {@code Action Button}. 735 * 736 * @param text button text to display. 737 * @param showDivider whether to display a vertical bar that separates {@code Text} and 738 * {@code Action Button}. 739 * @param listener the callback that will run when action button is clicked. 740 */ 741 public void setAction(String text, boolean showDivider, View.OnClickListener listener) { 742 if (TextUtils.isEmpty(text)) { 743 throw new IllegalArgumentException("Action text cannot be empty."); 744 } 745 if (listener == null) { 746 throw new IllegalArgumentException("Action OnClickListener cannot be null."); 747 } 748 mSupplementalActionType = SUPPLEMENTAL_ACTION_ONE_ACTION; 749 750 mAction1Text = text; 751 mAction1OnClickListener = listener; 752 mShowAction1Divider = showDivider; 753 754 markDirty(); 755 } 756 757 /** 758 * Sets {@code Supplemental Action} to be represented by two {@code Action Button}s. 759 * 760 * <p>These two action buttons will be aligned towards item end. 761 * 762 * @param action1Text button text to display - this button will be closer to item end. 763 * @param action2Text button text to display. 764 */ 765 public void setActions(String action1Text, boolean showAction1Divider, 766 View.OnClickListener action1OnClickListener, 767 String action2Text, boolean showAction2Divider, 768 View.OnClickListener action2OnClickListener) { 769 if (TextUtils.isEmpty(action1Text) || TextUtils.isEmpty(action2Text)) { 770 throw new IllegalArgumentException("Action text cannot be empty."); 771 } 772 if (action1OnClickListener == null || action2OnClickListener == null) { 773 throw new IllegalArgumentException("Action OnClickListener cannot be null."); 774 } 775 mSupplementalActionType = SUPPLEMENTAL_ACTION_TWO_ACTIONS; 776 777 mAction1Text = action1Text; 778 mAction1OnClickListener = action1OnClickListener; 779 mShowAction1Divider = showAction1Divider; 780 mAction2Text = action2Text; 781 mAction2OnClickListener = action2OnClickListener; 782 mShowAction2Divider = showAction2Divider; 783 784 markDirty(); 785 } 786 787 /** 788 * Sets {@code Supplemental Action} to be represented by a {@link android.widget.Switch}. 789 * 790 * @param checked initial value for switched. 791 * @param showDivider whether to display a vertical bar between switch and text. 792 * @param listener callback to be invoked when the checked state is markDirty. 793 */ 794 public void setSwitch(boolean checked, boolean showDivider, 795 CompoundButton.OnCheckedChangeListener listener) { 796 mSupplementalActionType = SUPPLEMENTAL_ACTION_SWITCH; 797 798 mSwitchChecked = checked; 799 mShowSwitchDivider = showDivider; 800 mSwitchOnCheckedChangeListener = listener; 801 802 markDirty(); 803 } 804 805 /** 806 * Sets the state of {@code Switch}. For this method to take effect, 807 * {@link #setSwitch(boolean, boolean, CompoundButton.OnCheckedChangeListener)} must be called 808 * first to set {@code Supplemental Action} as a {@code Switch}. 809 * 810 * @param isChecked sets the "checked/unchecked, namely on/off" state of switch. 811 */ 812 public void setSwitchState(boolean isChecked) { 813 mSwitchChecked = isChecked; 814 markDirty(); 815 } 816 817 /** 818 * Holds views of TextListItem. 819 */ 820 public static class ViewHolder extends ListItem.ViewHolder { 821 822 private RelativeLayout mContainerLayout; 823 824 private ImageView mPrimaryIcon; 825 826 private TextView mTitle; 827 private TextView mBody; 828 829 private View mSupplementalIconDivider; 830 private ImageView mSupplementalIcon; 831 832 private Button mAction1; 833 private View mAction1Divider; 834 835 private Button mAction2; 836 private View mAction2Divider; 837 838 private Switch mSwitch; 839 private View mSwitchDivider; 840 841 public ViewHolder(View itemView) { 842 super(itemView); 843 844 mContainerLayout = itemView.findViewById(R.id.container); 845 846 mPrimaryIcon = itemView.findViewById(R.id.primary_icon); 847 848 mTitle = itemView.findViewById(R.id.title); 849 mBody = itemView.findViewById(R.id.body); 850 851 mSupplementalIcon = itemView.findViewById(R.id.supplemental_icon); 852 mSupplementalIconDivider = itemView.findViewById(R.id.supplemental_icon_divider); 853 854 mSwitch = itemView.findViewById(R.id.switch_widget); 855 mSwitchDivider = itemView.findViewById(R.id.switch_divider); 856 857 mAction1 = itemView.findViewById(R.id.action1); 858 mAction1Divider = itemView.findViewById(R.id.action1_divider); 859 mAction2 = itemView.findViewById(R.id.action2); 860 mAction2Divider = itemView.findViewById(R.id.action2_divider); 861 } 862 863 /** 864 * Update children views to comply with car UX restrictions. 865 * 866 * <p>{@code Body} text might be truncated to meet length limit required by regulation. 867 * 868 * @param restrictions current car UX restrictions. 869 */ 870 @Override 871 protected void complyWithUxRestrictions(CarUxRestrictions restrictions) { 872 CarUxRestrictionsUtils.comply(itemView.getContext(), restrictions, getBody()); 873 } 874 875 public RelativeLayout getContainerLayout() { 876 return mContainerLayout; 877 } 878 879 public ImageView getPrimaryIcon() { 880 return mPrimaryIcon; 881 } 882 883 public TextView getTitle() { 884 return mTitle; 885 } 886 887 public TextView getBody() { 888 return mBody; 889 } 890 891 public ImageView getSupplementalIcon() { 892 return mSupplementalIcon; 893 } 894 895 public View getSupplementalIconDivider() { 896 return mSupplementalIconDivider; 897 } 898 899 public View getSwitchDivider() { 900 return mSwitchDivider; 901 } 902 903 public Switch getSwitch() { 904 return mSwitch; 905 } 906 907 public Button getAction1() { 908 return mAction1; 909 } 910 911 public View getAction1Divider() { 912 return mAction1Divider; 913 } 914 915 public Button getAction2() { 916 return mAction2; 917 } 918 919 public View getAction2Divider() { 920 return mAction2Divider; 921 } 922 } 923} 924