TextListItem.java revision 52a80988da3d27f591abd43faa27eae120996ec2
1/*
2 * Copyright 2018 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.car.drivingstate.CarUxRestrictions;
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.drawable.Drawable;
25import android.text.TextUtils;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.Button;
29import android.widget.CompoundButton;
30import android.widget.ImageView;
31import android.widget.RelativeLayout;
32import android.widget.Switch;
33import android.widget.TextView;
34
35import androidx.annotation.DrawableRes;
36import androidx.annotation.IdRes;
37import androidx.annotation.IntDef;
38import androidx.annotation.StyleRes;
39import androidx.car.R;
40import androidx.car.utils.CarUxRestrictionsUtils;
41import androidx.recyclerview.widget.RecyclerView;
42
43import java.lang.annotation.Retention;
44import java.util.ArrayList;
45import java.util.List;
46
47/**
48 * Class to build a list item of text.
49 *
50 * <p>An item supports primary action and supplemental action(s).
51 *
52 * <p>An item visually composes of 3 parts; each part may contain multiple views.
53 * <ul>
54 *     <li>{@code Primary Action}: represented by an icon of following types.
55 *     <ul>
56 *         <li>Primary Icon - icon size could be large or small.
57 *         <li>No Icon - no icon is shown.
58 *         <li>Empty Icon - {@code Text} offsets start space as if there was an icon.
59 *     </ul>
60 *     <li>{@code Text}: supports any combination of the following text views.
61 *     <ul>
62 *         <li>Title
63 *         <li>Body
64 *     </ul>
65 *     <li>{@code Supplemental Action}: represented by one of the following types; aligned toward
66 *     the end of item.
67 *     <ul>
68 *         <li>Supplemental Icon
69 *         <li>One Action Button
70 *         <li>Two Action Buttons
71 *         <li>Switch
72 *     </ul>
73 * </ul>
74 *
75 * <p>{@code TextListItem} binds data to {@link ViewHolder} based on components selected.
76 *
77 * <p>When conflicting setter methods are called (e.g. setting primary action to both primary icon
78 * and no icon), the last called method wins.
79 */
80public class TextListItem extends ListItem<TextListItem.ViewHolder> {
81
82    @Retention(SOURCE)
83    @IntDef({
84            PRIMARY_ACTION_TYPE_NO_ICON, PRIMARY_ACTION_TYPE_EMPTY_ICON,
85            PRIMARY_ACTION_TYPE_LARGE_ICON, PRIMARY_ACTION_TYPE_SMALL_ICON})
86    private @interface PrimaryActionType {}
87
88    private static final int PRIMARY_ACTION_TYPE_NO_ICON = 0;
89    private static final int PRIMARY_ACTION_TYPE_EMPTY_ICON = 1;
90    private static final int PRIMARY_ACTION_TYPE_LARGE_ICON = 2;
91    private static final int PRIMARY_ACTION_TYPE_SMALL_ICON = 3;
92
93    @Retention(SOURCE)
94    @IntDef({SUPPLEMENTAL_ACTION_NO_ACTION, SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON,
95            SUPPLEMENTAL_ACTION_ONE_ACTION, SUPPLEMENTAL_ACTION_TWO_ACTIONS,
96            SUPPLEMENTAL_ACTION_SWITCH})
97    private @interface SupplementalActionType {}
98
99    private static final int SUPPLEMENTAL_ACTION_NO_ACTION = 0;
100    private static final int SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON = 1;
101    private static final int SUPPLEMENTAL_ACTION_ONE_ACTION = 2;
102    private static final int SUPPLEMENTAL_ACTION_TWO_ACTIONS = 3;
103    private static final int SUPPLEMENTAL_ACTION_SWITCH = 4;
104
105    private final Context mContext;
106
107    private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
108
109    private View.OnClickListener mOnClickListener;
110
111    @PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
112    private Drawable mPrimaryActionIconDrawable;
113
114    private String mTitle;
115    private String mBody;
116    private boolean mIsBodyPrimary;
117
118    @SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
119    private Drawable mSupplementalIconDrawable;
120    private View.OnClickListener mSupplementalIconOnClickListener;
121    private boolean mShowSupplementalIconDivider;
122
123    private boolean mSwitchChecked;
124    private boolean mShowSwitchDivider;
125    private CompoundButton.OnCheckedChangeListener mSwitchOnCheckedChangeListener;
126
127    private String mAction1Text;
128    private View.OnClickListener mAction1OnClickListener;
129    private boolean mShowAction1Divider;
130    private String mAction2Text;
131    private View.OnClickListener mAction2OnClickListener;
132    private boolean mShowAction2Divider;
133
134    /**
135     * Creates a {@link TextListItem.ViewHolder}.
136     */
137    public static ViewHolder createViewHolder(View itemView) {
138        return new ViewHolder(itemView);
139    }
140
141    public TextListItem(Context context) {
142        mContext = context;
143        markDirty();
144    }
145
146    /**
147     * Used by {@link ListItemAdapter} to choose layout to inflate for view holder.
148     */
149    @Override
150    public int getViewType() {
151        return ListItemAdapter.LIST_ITEM_TYPE_TEXT;
152    }
153
154    /**
155     * Calculates the layout params for views in {@link ViewHolder}.
156     */
157    @Override
158    protected void resolveDirtyState() {
159        mBinders.clear();
160
161        // Create binders that adjust layout params of each view.
162        setItemLayoutHeight();
163        setPrimaryAction();
164        setText();
165        setSupplementalActions();
166        setOnClickListener();
167    }
168
169    /**
170     * Hides all views in {@link ViewHolder} then applies ViewBinders to adjust view layout params.
171     */
172    @Override
173    public void onBind(ViewHolder viewHolder) {
174        hideSubViews(viewHolder);
175        for (ViewBinder binder : mBinders) {
176            binder.bind(viewHolder);
177        }
178    }
179
180    /** Sets the title text appearance from the specified style resource. */
181    @Override
182    void setTitleTextAppearance(@StyleRes int titleTextAppearance) {
183        super.setTitleTextAppearance(titleTextAppearance);
184        setTextContent();
185    }
186
187    /** Sets the body text appearance from the specified style resource. */
188    @Override
189    void setBodyTextAppearance(@StyleRes int bodyTextAppearance) {
190        super.setBodyTextAppearance(bodyTextAppearance);
191        setTextContent();
192    }
193
194    private void hideSubViews(ViewHolder vh) {
195        View[] subviews = new View[] {
196                vh.getPrimaryIcon(),
197                vh.getTitle(), vh.getBody(),
198                vh.getSupplementalIcon(), vh.getSupplementalIconDivider(),
199                vh.getSwitch(), vh.getSwitchDivider(),
200                vh.getAction1(), vh.getAction1Divider(), vh.getAction2(), vh.getAction2Divider()};
201        for (View v : subviews) {
202            v.setVisibility(View.GONE);
203        }
204    }
205
206    /**
207     * Sets the height of item depending on which text field is set.
208     */
209    private void setItemLayoutHeight() {
210        if (TextUtils.isEmpty(mBody)) {
211            // If the item only has title or no text, it uses fixed-height as single line.
212            int height = mContext.getResources().getDimensionPixelSize(
213                     R.dimen.car_single_line_list_item_height);
214            mBinders.add(vh -> {
215                ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
216                layoutParams.height = height;
217                vh.itemView.requestLayout();
218            });
219        } else {
220            // If body is present, the item should be at least as tall as min height, and wraps
221            // content.
222            int minHeight = mContext.getResources().getDimensionPixelSize(
223                        R.dimen.car_double_line_list_item_height);
224            mBinders.add(vh -> {
225                vh.itemView.setMinimumHeight(minHeight);
226                vh.getContainerLayout().setMinimumHeight(minHeight);
227
228                ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
229                layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT;
230                vh.itemView.requestLayout();
231            });
232        }
233    }
234
235    private void setPrimaryAction() {
236        setPrimaryIconContent();
237        setPrimaryIconLayout();
238    }
239
240    private void setText() {
241        setTextContent();
242        setTextVerticalMargin();
243        // Only set start margin because text end is relative to the start of supplemental actions.
244        setTextStartMargin();
245        setTextEndLayout();
246    }
247
248    private void setOnClickListener() {
249        mBinders.add(vh -> vh.itemView.setOnClickListener(mOnClickListener));
250    }
251
252    private void setPrimaryIconContent() {
253        switch (mPrimaryActionType) {
254            case PRIMARY_ACTION_TYPE_SMALL_ICON:
255            case PRIMARY_ACTION_TYPE_LARGE_ICON:
256                mBinders.add(vh -> {
257                    vh.getPrimaryIcon().setVisibility(View.VISIBLE);
258
259                    vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
260                });
261                break;
262            case PRIMARY_ACTION_TYPE_EMPTY_ICON:
263            case PRIMARY_ACTION_TYPE_NO_ICON:
264                // Do nothing.
265                break;
266            default:
267                throw new IllegalStateException("Unknown primary action type.");
268        }
269    }
270
271    /**
272     * Sets layout params of primary icon.
273     *
274     * <p>Large icon will have no start margin, and always align center vertically.
275     *
276     * <p>Small icon will have start margin. When body text is present small icon uses a top
277     * margin otherwise align center vertically.
278     */
279    private void setPrimaryIconLayout() {
280        // Set all relevant fields in layout params to avoid carried over params when the item
281        // gets bound to a recycled view holder.
282        switch (mPrimaryActionType) {
283            case PRIMARY_ACTION_TYPE_SMALL_ICON:
284                mBinders.add(vh -> {
285                    int iconSize = mContext.getResources().getDimensionPixelSize(
286                            R.dimen.car_primary_icon_size);
287                    // Icon size.
288                    RelativeLayout.LayoutParams layoutParams =
289                            (RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams();
290                    layoutParams.height = layoutParams.width = iconSize;
291
292                    // Start margin.
293                    layoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize(
294                                            R.dimen.car_keyline_1));
295
296                    if (!TextUtils.isEmpty(mBody)) {
297                        // Set icon top margin so that the icon remains in the same position it
298                        // would've been in for non-long-text item, namely so that the center
299                        // line of icon matches that of line item.
300                        layoutParams.removeRule(RelativeLayout.CENTER_VERTICAL);
301                        int itemHeight = mContext.getResources().getDimensionPixelSize(
302                                     R.dimen.car_double_line_list_item_height);
303                        layoutParams.topMargin = (itemHeight - iconSize) / 2;
304                    } else {
305                        // If the icon can be centered vertically, leave the work for framework.
306                        layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
307                        layoutParams.topMargin = 0;
308                    }
309                    vh.getPrimaryIcon().requestLayout();
310                });
311                break;
312            case PRIMARY_ACTION_TYPE_LARGE_ICON:
313                mBinders.add(vh -> {
314                    int iconSize = mContext.getResources().getDimensionPixelSize(
315                               R.dimen.car_single_line_list_item_height);
316                    // Icon size.
317                    RelativeLayout.LayoutParams layoutParams =
318                            (RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams();
319                    layoutParams.height = layoutParams.width = iconSize;
320
321                    // No start margin.
322                    layoutParams.setMarginStart(0);
323
324                    // Always centered vertically.
325                    layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
326                    layoutParams.topMargin = 0;
327
328                    vh.getPrimaryIcon().requestLayout();
329                });
330                break;
331            case PRIMARY_ACTION_TYPE_EMPTY_ICON:
332            case PRIMARY_ACTION_TYPE_NO_ICON:
333                // Do nothing.
334                break;
335            default:
336                throw new IllegalStateException("Unknown primary action type.");
337        }
338    }
339
340    private void setTextContent() {
341        if (!TextUtils.isEmpty(mTitle)) {
342            mBinders.add(vh -> {
343                vh.getTitle().setVisibility(View.VISIBLE);
344                vh.getTitle().setText(mTitle);
345            });
346        }
347        if (!TextUtils.isEmpty(mBody)) {
348            mBinders.add(vh -> {
349                vh.getBody().setVisibility(View.VISIBLE);
350                vh.getBody().setText(mBody);
351            });
352        }
353
354        if (mIsBodyPrimary) {
355            mBinders.add(vh -> {
356                vh.getTitle().setTextAppearance(getBodyTextAppearance());
357                vh.getBody().setTextAppearance(getTitleTextAppearance());
358            });
359        } else {
360            mBinders.add(vh -> {
361                vh.getTitle().setTextAppearance(getTitleTextAppearance());
362                vh.getBody().setTextAppearance(getBodyTextAppearance());
363            });
364        }
365    }
366
367    /**
368     * Sets start margin of text view depending on icon type.
369     */
370    private void setTextStartMargin() {
371        final int startMarginResId;
372        switch (mPrimaryActionType) {
373            case PRIMARY_ACTION_TYPE_NO_ICON:
374                startMarginResId = R.dimen.car_keyline_1;
375                break;
376            case PRIMARY_ACTION_TYPE_EMPTY_ICON:
377                startMarginResId = R.dimen.car_keyline_3;
378                break;
379            case PRIMARY_ACTION_TYPE_SMALL_ICON:
380                startMarginResId = R.dimen.car_keyline_3;
381                break;
382            case PRIMARY_ACTION_TYPE_LARGE_ICON:
383                startMarginResId = R.dimen.car_keyline_4;
384                break;
385            default:
386                throw new IllegalStateException("Unknown primary action type.");
387        }
388        int startMargin = mContext.getResources().getDimensionPixelSize(startMarginResId);
389        mBinders.add(vh -> {
390            RelativeLayout.LayoutParams titleLayoutParams =
391                    (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams();
392            titleLayoutParams.setMarginStart(startMargin);
393            vh.getTitle().requestLayout();
394
395            RelativeLayout.LayoutParams bodyLayoutParams =
396                    (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams();
397            bodyLayoutParams.setMarginStart(startMargin);
398            vh.getBody().requestLayout();
399        });
400    }
401
402    /**
403     * Sets top/bottom margins of {@code Title} and {@code Body}.
404     */
405    private void setTextVerticalMargin() {
406        // Set all relevant fields in layout params to avoid carried over params when the item
407        // gets bound to a recycled view holder.
408        if (!TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mBody)) {
409            // Title only - view is aligned center vertically by itself.
410            mBinders.add(vh -> {
411                RelativeLayout.LayoutParams layoutParams =
412                        (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams();
413                layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
414                layoutParams.topMargin = 0;
415                vh.getTitle().requestLayout();
416            });
417        } else if (TextUtils.isEmpty(mTitle) && !TextUtils.isEmpty(mBody)) {
418            mBinders.add(vh -> {
419                // Body uses top and bottom margin.
420                int margin = mContext.getResources().getDimensionPixelSize(
421                         R.dimen.car_padding_3);
422                RelativeLayout.LayoutParams layoutParams =
423                        (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams();
424                layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
425                layoutParams.removeRule(RelativeLayout.BELOW);
426                layoutParams.topMargin = margin;
427                layoutParams.bottomMargin = margin;
428                vh.getBody().requestLayout();
429            });
430        } else {
431            mBinders.add(vh -> {
432                // Title has a top margin
433                Resources resources = mContext.getResources();
434                int padding1 = resources.getDimensionPixelSize(R.dimen.car_padding_1);
435                int padding3 = resources.getDimensionPixelSize(R.dimen.car_padding_3);
436
437                RelativeLayout.LayoutParams titleLayoutParams =
438                        (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams();
439                titleLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL);
440                titleLayoutParams.topMargin = padding3;
441                vh.getTitle().requestLayout();
442                // Body is below title with a margin, and has bottom margin.
443                RelativeLayout.LayoutParams bodyLayoutParams =
444                        (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams();
445                bodyLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL);
446                bodyLayoutParams.addRule(RelativeLayout.BELOW, R.id.title);
447                bodyLayoutParams.topMargin = padding1;
448                bodyLayoutParams.bottomMargin = padding3;
449                vh.getBody().requestLayout();
450            });
451        }
452    }
453
454    /**
455     * Returns the id of the leading (left most in LTR) view of supplemental actions.
456     * The view could be one of the supplemental actions (icon, button, switch), or their divider.
457     * Returns 0 if none is enabled.
458     */
459    @IdRes
460    private int getSupplementalActionLeadingView() {
461        int leadingViewId;
462        switch (mSupplementalActionType) {
463            case SUPPLEMENTAL_ACTION_NO_ACTION:
464                leadingViewId = 0;
465                break;
466            case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON:
467                leadingViewId = mShowSupplementalIconDivider
468                        ? R.id.supplemental_icon_divider : R.id.supplemental_icon;
469                break;
470            case SUPPLEMENTAL_ACTION_ONE_ACTION:
471                leadingViewId = mShowAction1Divider ? R.id.action1_divider : R.id.action1;
472                break;
473            case SUPPLEMENTAL_ACTION_TWO_ACTIONS:
474                leadingViewId = mShowAction2Divider ? R.id.action2_divider : R.id.action2;
475                break;
476            case SUPPLEMENTAL_ACTION_SWITCH:
477                leadingViewId = mShowSwitchDivider ? R.id.switch_divider : R.id.switch_widget;
478                break;
479            default:
480                throw new IllegalStateException("Unknown supplemental action type.");
481        }
482        return leadingViewId;
483    }
484
485    private void setTextEndLayout() {
486        // Figure out which view the text should align to.
487        @IdRes int leadingViewId = getSupplementalActionLeadingView();
488
489        if (leadingViewId == 0) {
490            // There is no supplemental action. Text should align to parent end with KL1 padding.
491            mBinders.add(vh -> {
492                Resources resources = mContext.getResources();
493                int padding = resources.getDimensionPixelSize(R.dimen.car_keyline_1);
494
495                RelativeLayout.LayoutParams titleLayoutParams =
496                        (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams();
497                titleLayoutParams.setMarginEnd(padding);
498                titleLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
499                titleLayoutParams.removeRule(RelativeLayout.START_OF);
500
501                RelativeLayout.LayoutParams bodyLayoutParams =
502                        (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams();
503                bodyLayoutParams.setMarginEnd(padding);
504                bodyLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
505                bodyLayoutParams.removeRule(RelativeLayout.START_OF);
506            });
507        } else {
508            // Text align to start of leading supplemental view with padding.
509            mBinders.add(vh -> {
510                Resources resources = mContext.getResources();
511                int padding = resources.getDimensionPixelSize(R.dimen.car_padding_4);
512
513                RelativeLayout.LayoutParams titleLayoutParams =
514                        (RelativeLayout.LayoutParams) vh.getTitle().getLayoutParams();
515                titleLayoutParams.setMarginEnd(padding);
516                titleLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_END);
517                titleLayoutParams.addRule(RelativeLayout.START_OF, leadingViewId);
518                vh.getTitle().requestLayout();
519
520                RelativeLayout.LayoutParams bodyLayoutParams =
521                        (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams();
522                bodyLayoutParams.setMarginEnd(padding);
523                bodyLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_END);
524                bodyLayoutParams.addRule(RelativeLayout.START_OF, leadingViewId);
525                vh.getBody().requestLayout();
526            });
527        }
528    }
529
530    /**
531     * Sets up view(s) for supplemental action.
532     */
533    private void setSupplementalActions() {
534        switch (mSupplementalActionType) {
535            case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON:
536                mBinders.add(vh -> {
537                    vh.getSupplementalIcon().setVisibility(View.VISIBLE);
538                    if (mShowSupplementalIconDivider) {
539                        vh.getSupplementalIconDivider().setVisibility(View.VISIBLE);
540                    }
541
542                    vh.getSupplementalIcon().setImageDrawable(mSupplementalIconDrawable);
543                    vh.getSupplementalIcon().setOnClickListener(
544                            mSupplementalIconOnClickListener);
545                    vh.getSupplementalIcon().setClickable(
546                            mSupplementalIconOnClickListener != null);
547                });
548                break;
549            case SUPPLEMENTAL_ACTION_TWO_ACTIONS:
550                mBinders.add(vh -> {
551                    vh.getAction2().setVisibility(View.VISIBLE);
552                    if (mShowAction2Divider) {
553                        vh.getAction2Divider().setVisibility(View.VISIBLE);
554                    }
555
556                    vh.getAction2().setText(mAction2Text);
557                    vh.getAction2().setOnClickListener(mAction2OnClickListener);
558                });
559                // Fall through
560            case SUPPLEMENTAL_ACTION_ONE_ACTION:
561                mBinders.add(vh -> {
562                    vh.getAction1().setVisibility(View.VISIBLE);
563                    if (mShowAction1Divider) {
564                        vh.getAction1Divider().setVisibility(View.VISIBLE);
565                    }
566
567                    vh.getAction1().setText(mAction1Text);
568                    vh.getAction1().setOnClickListener(mAction1OnClickListener);
569                });
570                break;
571            case SUPPLEMENTAL_ACTION_NO_ACTION:
572                // Do nothing
573                break;
574            case SUPPLEMENTAL_ACTION_SWITCH:
575                mBinders.add(vh -> {
576                    vh.getSwitch().setVisibility(View.VISIBLE);
577                    vh.getSwitch().setChecked(mSwitchChecked);
578                    vh.getSwitch().setOnCheckedChangeListener(mSwitchOnCheckedChangeListener);
579                    if (mShowSwitchDivider) {
580                        vh.getSwitchDivider().setVisibility(View.VISIBLE);
581                    }
582                });
583                break;
584            default:
585                throw new IllegalStateException("Unknown supplemental action type.");
586        }
587    }
588
589    /**
590     * Sets {@link View.OnClickListener} of {@code TextListItem}.
591     */
592    public void setOnClickListener(View.OnClickListener listener) {
593        mOnClickListener = listener;
594        markDirty();
595    }
596
597    /**
598     * Sets {@code Primary Action} to be represented by an icon.
599     *
600     * @param iconResId the resource identifier of the drawable.
601     * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item.
602     */
603    public void setPrimaryActionIcon(@DrawableRes int iconResId, boolean useLargeIcon) {
604        setPrimaryActionIcon(mContext.getDrawable(iconResId), useLargeIcon);
605    }
606
607    /**
608     * Sets {@code Primary Action} to be represented by an icon.
609     *
610     * @param drawable the Drawable to set, or null to clear the content.
611     * @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item.
612     */
613    public void setPrimaryActionIcon(Drawable drawable, boolean useLargeIcon) {
614        mPrimaryActionType = useLargeIcon
615                ? PRIMARY_ACTION_TYPE_LARGE_ICON
616                : PRIMARY_ACTION_TYPE_SMALL_ICON;
617        mPrimaryActionIconDrawable = drawable;
618
619        markDirty();
620    }
621
622    /**
623     * Sets {@code Primary Action} to be empty icon.
624     *
625     * <p>{@code Text} would have a start margin as if {@code Primary Action} were set to primary
626     * icon.
627     */
628    public void setPrimaryActionEmptyIcon() {
629        mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON;
630        markDirty();
631    }
632
633    /**
634     * Sets {@code Primary Action} to have no icon. Text would align to the start of item.
635     */
636    public void setPrimaryActionNoIcon() {
637        mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
638        markDirty();
639    }
640
641    /**
642     * Sets the title of item.
643     *
644     * <p>Primary text is {@code Title} by default. It can be set by
645     * {@link #setBody(String, boolean)}
646     *
647     * <p>{@code Title} text is limited to one line, and ellipses at the end.
648     *
649     * @param title text to display as title.
650     */
651    public void setTitle(String title) {
652        mTitle = title;
653        markDirty();
654    }
655
656    /**
657     * Sets the body text of item.
658     *
659     * <p>Text beyond length required by regulation will be truncated. Defaults {@code Title}
660     * text as the primary.
661     * @param body text to be displayed.
662     */
663    public void setBody(String body) {
664        setBody(body, false);
665    }
666
667    /**
668     * Sets the body text of item.
669     *
670     * @param body text to be displayed.
671     * @param asPrimary sets {@code Body Text} as primary text of item.
672     */
673    public void setBody(String body, boolean asPrimary) {
674        mBody = body;
675        mIsBodyPrimary = asPrimary;
676
677        markDirty();
678    }
679
680    /**
681     * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
682     *
683     * @param iconResId drawable resource id.
684     * @param showDivider whether to display a vertical bar that separates {@code text} and
685     *                    {@code Supplemental Icon}.
686     */
687    public void setSupplementalIcon(int iconResId, boolean showDivider) {
688        setSupplementalIcon(mContext.getDrawable(iconResId), showDivider, null);
689    }
690
691    /**
692     * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
693     *
694     * @param drawable the Drawable to set, or null to clear the content.
695     * @param showDivider whether to display a vertical bar that separates {@code text} and
696     *                    {@code Supplemental Icon}.
697     */
698    public void setSupplementalIcon(Drawable drawable, boolean showDivider) {
699        setSupplementalIcon(drawable, showDivider, null);
700    }
701
702    /**
703     * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
704     *
705     * @param iconResId drawable resource id.
706     * @param showDivider whether to display a vertical bar that separates {@code text} and
707     *                    {@code Supplemental Icon}.
708     * @param listener the callback that will run when icon is clicked.
709     */
710    public void setSupplementalIcon(int iconResId, boolean showDivider,
711            View.OnClickListener listener) {
712        setSupplementalIcon(mContext.getDrawable(iconResId), showDivider, listener);
713    }
714
715    /**
716     * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
717     *
718     * @param drawable the Drawable to set, or null to clear the content.
719     * @param showDivider whether to display a vertical bar that separates {@code text} and
720     *                    {@code Supplemental Icon}.
721     * @param listener the callback that will run when icon is clicked.
722     */
723    public void setSupplementalIcon(Drawable drawable, boolean showDivider,
724                                    View.OnClickListener listener) {
725        mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON;
726
727        mSupplementalIconDrawable = drawable;
728        mSupplementalIconOnClickListener = listener;
729        mShowSupplementalIconDivider = showDivider;
730        markDirty();
731    }
732
733    /**
734     * Sets {@code Supplemental Action} to be represented by an {@code Action Button}.
735     *
736     * @param text button text to display.
737     * @param showDivider whether to display a vertical bar that separates {@code Text} and
738     *                    {@code Action Button}.
739     * @param listener the callback that will run when action button is clicked.
740     */
741    public void setAction(String text, boolean showDivider, View.OnClickListener listener) {
742        if (TextUtils.isEmpty(text)) {
743            throw new IllegalArgumentException("Action text cannot be empty.");
744        }
745        if (listener == null) {
746            throw new IllegalArgumentException("Action OnClickListener cannot be null.");
747        }
748        mSupplementalActionType = SUPPLEMENTAL_ACTION_ONE_ACTION;
749
750        mAction1Text = text;
751        mAction1OnClickListener = listener;
752        mShowAction1Divider = showDivider;
753
754        markDirty();
755    }
756
757    /**
758     * Sets {@code Supplemental Action} to be represented by two {@code Action Button}s.
759     *
760     * <p>These two action buttons will be aligned towards item end.
761     *
762     * @param action1Text button text to display - this button will be closer to item end.
763     * @param action2Text button text to display.
764     */
765    public void setActions(String action1Text, boolean showAction1Divider,
766            View.OnClickListener action1OnClickListener,
767            String action2Text, boolean showAction2Divider,
768            View.OnClickListener action2OnClickListener) {
769        if (TextUtils.isEmpty(action1Text) || TextUtils.isEmpty(action2Text)) {
770            throw new IllegalArgumentException("Action text cannot be empty.");
771        }
772        if (action1OnClickListener == null || action2OnClickListener == null) {
773            throw new IllegalArgumentException("Action OnClickListener cannot be null.");
774        }
775        mSupplementalActionType = SUPPLEMENTAL_ACTION_TWO_ACTIONS;
776
777        mAction1Text = action1Text;
778        mAction1OnClickListener = action1OnClickListener;
779        mShowAction1Divider = showAction1Divider;
780        mAction2Text = action2Text;
781        mAction2OnClickListener = action2OnClickListener;
782        mShowAction2Divider = showAction2Divider;
783
784        markDirty();
785    }
786
787    /**
788     * Sets {@code Supplemental Action} to be represented by a {@link android.widget.Switch}.
789     *
790     * @param checked initial value for switched.
791     * @param showDivider whether to display a vertical bar between switch and text.
792     * @param listener callback to be invoked when the checked state is markDirty.
793     */
794    public void setSwitch(boolean checked, boolean showDivider,
795            CompoundButton.OnCheckedChangeListener listener) {
796        mSupplementalActionType = SUPPLEMENTAL_ACTION_SWITCH;
797
798        mSwitchChecked = checked;
799        mShowSwitchDivider = showDivider;
800        mSwitchOnCheckedChangeListener = listener;
801
802        markDirty();
803    }
804
805    /**
806     * Sets the state of {@code Switch}. For this method to take effect,
807     * {@link #setSwitch(boolean, boolean, CompoundButton.OnCheckedChangeListener)} must be called
808     * first to set {@code Supplemental Action} as a {@code Switch}.
809     *
810     * @param isChecked sets the "checked/unchecked, namely on/off" state of switch.
811     */
812    public void setSwitchState(boolean isChecked) {
813        mSwitchChecked = isChecked;
814        markDirty();
815    }
816
817    /**
818     * Holds views of TextListItem.
819     */
820    public static class ViewHolder extends ListItem.ViewHolder {
821
822        private RelativeLayout mContainerLayout;
823
824        private ImageView mPrimaryIcon;
825
826        private TextView mTitle;
827        private TextView mBody;
828
829        private View mSupplementalIconDivider;
830        private ImageView mSupplementalIcon;
831
832        private Button mAction1;
833        private View mAction1Divider;
834
835        private Button mAction2;
836        private View mAction2Divider;
837
838        private Switch mSwitch;
839        private View mSwitchDivider;
840
841        public ViewHolder(View itemView) {
842            super(itemView);
843
844            mContainerLayout = itemView.findViewById(R.id.container);
845
846            mPrimaryIcon = itemView.findViewById(R.id.primary_icon);
847
848            mTitle = itemView.findViewById(R.id.title);
849            mBody = itemView.findViewById(R.id.body);
850
851            mSupplementalIcon = itemView.findViewById(R.id.supplemental_icon);
852            mSupplementalIconDivider = itemView.findViewById(R.id.supplemental_icon_divider);
853
854            mSwitch = itemView.findViewById(R.id.switch_widget);
855            mSwitchDivider = itemView.findViewById(R.id.switch_divider);
856
857            mAction1 = itemView.findViewById(R.id.action1);
858            mAction1Divider = itemView.findViewById(R.id.action1_divider);
859            mAction2 = itemView.findViewById(R.id.action2);
860            mAction2Divider = itemView.findViewById(R.id.action2_divider);
861        }
862
863        /**
864         * Update children views to comply with car UX restrictions.
865         *
866         * <p>{@code Body} text might be truncated to meet length limit required by regulation.
867         *
868         * @param restrictions current car UX restrictions.
869         */
870        @Override
871        protected void complyWithUxRestrictions(CarUxRestrictions restrictions) {
872            CarUxRestrictionsUtils.comply(itemView.getContext(), restrictions, getBody());
873        }
874
875        public RelativeLayout getContainerLayout() {
876            return mContainerLayout;
877        }
878
879        public ImageView getPrimaryIcon() {
880            return mPrimaryIcon;
881        }
882
883        public TextView getTitle() {
884            return mTitle;
885        }
886
887        public TextView getBody() {
888            return mBody;
889        }
890
891        public ImageView getSupplementalIcon() {
892            return mSupplementalIcon;
893        }
894
895        public View getSupplementalIconDivider() {
896            return mSupplementalIconDivider;
897        }
898
899        public View getSwitchDivider() {
900            return mSwitchDivider;
901        }
902
903        public Switch getSwitch() {
904            return mSwitch;
905        }
906
907        public Button getAction1() {
908            return mAction1;
909        }
910
911        public View getAction1Divider() {
912            return mAction1Divider;
913        }
914
915        public Button getAction2() {
916            return mAction2;
917        }
918
919        public View getAction2Divider() {
920            return mAction2Divider;
921        }
922    }
923}
924