ListItem.java revision acdf6a30e3d5b12defece58c35cfca2129838264
1/* 2 * Copyright 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 17package androidx.car.widget; 18 19import static java.lang.annotation.RetentionPolicy.SOURCE; 20 21import android.content.Context; 22import android.content.res.Resources; 23import android.graphics.drawable.Drawable; 24import android.support.annotation.DrawableRes; 25import android.support.annotation.IntDef; 26import android.support.v7.widget.RecyclerView; 27import android.text.TextUtils; 28import android.view.View; 29import android.widget.CompoundButton; 30import android.widget.RelativeLayout; 31 32import java.lang.annotation.Retention; 33import java.util.ArrayList; 34import java.util.List; 35 36import androidx.car.R; 37 38/** 39 * Class to build a list item. 40 * 41 * <p>An item supports primary action and supplemental action(s). 42 * 43 * <p>An item visually composes of 3 parts; each part may contain multiple views. 44 * <ul> 45 * <li>{@code Primary Action}: represented by an icon of following types. 46 * <ul> 47 * <li>Primary Icon - icon size could be large or small. 48 * <li>No Icon 49 * <li>Empty Icon - different from No Icon by how much margin {@code Text} offsets 50 * </ul> 51 * <li>{@code Text}: supports any combination of the follow text views. 52 * <ul> 53 * <li>Title 54 * <li>Body 55 * </ul> 56 * <li>{@code Supplemental Action(s)}: represented by one of the following types; aligned toward 57 * the end of item. 58 * <ul> 59 * <li>Supplemental Icon 60 * <li>One Action Button 61 * <li>Two Action Buttons 62 * <li>Switch</li> 63 * </ul> 64 * </ul> 65 * 66 * {@link ListItem} can be built through its {@link ListItem.Builder}. It binds data 67 * to {@link ListItemAdapter.ViewHolder} based on components selected. 68 */ 69public class ListItem { 70 71 private Builder mBuilder; 72 73 private ListItem(Builder builder) { 74 mBuilder = builder; 75 } 76 77 /** 78 * Applies all {@link ViewBinder} to {@code viewHolder}. 79 */ 80 void bind(ListItemAdapter.ViewHolder viewHolder) { 81 setAllSubViewsGone(viewHolder); 82 for (ViewBinder binder : mBuilder.mBinders) { 83 binder.bind(viewHolder); 84 } 85 } 86 87 void setAllSubViewsGone(ListItemAdapter.ViewHolder vh) { 88 View[] subviews = new View[] { 89 vh.getPrimaryIcon(), 90 vh.getTitle(), vh.getBody(), 91 vh.getSupplementalIcon(), vh.getSupplementalIconDivider(), 92 vh.getSwitch(), vh.getSwitchDivider(), 93 vh.getAction1(), vh.getAction1Divider(), vh.getAction2(), vh.getAction2Divider()}; 94 for (View v : subviews) { 95 v.setVisibility(View.GONE); 96 } 97 } 98 99 /** 100 * Returns whether the divider that comes after the ListItem should be hidden or not. 101 * 102 * <p>Note: For this to work, one must invoke 103 * {@code PagedListView.setDividerVisibilityManager(adapter} for {@link ListItemAdapter} and 104 * have dividers enabled on {@link PagedListView}. 105 * 106 * @return {@code true} if divider coming after the item should be hidden, {@code false} 107 * otherwise. 108 */ 109 boolean shouldHideDivider() { 110 return mBuilder.mHideDivider; 111 } 112 113 /** 114 * Functional interface to provide a way to interact with views in 115 * {@link ListItemAdapter.ViewHolder}. {@code ViewBinder}s added to a 116 * {@code ListItem} will be called when {@code ListItem} {@code bind}s to 117 * {@link ListItemAdapter.ViewHolder}. 118 */ 119 public interface ViewBinder { 120 /** 121 * Provides a way to interact with views in view holder. 122 */ 123 void bind(ListItemAdapter.ViewHolder viewHolder); 124 } 125 126 /** 127 * Builds a {@link ListItem}. 128 * 129 * <p>With conflicting methods are called, e.g. setting primary action to both primary icon and 130 * no icon, the last called method wins. 131 */ 132 public static class Builder { 133 134 @Retention(SOURCE) 135 @IntDef({ 136 PRIMARY_ACTION_TYPE_NO_ICON, PRIMARY_ACTION_TYPE_EMPTY_ICON, 137 PRIMARY_ACTION_TYPE_LARGE_ICON, PRIMARY_ACTION_TYPE_SMALL_ICON}) 138 private @interface PrimaryActionType {} 139 140 private static final int PRIMARY_ACTION_TYPE_NO_ICON = 0; 141 private static final int PRIMARY_ACTION_TYPE_EMPTY_ICON = 1; 142 private static final int PRIMARY_ACTION_TYPE_LARGE_ICON = 2; 143 private static final int PRIMARY_ACTION_TYPE_SMALL_ICON = 3; 144 145 @Retention(SOURCE) 146 @IntDef({SUPPLEMENTAL_ACTION_NO_ACTION, SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON, 147 SUPPLEMENTAL_ACTION_ONE_ACTION, SUPPLEMENTAL_ACTION_TWO_ACTIONS, 148 SUPPLEMENTAL_ACTION_SWITCH}) 149 private @interface SupplementalActionType {} 150 151 private static final int SUPPLEMENTAL_ACTION_NO_ACTION = 0; 152 private static final int SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON = 1; 153 private static final int SUPPLEMENTAL_ACTION_ONE_ACTION = 2; 154 private static final int SUPPLEMENTAL_ACTION_TWO_ACTIONS = 3; 155 private static final int SUPPLEMENTAL_ACTION_SWITCH = 4; 156 157 private final Context mContext; 158 private final List<ViewBinder> mBinders = new ArrayList<>(); 159 // Store custom binders separately so they will bind after binders are created in build(). 160 private final List<ViewBinder> mCustomBinders = new ArrayList<>(); 161 162 private View.OnClickListener mOnClickListener; 163 164 @PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON; 165 private int mPrimaryActionIconResId; 166 private Drawable mPrimaryActionIconDrawable; 167 168 private String mTitle; 169 private String mBody; 170 private boolean mIsBodyPrimary; 171 // tag for indicating whether to hide the divider 172 private boolean mHideDivider; 173 174 @SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION; 175 private int mSupplementalIconResId; 176 private View.OnClickListener mSupplementalIconOnClickListener; 177 private boolean mShowSupplementalIconDivider; 178 179 private boolean mSwitchChecked; 180 private boolean mShowSwitchDivider; 181 private CompoundButton.OnCheckedChangeListener mSwitchOnCheckedChangeListener; 182 183 private String mAction1Text; 184 private View.OnClickListener mAction1OnClickListener; 185 private boolean mShowAction1Divider; 186 private String mAction2Text; 187 private View.OnClickListener mAction2OnClickListener; 188 private boolean mShowAction2Divider; 189 190 public Builder(Context context) { 191 mContext = context; 192 } 193 194 /** 195 * Builds a {@link ListItem}. Adds {@link ViewBinder}s that will adjust layout in 196 * {@link ListItemAdapter.ViewHolder} depending on sub-views used. 197 */ 198 public ListItem build() { 199 setItemLayoutHeight(); 200 setPrimaryAction(); 201 setText(); 202 setSupplementalActions(); 203 setOnClickListener(); 204 205 mBinders.addAll(mCustomBinders); 206 207 return new ListItem(this); 208 } 209 210 /** 211 * Sets the height of item depending on which text field is set. 212 */ 213 private void setItemLayoutHeight() { 214 if (TextUtils.isEmpty(mBody)) { 215 // If the item only has title or no text, it uses fixed-height as single line. 216 int height = (int) mContext.getResources().getDimension( 217 R.dimen.car_single_line_list_item_height); 218 mBinders.add((vh) -> { 219 RecyclerView.LayoutParams layoutParams = 220 (RecyclerView.LayoutParams) vh.itemView.getLayoutParams(); 221 layoutParams.height = height; 222 vh.itemView.setLayoutParams(layoutParams); 223 }); 224 } else { 225 // If body is present, the item should be at least as tall as min height, and wraps 226 // content. 227 int minHeight = (int) mContext.getResources().getDimension( 228 R.dimen.car_double_line_list_item_height); 229 mBinders.add((vh) -> { 230 vh.itemView.setMinimumHeight(minHeight); 231 vh.getContainerLayout().setMinimumHeight(minHeight); 232 233 RecyclerView.LayoutParams layoutParams = 234 (RecyclerView.LayoutParams) vh.itemView.getLayoutParams(); 235 layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT; 236 vh.itemView.setLayoutParams(layoutParams); 237 }); 238 } 239 } 240 241 private void setPrimaryAction() { 242 setPrimaryIconContent(); 243 setPrimaryIconLayout(); 244 } 245 246 private void setText() { 247 setTextContent(); 248 setTextVerticalMargin(); 249 // Only setting start margin because text end is relative to the start of supplemental 250 // actions. 251 setTextStartMargin(); 252 } 253 254 private void setOnClickListener() { 255 if (mOnClickListener != null) { 256 mBinders.add(vh -> vh.itemView.setOnClickListener(mOnClickListener)); 257 } 258 } 259 260 private void setPrimaryIconContent() { 261 switch (mPrimaryActionType) { 262 case PRIMARY_ACTION_TYPE_SMALL_ICON: 263 case PRIMARY_ACTION_TYPE_LARGE_ICON: 264 mBinders.add((vh) -> { 265 vh.getPrimaryIcon().setVisibility(View.VISIBLE); 266 267 if (mPrimaryActionIconDrawable != null) { 268 vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable); 269 } else if (mPrimaryActionIconResId != 0) { 270 vh.getPrimaryIcon().setImageResource(mPrimaryActionIconResId); 271 } 272 }); 273 break; 274 case PRIMARY_ACTION_TYPE_EMPTY_ICON: 275 case PRIMARY_ACTION_TYPE_NO_ICON: 276 // Do nothing. 277 break; 278 default: 279 throw new IllegalStateException("Unrecognizable primary action type."); 280 } 281 } 282 283 /** 284 * Sets layout params of primary icon. 285 * 286 * <p>Large icon will have no start margin, and always align center vertically. 287 * 288 * <p>Small icon will have start margin. When body text is present small icon uses a top 289 * margin otherwise align center vertically. 290 */ 291 private void setPrimaryIconLayout() { 292 // Set all relevant fields in layout params to avoid carried over params when the item 293 // gets bound to a recycled view holder. 294 switch (mPrimaryActionType) { 295 case PRIMARY_ACTION_TYPE_SMALL_ICON: 296 mBinders.add(vh -> { 297 int iconSize = mContext.getResources().getDimensionPixelSize( 298 R.dimen.car_primary_icon_size); 299 // Icon size. 300 RelativeLayout.LayoutParams layoutParams = 301 (RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams(); 302 layoutParams.height = iconSize; 303 layoutParams.width = iconSize; 304 305 // Start margin. 306 layoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize( 307 R.dimen.car_keyline_1)); 308 309 if (!TextUtils.isEmpty(mBody)) { 310 // Set icon top margin so that the icon remains in the same position it 311 // would've been in for non-long-text item, namely so that the center 312 // line of icon matches that of line item. 313 layoutParams.removeRule(RelativeLayout.CENTER_VERTICAL); 314 int itemHeight = mContext.getResources().getDimensionPixelSize( 315 R.dimen.car_double_line_list_item_height); 316 layoutParams.topMargin = (itemHeight - iconSize) / 2; 317 } else { 318 // If the icon can be centered vertically, leave the work for framework. 319 layoutParams.addRule(RelativeLayout.CENTER_VERTICAL); 320 layoutParams.topMargin = 0; 321 } 322 vh.getPrimaryIcon().setLayoutParams(layoutParams); 323 }); 324 break; 325 case PRIMARY_ACTION_TYPE_LARGE_ICON: 326 mBinders.add(vh -> { 327 int iconSize = mContext.getResources().getDimensionPixelSize( 328 R.dimen.car_single_line_list_item_height); 329 // Icon size. 330 RelativeLayout.LayoutParams layoutParams = 331 (RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams(); 332 layoutParams.height = iconSize; 333 layoutParams.width = iconSize; 334 335 // No start margin. 336 layoutParams.setMarginStart(0); 337 338 // Always centered vertically. 339 layoutParams.addRule(RelativeLayout.CENTER_VERTICAL); 340 layoutParams.topMargin = 0; 341 342 vh.getPrimaryIcon().setLayoutParams(layoutParams); 343 }); 344 break; 345 case PRIMARY_ACTION_TYPE_EMPTY_ICON: 346 case PRIMARY_ACTION_TYPE_NO_ICON: 347 // Do nothing. 348 break; 349 default: 350 throw new IllegalStateException("Unrecognizable primary action type."); 351 } 352 } 353 354 private void setTextContent() { 355 if (!TextUtils.isEmpty(mTitle)) { 356 mBinders.add(vh -> { 357 vh.getTitle().setVisibility(View.VISIBLE); 358 vh.getTitle().setText(mTitle); 359 }); 360 } 361 if (!TextUtils.isEmpty(mBody)) { 362 mBinders.add(vh -> { 363 vh.getBody().setVisibility(View.VISIBLE); 364 vh.getBody().setText(mBody); 365 }); 366 } 367 368 if (mIsBodyPrimary) { 369 mBinders.add((vh) -> { 370 vh.getTitle().setTextAppearance(R.style.CarBody2); 371 vh.getBody().setTextAppearance(R.style.CarBody1); 372 }); 373 } else { 374 mBinders.add((vh) -> { 375 vh.getTitle().setTextAppearance(R.style.CarBody1); 376 vh.getBody().setTextAppearance(R.style.CarBody2); 377 }); 378 } 379 } 380 381 /** 382 * Sets start margin of text view depending on icon type. 383 */ 384 private void setTextStartMargin() { 385 final int startMarginResId; 386 switch (mPrimaryActionType) { 387 case PRIMARY_ACTION_TYPE_NO_ICON: 388 startMarginResId = R.dimen.car_keyline_1; 389 break; 390 case PRIMARY_ACTION_TYPE_EMPTY_ICON: 391 startMarginResId = R.dimen.car_keyline_3; 392 break; 393 case PRIMARY_ACTION_TYPE_SMALL_ICON: 394 startMarginResId = R.dimen.car_keyline_3; 395 break; 396 case PRIMARY_ACTION_TYPE_LARGE_ICON: 397 startMarginResId = R.dimen.car_keyline_4; 398 break; 399 default: 400 throw new IllegalStateException("Unrecognizable primary action type."); 401 } 402 int startMargin = mContext.getResources().getDimensionPixelSize(startMarginResId); 403 mBinders.add(vh -> { 404 RelativeLayout.LayoutParams titleLayoutParams = 405 (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams(); 406 titleLayoutParams.setMarginStart(startMargin); 407 vh.getTitle().setLayoutParams(titleLayoutParams); 408 409 RelativeLayout.LayoutParams bodyLayoutParams = 410 (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams(); 411 bodyLayoutParams.setMarginStart(startMargin); 412 vh.getBody().setLayoutParams(bodyLayoutParams); 413 }); 414 } 415 416 /** 417 * Sets top/bottom margins of {@code Title} and {@code Body}. 418 */ 419 private void setTextVerticalMargin() { 420 // Set all relevant fields in layout params to avoid carried over params when the item 421 // gets bound to a recycled view holder. 422 if (!TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mBody)) { 423 // Title only - view is aligned center vertically by itself. 424 mBinders.add(vh -> { 425 RelativeLayout.LayoutParams layoutParams = 426 (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams(); 427 layoutParams.addRule(RelativeLayout.CENTER_VERTICAL); 428 layoutParams.topMargin = 0; 429 vh.getTitle().setLayoutParams(layoutParams); 430 }); 431 } else if (TextUtils.isEmpty(mTitle) && !TextUtils.isEmpty(mBody)) { 432 mBinders.add(vh -> { 433 // Body uses top and bottom margin. 434 int margin = mContext.getResources().getDimensionPixelSize( 435 R.dimen.car_padding_3); 436 RelativeLayout.LayoutParams layoutParams = 437 (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams(); 438 layoutParams.addRule(RelativeLayout.CENTER_VERTICAL); 439 layoutParams.removeRule(RelativeLayout.BELOW); 440 layoutParams.topMargin = margin; 441 layoutParams.bottomMargin = margin; 442 vh.getBody().setLayoutParams(layoutParams); 443 }); 444 } else { 445 mBinders.add(vh -> { 446 // Title has a top margin 447 Resources resources = mContext.getResources(); 448 int padding1 = resources.getDimensionPixelSize(R.dimen.car_padding_1); 449 int padding3 = resources.getDimensionPixelSize(R.dimen.car_padding_3); 450 451 RelativeLayout.LayoutParams titleLayoutParams = 452 (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams(); 453 titleLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL); 454 titleLayoutParams.topMargin = padding3; 455 vh.getTitle().setLayoutParams(titleLayoutParams); 456 // Body is below title with a margin, and has bottom margin. 457 RelativeLayout.LayoutParams bodyLayoutParams = 458 (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams(); 459 bodyLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL); 460 bodyLayoutParams.addRule(RelativeLayout.BELOW, R.id.title); 461 bodyLayoutParams.topMargin = padding1; 462 bodyLayoutParams.bottomMargin = padding3; 463 vh.getBody().setLayoutParams(bodyLayoutParams); 464 }); 465 } 466 } 467 468 /** 469 * Sets up view(s) for supplemental action. 470 */ 471 private void setSupplementalActions() { 472 switch (mSupplementalActionType) { 473 case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON: 474 mBinders.add((vh) -> { 475 vh.getSupplementalIcon().setVisibility(View.VISIBLE); 476 if (mShowSupplementalIconDivider) { 477 vh.getSupplementalIconDivider().setVisibility(View.VISIBLE); 478 } 479 480 vh.getSupplementalIcon().setImageResource(mSupplementalIconResId); 481 vh.getSupplementalIcon().setOnClickListener( 482 mSupplementalIconOnClickListener); 483 vh.getSupplementalIcon().setClickable( 484 mSupplementalIconOnClickListener != null); 485 }); 486 break; 487 case SUPPLEMENTAL_ACTION_TWO_ACTIONS: 488 mBinders.add((vh) -> { 489 vh.getAction2().setVisibility(View.VISIBLE); 490 if (mShowAction2Divider) { 491 vh.getAction2Divider().setVisibility(View.VISIBLE); 492 } 493 494 vh.getAction2().setText(mAction2Text); 495 vh.getAction2().setOnClickListener(mAction2OnClickListener); 496 }); 497 // Fall through 498 case SUPPLEMENTAL_ACTION_ONE_ACTION: 499 mBinders.add((vh) -> { 500 vh.getAction1().setVisibility(View.VISIBLE); 501 if (mShowAction1Divider) { 502 vh.getAction1Divider().setVisibility(View.VISIBLE); 503 } 504 505 vh.getAction1().setText(mAction1Text); 506 vh.getAction1().setOnClickListener(mAction1OnClickListener); 507 }); 508 break; 509 case SUPPLEMENTAL_ACTION_NO_ACTION: 510 // Do nothing 511 break; 512 case SUPPLEMENTAL_ACTION_SWITCH: 513 mBinders.add(vh -> { 514 vh.getSwitch().setVisibility(View.VISIBLE); 515 vh.getSwitch().setChecked(mSwitchChecked); 516 vh.getSwitch().setOnCheckedChangeListener(mSwitchOnCheckedChangeListener); 517 if (mShowSwitchDivider) { 518 vh.getSwitchDivider().setVisibility(View.VISIBLE); 519 } 520 }); 521 break; 522 default: 523 throw new IllegalArgumentException("Unrecognized supplemental action type."); 524 } 525 } 526 527 /** 528 * Instructs the Builder to always hide the item divider coming after this ListItem. 529 * 530 * @return This Builder object to allow for chaining calls to set methods. 531 */ 532 public Builder withDividerHidden() { 533 mHideDivider = true; 534 return this; 535 } 536 537 /** 538 * Sets {@link View.OnClickListener} of {@code ListItem}. 539 * 540 * @return This Builder object to allow for chaining calls to set methods. 541 */ 542 public Builder withOnClickListener(View.OnClickListener listener) { 543 mOnClickListener = listener; 544 return this; 545 } 546 547 /** 548 * Sets {@code Primary Action} to be represented by an icon. 549 * 550 * @param iconResId the resource identifier of the drawable. 551 * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item 552 * with only title set; useful for album cover art. 553 * @return This Builder object to allow for chaining calls to set methods. 554 */ 555 public Builder withPrimaryActionIcon(@DrawableRes int iconResId, boolean useLargeIcon) { 556 return withPrimaryActionIcon(null, iconResId, useLargeIcon); 557 } 558 559 /** 560 * Sets {@code Primary Action} to be represented by an icon. 561 * 562 * @param drawable the Drawable to set, or null to clear the content. 563 * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item 564 * with only title set; useful for album cover art. 565 * @return This Builder object to allow for chaining calls to set methods. 566 */ 567 public Builder withPrimaryActionIcon(Drawable drawable, boolean useLargeIcon) { 568 return withPrimaryActionIcon(drawable, 0, useLargeIcon); 569 } 570 571 private Builder withPrimaryActionIcon(Drawable drawable, @DrawableRes int iconResId, 572 boolean useLargeIcon) { 573 mPrimaryActionType = useLargeIcon 574 ? PRIMARY_ACTION_TYPE_LARGE_ICON 575 : PRIMARY_ACTION_TYPE_SMALL_ICON; 576 mPrimaryActionIconResId = iconResId; 577 mPrimaryActionIconDrawable = drawable; 578 return this; 579 } 580 581 /** 582 * Sets {@code Primary Action} to be empty icon. 583 * 584 * {@code Text} would have a start margin as if {@code Primary Action} were set to 585 * primary icon. 586 * 587 * @return This Builder object to allow for chaining calls to set methods. 588 */ 589 public Builder withPrimaryActionEmptyIcon() { 590 mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON; 591 return this; 592 } 593 594 /** 595 * Sets {@code Primary Action} to have no icon. Text would align to the start of item. 596 * 597 * @return This Builder object to allow for chaining calls to set methods. 598 */ 599 public Builder withPrimaryActionNoIcon() { 600 mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON; 601 return this; 602 } 603 604 /** 605 * Sets the title of item. 606 * 607 * <p>Primary text is {@code title} by default. It can be set by 608 * {@link #withBody(String, boolean)} 609 * 610 * @param title text to display as title. 611 * @return This Builder object to allow for chaining calls to set methods. 612 */ 613 public Builder withTitle(String title) { 614 mTitle = title; 615 return this; 616 } 617 618 /** 619 * Sets the body text of item. 620 * 621 * <p>Text beyond length required by regulation will be truncated. Defaults {@code Title} 622 * text as the primary. 623 * 624 * @return This Builder object to allow for chaining calls to set methods. 625 */ 626 public Builder withBody(String body) { 627 return withBody(body, false); 628 } 629 630 /** 631 * Sets the body text of item. 632 * 633 * <p>Text beyond length required by regulation will be truncated. 634 * 635 * @param asPrimary sets {@code Body Text} as primary text of item. 636 * @return This Builder object to allow for chaining calls to set methods. 637 */ 638 public Builder withBody(String body, boolean asPrimary) { 639 int limit = mContext.getResources().getInteger( 640 R.integer.car_list_item_text_length_limit); 641 if (body.length() < limit) { 642 mBody = body; 643 } else { 644 mBody = body.substring(0, limit) + mContext.getString(R.string.ellipsis); 645 } 646 mIsBodyPrimary = asPrimary; 647 return this; 648 } 649 650 /** 651 * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}. 652 * 653 * @return This Builder object to allow for chaining calls to set methods. 654 */ 655 public Builder withSupplementalIcon(int iconResId, boolean showDivider) { 656 return withSupplementalIcon(iconResId, showDivider, null); 657 } 658 659 /** 660 * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}. 661 * 662 * @param iconResId drawable resource id. 663 * @return This Builder object to allow for chaining calls to set methods. 664 */ 665 public Builder withSupplementalIcon(int iconResId, boolean showDivider, 666 View.OnClickListener listener) { 667 mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON; 668 669 mSupplementalIconResId = iconResId; 670 mSupplementalIconOnClickListener = listener; 671 mShowSupplementalIconDivider = showDivider; 672 return this; 673 } 674 675 /** 676 * Sets {@code Supplemental Action} to be represented by an {@code Action Button}. 677 * 678 * @param text button text to display. 679 * @return This Builder object to allow for chaining calls to set methods. 680 */ 681 public Builder withAction(String text, boolean showDivider, View.OnClickListener listener) { 682 if (TextUtils.isEmpty(text)) { 683 throw new IllegalArgumentException("Action text cannot be empty."); 684 } 685 if (listener == null) { 686 throw new IllegalArgumentException("Action OnClickListener cannot be null."); 687 } 688 mSupplementalActionType = SUPPLEMENTAL_ACTION_ONE_ACTION; 689 690 mAction1Text = text; 691 mAction1OnClickListener = listener; 692 mShowAction1Divider = showDivider; 693 return this; 694 } 695 696 /** 697 * Sets {@code Supplemental Action} to be represented by two {@code Action Button}s. 698 * 699 * <p>These two action buttons will be aligned towards item end. 700 * 701 * @param action1Text button text to display - this button will be closer to item end. 702 * @param action2Text button text to display. 703 * @return This Builder object to allow for chaining calls to set methods. 704 */ 705 public Builder withActions(String action1Text, boolean showAction1Divider, 706 View.OnClickListener action1OnClickListener, 707 String action2Text, boolean showAction2Divider, 708 View.OnClickListener action2OnClickListener) { 709 if (TextUtils.isEmpty(action1Text) || TextUtils.isEmpty(action2Text)) { 710 throw new IllegalArgumentException("Action text cannot be empty."); 711 } 712 if (action1OnClickListener == null || action2OnClickListener == null) { 713 throw new IllegalArgumentException("Action OnClickListener cannot be null."); 714 } 715 mSupplementalActionType = SUPPLEMENTAL_ACTION_TWO_ACTIONS; 716 717 mAction1Text = action1Text; 718 mAction1OnClickListener = action1OnClickListener; 719 mShowAction1Divider = showAction1Divider; 720 mAction2Text = action2Text; 721 mAction2OnClickListener = action2OnClickListener; 722 mShowAction2Divider = showAction2Divider; 723 return this; 724 } 725 726 /** 727 * Sets {@code Supplemental Action} to be represented by a {@link android.widget.Switch}. 728 * 729 * @param checked initial value for switched. 730 * @param showDivider whether to display a vertical bar between switch and text. 731 * @param listener callback to be invoked when the checked state is changed. 732 * @return This Builder object to allow for chaining calls to set methods. 733 */ 734 public Builder withSwitch(boolean checked, boolean showDivider, 735 CompoundButton.OnCheckedChangeListener listener) { 736 mSupplementalActionType = SUPPLEMENTAL_ACTION_SWITCH; 737 738 mSwitchChecked = checked; 739 mShowSwitchDivider = showDivider; 740 mSwitchOnCheckedChangeListener = listener; 741 return this; 742 } 743 744 /** 745 * Adds {@link ViewBinder} to interact with sub-views in 746 * {@link ListItemAdapter.ViewHolder}. These ViewBinders will always bind after 747 * other {@link Builder} methods have bond. 748 * 749 * <p>Make sure to call with...() method on the intended sub-view first. 750 * 751 * <p>Example: 752 * <pre> 753 * {@code 754 * new Builder() 755 * .withTitle("title") 756 * .withViewBinder((viewHolder) -> { 757 * viewHolder.getTitle().doMoreStuff(); 758 * }) 759 * .build(); 760 * } 761 * </pre> 762 */ 763 public Builder withViewBinder(ViewBinder binder) { 764 mCustomBinders.add(binder); 765 return this; 766 } 767 } 768} 769