Home | History | Annotate | Download | only in ui
      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      */
     43     protected abstract void buildGroups(List<E> data);
     44 
     45     protected abstract VH onCreateStandAloneViewHolder(Context context, ViewGroup parent);
     46     protected abstract void onBindStandAloneViewHolder(
     47             VH holder, Context context, int positionInData);
     48 
     49     protected abstract VH onCreateGroupViewHolder(Context context, ViewGroup parent);
     50     protected abstract void onBindGroupViewHolder(VH holder, Context context, int positionInData,
     51                                                   int groupSize, boolean expanded);
     52 
     53     protected abstract VH onCreateChildViewHolder(Context context, ViewGroup parent);
     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 
     62     public GroupingRecyclerViewAdapter(Context context) {
     63         mContext = context;
     64         mGroupMetadata = new ArrayList<>();
     65         resetGroup();
     66     }
     67 
     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
     79     public int getItemCount() {
     80         if (mData != null && mCount != -1) {
     81             return mCount;
     82         }
     83         return 0;
     84     }
     85 
     86     @Override
     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
     96     public int getItemViewType(int position) {
     97         return getPositionMetadata(position).itemType;
     98     }
     99 
    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
    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
    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 
    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      */
    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      */
    173     protected void addGroup(int offset, int size, boolean expanded) {
    174         mGroupMetadata.add(GroupMetadata.obtain(offset, size, expanded));
    175     }
    176 
    177     private void resetGroup() {
    178         mCount = -1;
    179         mGroupMetadata.clear();
    180     }
    181 
    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 
    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 
    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 
    251         public boolean isExpanded() {
    252             return !isStandAlone() && isExpanded;
    253         }
    254 
    255         public boolean isStandAlone() {
    256             return itemNumber == 1;
    257         }
    258 
    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 
    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