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