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