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