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