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