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 
17 package androidx.car.widget;
18 
19 import android.app.Activity;
20 import android.car.drivingstate.CarUxRestrictions;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.os.Bundle;
24 import android.util.SparseArray;
25 import android.util.SparseIntArray;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.FrameLayout;
30 
31 import androidx.annotation.ColorInt;
32 import androidx.annotation.IntDef;
33 import androidx.annotation.LayoutRes;
34 import androidx.annotation.StyleRes;
35 import androidx.car.R;
36 import androidx.car.utils.CarUxRestrictionsHelper;
37 import androidx.car.utils.ListItemBackgroundResolver;
38 import androidx.cardview.widget.CardView;
39 import androidx.recyclerview.widget.RecyclerView;
40 
41 import java.lang.annotation.Retention;
42 import java.lang.annotation.RetentionPolicy;
43 import java.util.function.Function;
44 
45 /**
46  * Adapter for {@link PagedListView} to display {@link ListItem}.
47  *
48  * <ul>
49  *     <li> Implements {@link PagedListView.ItemCap} - defaults to unlimited item count.
50  *     <li> Implements {@link PagedListView.DividerVisibilityManager} - to control dividers after
51  *     individual {@link ListItem}.
52  * </ul>
53  *
54  * <p>To enable support for {@link CarUxRestrictions}, call {@link #start()} in your
55  * {@code Activity}'s {@link android.app.Activity#onCreate(Bundle)}, and {@link #stop()} in
56  * {@link Activity#onStop()}.
57  */
58 public class ListItemAdapter extends
59         RecyclerView.Adapter<ListItem.ViewHolder> implements PagedListView.ItemCap,
60         PagedListView.DividerVisibilityManager {
61 
62     /**
63      * Constant class for background style of items.
64      */
65     public static final class BackgroundStyle {
BackgroundStyle()66         private BackgroundStyle() {}
67 
68         /**
69          * Sets the background color of each item.
70          * Background can be configured by {@link R.styleable#ListItem_listItemBackgroundColor}.
71          */
72         public static final int SOLID = 0;
73         /**
74          * Sets the background color of each item to none (transparent).
75          */
76         public static final int NONE = 1;
77         /**
78          * Sets each item in {@link CardView} with a rounded corner background and shadow.
79          */
80         public static final int CARD = 2;
81         /**
82          * Sets background of each item so the combined list looks like one elongated card, namely
83          * top and bottom item will have rounded corner at only top/bottom side respectively. If
84          * only one item exists, it will have both top and bottom rounded corner.
85          */
86         public static final int PANEL = 3;
87     }
88 
89     @Retention(RetentionPolicy.SOURCE)
90     @IntDef({
91         BackgroundStyle.SOLID,
92         BackgroundStyle.NONE,
93         BackgroundStyle.CARD,
94         BackgroundStyle.PANEL
95     })
96     private @interface ListBackgroundStyle {}
97 
98     static final int LIST_ITEM_TYPE_TEXT = 1;
99     static final int LIST_ITEM_TYPE_SEEKBAR = 2;
100     static final int LIST_ITEM_TYPE_SUBHEADER = 3;
101 
102     private final SparseIntArray mViewHolderLayoutResIds = new SparseIntArray();
103     private final SparseArray<Function<View, ListItem.ViewHolder>> mViewHolderCreator =
104             new SparseArray<>();
105 
106     @ListBackgroundStyle private int mBackgroundStyle;
107 
108     @ColorInt private int mListItemBackgroundColor;
109     @StyleRes private int mListItemTitleTextAppearance;
110     @StyleRes private int mListItemBodyTextAppearance;
111 
112     private final CarUxRestrictionsHelper mUxRestrictionsHelper;
113     private CarUxRestrictions mCurrentUxRestrictions;
114 
115     private Context mContext;
116     private final ListItemProvider mItemProvider;
117 
118     private int mMaxItems = PagedListView.ItemCap.UNLIMITED;
119 
120     /**
121      * Defaults {@link BackgroundStyle} to {@link BackgroundStyle#SOLID}.
122      */
ListItemAdapter(Context context, ListItemProvider itemProvider)123     public ListItemAdapter(Context context, ListItemProvider itemProvider) {
124         this(context, itemProvider, BackgroundStyle.SOLID);
125     }
126 
ListItemAdapter(Context context, ListItemProvider itemProvider, @ListBackgroundStyle int backgroundStyle)127     public ListItemAdapter(Context context, ListItemProvider itemProvider,
128             @ListBackgroundStyle int backgroundStyle) {
129         mContext = context;
130         mItemProvider = itemProvider;
131         mBackgroundStyle = backgroundStyle;
132 
133         registerListItemViewType(LIST_ITEM_TYPE_TEXT,
134                 R.layout.car_list_item_text_content, TextListItem::createViewHolder);
135         registerListItemViewType(LIST_ITEM_TYPE_SEEKBAR,
136                 R.layout.car_list_item_seekbar_content, SeekbarListItem::createViewHolder);
137         registerListItemViewType(LIST_ITEM_TYPE_SUBHEADER,
138                 R.layout.car_list_item_subheader_content, SubheaderListItem::createViewHolder);
139 
140         mUxRestrictionsHelper =
141                 new CarUxRestrictionsHelper(context, carUxRestrictions -> {
142                     mCurrentUxRestrictions = carUxRestrictions;
143                     notifyDataSetChanged();
144                 });
145     }
146 
147     /**
148      * Enables support for {@link CarUxRestrictions}.
149      *
150      * <p>This method can be called from {@code Activity}'s {@link Activity#onStart()}, or at the
151      * time of construction.
152      *
153      * <p>This method must be accompanied with a matching {@link #stop()} to avoid leak.
154      */
start()155     public void start() {
156         mUxRestrictionsHelper.start();
157     }
158 
159     /**
160      * Disables support for {@link CarUxRestrictions}, and frees up resources.
161      *
162      * <p>This method should be called from {@code Activity}'s {@link Activity#onStop()}, or at the
163      * time of this adapter being discarded.
164      */
stop()165     public void stop() {
166         mUxRestrictionsHelper.stop();
167     }
168 
169     /**
170      * Registers a function that returns {@link RecyclerView.ViewHolder}
171      * for its matching view type returned by {@link ListItem#getViewType()}.
172      *
173      * <p>The function will receive a view as {@link RecyclerView.ViewHolder#itemView}. This view
174      * uses background defined by {@link BackgroundStyle}.
175      *
176      * <p>Subclasses of {@link ListItem} in package androidx.car.widget are already registered.
177      *
178      * @param viewType use negative value for custom view type.
179      * @param function function to create ViewHolder for {@code viewType}.
180      */
registerListItemViewType(int viewType, @LayoutRes int layoutResId, Function<View, ListItem.ViewHolder> function)181     public void registerListItemViewType(int viewType, @LayoutRes int layoutResId,
182             Function<View, ListItem.ViewHolder> function) {
183         if (mViewHolderLayoutResIds.get(viewType) != 0
184                 || mViewHolderCreator.get(viewType) != null) {
185             throw new IllegalArgumentException("View type is already registered.");
186         }
187         mViewHolderCreator.put(viewType, function);
188         mViewHolderLayoutResIds.put(viewType, layoutResId);
189     }
190 
191     @Override
onAttachedToRecyclerView(RecyclerView recyclerView)192     public void onAttachedToRecyclerView(RecyclerView recyclerView) {
193         super.onAttachedToRecyclerView(recyclerView);
194         // When attached to the RecyclerView, update the Context so that this ListItemAdapter can
195         // retrieve theme information off that view.
196         mContext = recyclerView.getContext();
197 
198         TypedArray a = mContext.getTheme().obtainStyledAttributes(R.styleable.ListItem);
199 
200         mListItemBackgroundColor = a.getColor(R.styleable.ListItem_listItemBackgroundColor,
201                 mContext.getColor(R.color.car_card));
202         mListItemTitleTextAppearance = a.getResourceId(
203                 R.styleable.ListItem_listItemTitleTextAppearance,
204                 R.style.TextAppearance_Car_Body1);
205         mListItemBodyTextAppearance = a.getResourceId(
206                 R.styleable.ListItem_listItemBodyTextAppearance,
207                 R.style.TextAppearance_Car_Body2);
208         a.recycle();
209     }
210 
211     @Override
onCreateViewHolder(ViewGroup parent, int viewType)212     public ListItem.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
213         if (mViewHolderLayoutResIds.get(viewType) == 0
214                 || mViewHolderCreator.get(viewType) == null) {
215             throw new IllegalArgumentException("Unregistered view type.");
216         }
217 
218         LayoutInflater inflater = LayoutInflater.from(mContext);
219         View itemView = inflater.inflate(mViewHolderLayoutResIds.get(viewType), parent, false);
220 
221         ViewGroup container = createListItemContainer();
222         container.addView(itemView);
223         return mViewHolderCreator.get(viewType).apply(container);
224     }
225 
226     /**
227      * Creates a view with background set by {@link BackgroundStyle}.
228      */
createListItemContainer()229     private ViewGroup createListItemContainer() {
230         ViewGroup container;
231         if (mBackgroundStyle == BackgroundStyle.CARD) {
232             CardView card = new CardView(mContext);
233             RecyclerView.LayoutParams cardLayoutParams = new RecyclerView.LayoutParams(
234                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
235             cardLayoutParams.bottomMargin = mContext.getResources().getDimensionPixelSize(
236                     R.dimen.car_padding_1);
237             card.setLayoutParams(cardLayoutParams);
238             card.setRadius(mContext.getResources().getDimensionPixelSize(R.dimen.car_radius_1));
239             card.setCardBackgroundColor(mListItemBackgroundColor);
240 
241             container = card;
242         } else {
243             FrameLayout frameLayout = new FrameLayout(mContext);
244             frameLayout.setLayoutParams(new RecyclerView.LayoutParams(
245                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
246             // Skip setting background color for NONE.
247             if (mBackgroundStyle != BackgroundStyle.NONE) {
248                 frameLayout.setBackgroundColor(mListItemBackgroundColor);
249             }
250 
251             container = frameLayout;
252         }
253         return container;
254     }
255 
256     @Override
getItemViewType(int position)257     public int getItemViewType(int position) {
258         return mItemProvider.get(position).getViewType();
259     }
260 
261     @Override
onBindViewHolder(ListItem.ViewHolder holder, int position)262     public void onBindViewHolder(ListItem.ViewHolder holder, int position) {
263         if (mBackgroundStyle == BackgroundStyle.PANEL) {
264             ListItemBackgroundResolver.setBackground(
265                     holder.itemView, position, mItemProvider.size());
266         }
267 
268         // Car may not be initialized thus current UXR will not be available.
269         if (mCurrentUxRestrictions != null) {
270             holder.complyWithUxRestrictions(mCurrentUxRestrictions);
271         }
272 
273         ListItem item = mItemProvider.get(position);
274         item.setTitleTextAppearance(mListItemTitleTextAppearance);
275         item.setBodyTextAppearance(mListItemBodyTextAppearance);
276 
277         item.bind(holder);
278     }
279 
280     @Override
getItemCount()281     public int getItemCount() {
282         return mMaxItems == PagedListView.ItemCap.UNLIMITED
283                 ? mItemProvider.size()
284                 : Math.min(mItemProvider.size(), mMaxItems);
285     }
286 
287     @Override
setMaxItems(int maxItems)288     public void setMaxItems(int maxItems) {
289         mMaxItems = maxItems;
290     }
291 
292     @Override
shouldHideDivider(int position)293     public boolean shouldHideDivider(int position) {
294         // By default we should show the divider i.e. return false.
295 
296         // Check if position is within range, and then check the item flag.
297         return position >= 0 && position < getItemCount()
298                 && mItemProvider.get(position).shouldHideDivider();
299     }
300 }
301