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