1 /*
2  * Copyright (C) 2015 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 package android.support.car.ui;
17 
18 import android.content.Context;
19 import android.support.v7.widget.RecyclerView;
20 import android.util.Log;
21 import android.view.ViewGroup;
22 
23 import java.util.ArrayList;
24 import java.util.List;
25 
26 /**
27  * Maintains a list that groups adjacent items sharing the same value of
28  * a "group-by" field.  The list has three types of elements: stand-alone, group header and group
29  * child. Groups are collapsible and collapsed by default.
30  */
31 public abstract class GroupingRecyclerViewAdapter<E, VH extends RecyclerView.ViewHolder>
32         extends RecyclerView.Adapter<VH> {
33     private static final String TAG = "CAR.UI.GroupingRecyclerViewAdapter";
34 
35     public static final int VIEW_TYPE_STANDALONE = 0;
36     public static final int VIEW_TYPE_GROUP_HEADER = 1;
37     public static final int VIEW_TYPE_IN_GROUP = 2;
38 
39     /**
40      * Build all groups based on grouping rules given cursor and calls {@link #addGroup} for
41      * each of them.
42      */
buildGroups(List<E> data)43     protected abstract void buildGroups(List<E> data);
44 
onCreateStandAloneViewHolder(Context context, ViewGroup parent)45     protected abstract VH onCreateStandAloneViewHolder(Context context, ViewGroup parent);
onBindStandAloneViewHolder( VH holder, Context context, int positionInData)46     protected abstract void onBindStandAloneViewHolder(
47             VH holder, Context context, int positionInData);
48 
onCreateGroupViewHolder(Context context, ViewGroup parent)49     protected abstract VH onCreateGroupViewHolder(Context context, ViewGroup parent);
onBindGroupViewHolder(VH holder, Context context, int positionInData, int groupSize, boolean expanded)50     protected abstract void onBindGroupViewHolder(VH holder, Context context, int positionInData,
51                                                   int groupSize, boolean expanded);
52 
onCreateChildViewHolder(Context context, ViewGroup parent)53     protected abstract VH onCreateChildViewHolder(Context context, ViewGroup parent);
onBindChildViewHolder(VH holder, Context context, int positionInData)54     protected abstract void onBindChildViewHolder(VH holder, Context context, int positionInData);
55 
56     protected Context mContext;
57     protected List<E> mData;
58 
59     private int mCount;
60     private List<GroupMetadata> mGroupMetadata;
61 
GroupingRecyclerViewAdapter(Context context)62     public GroupingRecyclerViewAdapter(Context context) {
63         mContext = context;
64         mGroupMetadata = new ArrayList<>();
65         resetGroup();
66     }
67 
setData(List<E> data)68     public void setData(List<E> data) {
69         mData = data;
70         resetGroup();
71         if (mData != null) {
72             buildGroups(mData);
73             rebuildGroupMetadata();
74         }
75         notifyDataSetChanged();
76     }
77 
78     @Override
getItemCount()79     public int getItemCount() {
80         if (mData != null && mCount != -1) {
81             return mCount;
82         }
83         return 0;
84     }
85 
86     @Override
getItemId(int position)87     public long getItemId(int position) {
88         E item = getItem(position);
89         if (item != null) {
90             return item.hashCode();
91         }
92         return 0;
93     }
94 
95     @Override
getItemViewType(int position)96     public int getItemViewType(int position) {
97         return getPositionMetadata(position).itemType;
98     }
99 
getItem(int position)100     public E getItem(int position) {
101         if (mData == null) {
102             return null;
103         }
104 
105         PositionMetadata pMetadata = getPositionMetadata(position);
106         return mData.get(pMetadata.positionInData);
107     }
108 
109     @Override
onCreateViewHolder(ViewGroup parent, int viewType)110     public VH onCreateViewHolder(ViewGroup parent, int viewType) {
111         switch (viewType) {
112             case VIEW_TYPE_STANDALONE:
113                 return onCreateStandAloneViewHolder(mContext, parent);
114             case VIEW_TYPE_GROUP_HEADER:
115                 return onCreateGroupViewHolder(mContext, parent);
116             case VIEW_TYPE_IN_GROUP:
117                 return onCreateChildViewHolder(mContext, parent);
118         }
119         Log.e(TAG, "Unknown viewType. Returning null ViewHolder");
120         return null;
121     }
122 
123     @Override
onBindViewHolder(VH holder, int position)124     public void onBindViewHolder(VH holder, int position) {
125         PositionMetadata pMetadata = getPositionMetadata(position);
126         switch (holder.getItemViewType()) {
127             case VIEW_TYPE_STANDALONE:
128                 onBindStandAloneViewHolder(holder, mContext, pMetadata.positionInData);
129                 break;
130             case VIEW_TYPE_GROUP_HEADER:
131                 onBindGroupViewHolder(holder, mContext, pMetadata.positionInData,
132                         pMetadata.gMetadata.itemNumber, pMetadata.gMetadata.isExpanded());
133                 break;
134             case VIEW_TYPE_IN_GROUP:
135                 onBindChildViewHolder(holder, mContext, pMetadata.positionInData);
136                 break;
137         }
138     }
139 
toggleGroup(int positionInData, int positionOnUI)140     public boolean toggleGroup(int positionInData, int positionOnUI) {
141         PositionMetadata pMetadata = getPositionMetadata(positionInData);
142         if (pMetadata.itemType != VIEW_TYPE_GROUP_HEADER) {
143             return false;
144         }
145 
146         pMetadata.gMetadata.isExpanded = !pMetadata.gMetadata.isExpanded;
147         rebuildGroupMetadata();
148         if (pMetadata.gMetadata.isExpanded) {
149             notifyItemRangeInserted(positionOnUI + 1, pMetadata.gMetadata.itemNumber);
150         } else {
151             notifyItemRangeRemoved(positionOnUI + 1, pMetadata.gMetadata.itemNumber);
152         }
153         return true;
154     }
155 
156     /**
157      * Return True if the item on the given position is a group header and the group is expanded,
158      * otherwise False.
159      */
isGroupExpanded(int position)160     public boolean isGroupExpanded(int position) {
161         PositionMetadata pMetadata = getPositionMetadata(position);
162         if (pMetadata.itemType != VIEW_TYPE_GROUP_HEADER) {
163             return false;
164         }
165 
166         return pMetadata.gMetadata.isExpanded();
167     }
168 
169     /**
170      * Records information about grouping in the list. Should only be called by the overridden
171      * {@link #buildGroups} method.
172      */
addGroup(int offset, int size, boolean expanded)173     protected void addGroup(int offset, int size, boolean expanded) {
174         mGroupMetadata.add(GroupMetadata.obtain(offset, size, expanded));
175     }
176 
resetGroup()177     private void resetGroup() {
178         mCount = -1;
179         mGroupMetadata.clear();
180     }
181 
rebuildGroupMetadata()182     private void rebuildGroupMetadata() {
183         int currentPos = 0;
184         for (int groupIndex = 0; groupIndex < mGroupMetadata.size(); groupIndex++) {
185             GroupMetadata gMetadata = mGroupMetadata.get(groupIndex);
186             gMetadata.offsetInDisplayList = currentPos;
187             currentPos += gMetadata.getActualSize();
188         }
189         mCount = currentPos;
190     }
191 
getPositionMetadata(int position)192     private PositionMetadata getPositionMetadata(int position) {
193         int left = 0;
194         int right = mGroupMetadata.size() - 1;
195         int mid;
196         GroupMetadata midItem;
197 
198         while (left <= right) {
199             mid = (right - left) / 2 + left;
200             midItem = mGroupMetadata.get(mid);
201 
202             if (position > midItem.offsetInDisplayList + midItem.getActualSize() - 1) {
203                 left = mid + 1;
204                 continue;
205             }
206 
207             if (position < midItem.offsetInDisplayList) {
208                 right = mid - 1;
209                 continue;
210             }
211 
212             int cursorOffset = midItem.offsetInDataList + (position - midItem.offsetInDisplayList);
213             int viewType;
214             if (midItem.offsetInDisplayList == position) {
215                 if (midItem.isStandAlone()) {
216                     viewType = VIEW_TYPE_STANDALONE;
217                 } else {
218                     viewType = VIEW_TYPE_GROUP_HEADER;
219                 }
220             } else {
221                 viewType = VIEW_TYPE_IN_GROUP;
222                 // Offset cursorOffset by 1, because the group_header and the first child
223                 // will share the same cursor.
224                 cursorOffset--;
225             }
226             return new PositionMetadata(viewType, cursorOffset, midItem);
227         }
228 
229         throw new IllegalStateException(
230                 "illegal position " + position + ", total size is " + mCount);
231     }
232 
233     /**
234      * Information about where groups are located in the list, how large they are
235      * and whether they are expanded.
236      */
237     protected static class GroupMetadata {
238         private int offsetInDisplayList;
239         private int offsetInDataList;
240         private int itemNumber;
241         private boolean isExpanded;
242 
obtain(int offset, int itemNumber, boolean isExpanded)243         static GroupMetadata obtain(int offset, int itemNumber, boolean isExpanded) {
244             GroupMetadata gm = new GroupMetadata();
245             gm.offsetInDataList = offset;
246             gm.itemNumber = itemNumber;
247             gm.isExpanded = isExpanded;
248             return gm;
249         }
250 
isExpanded()251         public boolean isExpanded() {
252             return !isStandAlone() && isExpanded;
253         }
254 
isStandAlone()255         public boolean isStandAlone() {
256             return itemNumber == 1;
257         }
258 
getActualSize()259         public int getActualSize() {
260             if (!isExpanded()) {
261                 return 1;
262             } else {
263                 return itemNumber + 1;
264             }
265         }
266     }
267 
268     protected static class PositionMetadata {
269         int itemType;
270         int positionInData;
271         GroupMetadata gMetadata;
272 
PositionMetadata(int itemType, int positionInData, GroupMetadata gMetadata)273         public PositionMetadata(int itemType, int positionInData, GroupMetadata gMetadata) {
274             this.itemType = itemType;
275             this.positionInData = positionInData;
276             this.gMetadata = gMetadata;
277         }
278     }
279 }
280