Added Android code
[wl-app.git] / Android / folioreader / src / main / java / com / folioreader / util / MultiLevelExpIndListAdapter.java
1 package com.folioreader.util;
2
3 import android.support.v7.widget.RecyclerView;
4
5 import com.folioreader.model.TOCLinkWrapper;
6
7 import java.util.ArrayList;
8 import java.util.Collection;
9 import java.util.HashMap;
10 import java.util.List;
11
12 /**
13  * Multi-level expandable indentable list adapter.
14  * Initially all elements in the list are single items. When you want to collapse an item and all its
15  * descendants call {@link #collapseGroup(int)}. When you want to exapand a group call {@link #expandGroup(int)}.
16  * Note that groups inside other groups are kept collapsed.
17  *
18  * To collapse an item and all its descendants or expand a group at a certain position
19  * you can call {@link #toggleGroup(int)}.
20  *
21  * To preserve state (i.e. which items are collapsed) when a configuration change happens (e.g. screen rotation)
22  * you should call {@link #saveGroups()} inside onSaveInstanceState and save the returned value into
23  * the Bundle. When the activity/fragment is recreated you can call {@link #restoreGroups(List)}
24  * to restore the previous state. The actual data (e.g. the comments in the sample app) is not preserved,
25  * so you should save it yourself with a static field or implementing Parcelable or using setRetainInstance(true)
26  * or saving data to a file or something like that.
27  *
28  * To see an example of how to extend this abstract class see MyAdapter.java in sampleapp.
29  */
30 public abstract class MultiLevelExpIndListAdapter extends RecyclerView.Adapter {
31     /**
32      * Indicates whether or not the observers must be notified whenever
33      * {@link #mData} is modified.
34      */
35     private boolean mNotifyOnChange;
36
37     /**
38      * List of items to display.
39      */
40     private List<ExpIndData> mData;
41
42     /**
43      * Map an item to the relative group.
44      * e.g.: if the user click on item 6 then mGroups(item(6)) = {all items/groups below item 6}
45      */
46     private HashMap<ExpIndData, List<? extends ExpIndData>> mGroups;
47
48     /**
49      * Interface that every item to be displayed has to implement. If an object implements
50      * this interface it means that it can be expanded/collapsed and has a level of indentation.
51      * Note: some methods are commented out because they're not used here, but they should be
52      * implemented if you want your data to be expandable/collapsible and indentable.
53      * See MyComment in the sample app to see an example of how to implement this.
54      */
55     public interface ExpIndData {
56         /**
57          * @return The children of this item.
58          */
59         List<? extends ExpIndData> getChildren();
60
61         /**
62          * @return True if this item is a group.
63          */
64         boolean isGroup();
65
66         /**
67          * @param value True if this item is a group
68          */
69         void setIsGroup(boolean value);
70
71         /**
72          * @param groupSize Set the number of items in the group.
73          *                  Note: groups contained in other groups are counted just as one, not
74          *                        as the number of items that they contain.
75          */
76         void setGroupSize(int groupSize);
77
78         /** Note: actually this method is never called in MultiLevelExpIndListAdapter,
79          * that's why it's not strictly required that you implement this function and so
80          * it's commented out.
81          * @return The number of items in the group.
82          *         Note: groups contained in other groups are counted just as one, not
83          *               as the number of items that they contain.
84          */
85         //int getGroupSize();
86
87         /** Note: actually this method is never called in MultiLevelExpIndListAdapter,
88          * that's why it's not strictly required that you implement this function and so
89          * it's commented out.
90          * @return The level of indentation in the range [0, n-1]
91          */
92         //int getIndentation();
93
94         /** Note: actually this method is never called in MultiLevelExpIndListAdapter,
95          * that's why it's not strictly required that you implement this function and so
96          * it's commented out.
97          * @param indentation The level of indentation in the range [0, n-1]
98          */
99         //int setIndentation(int indentation);
100     }
101
102     public MultiLevelExpIndListAdapter() {
103         mData = new ArrayList<ExpIndData>();
104         mGroups = new HashMap<ExpIndData, List<? extends ExpIndData>>();
105         mNotifyOnChange = true;
106     }
107
108     public MultiLevelExpIndListAdapter(ArrayList<TOCLinkWrapper> tocLinkWrappers) {
109         mData = new ArrayList<ExpIndData>();
110         mGroups = new HashMap<ExpIndData, List<? extends ExpIndData>>();
111         mNotifyOnChange = true;
112         mData.addAll(tocLinkWrappers);
113         collapseAllTOCLinks(tocLinkWrappers);
114     }
115
116     public void add(ExpIndData item) {
117         if (item != null) {
118             mData.add(item);
119             if (mNotifyOnChange)
120                 notifyItemChanged(mData.size() - 1);
121         }
122     }
123
124     public void addAll(int position, Collection<? extends ExpIndData> data) {
125         if (data != null && data.size() > 0) {
126             mData.addAll(position, data);
127             if (mNotifyOnChange)
128                 notifyItemRangeInserted(position, data.size());
129         }
130     }
131
132     public void addAll(Collection<? extends ExpIndData> data) {
133         addAll(mData.size(), data);
134     }
135
136     public void insert(int position, ExpIndData item) {
137         mData.add(position, item);
138         if (mNotifyOnChange)
139             notifyItemInserted(position);
140     }
141
142     /**
143      * Clear all items and groups.
144      */
145     public void clear() {
146         if (mData.size() > 0) {
147             int size = mData.size();
148             mData.clear();
149             mGroups.clear();
150             if (mNotifyOnChange)
151                 notifyItemRangeRemoved(0, size);
152         }
153     }
154
155     /**
156      * Remove an item or group.If it's a group it removes also all the
157      * items and groups that it contains.
158      * @param item The item or group to be removed.
159      * @return true if this adapter was modified by this operation, false otherwise.
160      */
161     public boolean remove(ExpIndData item) {
162         return remove(item, false);
163     }
164
165     /**
166      * Remove an item or group. If it's a group it removes also all the
167      * items and groups that it contains if expandGroupBeforeRemoval is false.
168      * If it's true the group is expanded and then only the item is removed.
169      * @param item The item or group to be removed.
170      * @param expandGroupBeforeRemoval True to expand the group before removing the item.
171      *                                 False to remove also all the items and groups contained if
172      *                                 the item to be removed is a group.
173      * @return true if this adapter was modified by this operation, false otherwise.
174      */
175     public boolean remove(ExpIndData item, boolean expandGroupBeforeRemoval) {
176         int index;
177         boolean removed = false;
178         if (item != null && (index = mData.indexOf(item)) != -1 && (removed = mData.remove(item))) {
179             if (mGroups.containsKey(item)) {
180                 if (expandGroupBeforeRemoval)
181                     expandGroup(index);
182                 mGroups.remove(item);
183             }
184             if (mNotifyOnChange)
185                 notifyItemRemoved(index);
186         }
187         return removed;
188     }
189
190     public ExpIndData getItemAt(int position) {
191         return mData.get(position);
192     }
193
194     @Override
195     public int getItemCount() {
196         return mData.size();
197     }
198
199     /**
200      * Expand the group at position "posititon".
201      * @param position The position (range [0,n-1]) of the group that has to be expanded
202      */
203     public void expandGroup(int position) {
204         ExpIndData firstItem = getItemAt(position);
205
206         if (!firstItem.isGroup()) {
207             return;
208         }
209
210         // get the group of the descendants of firstItem
211         List<? extends ExpIndData> group = mGroups.remove(firstItem);
212
213         firstItem.setIsGroup(false);
214         firstItem.setGroupSize(0);
215
216         notifyItemChanged(position);
217         addAll(position + 1, group);
218     }
219
220     /**
221      * Collapse the descendants of the item at position "position".
222      * @param position The position (range [0,n-1]) of the element that has to be collapsed
223      */
224     public void collapseGroup(int position) {
225         ExpIndData firstItem = getItemAt(position);
226
227         if (firstItem.getChildren() == null || firstItem.getChildren().isEmpty())
228             return;
229
230         // group containing all the descendants of firstItem
231         List<ExpIndData> group = new ArrayList<ExpIndData>();
232         // stack for depth first search
233         List<ExpIndData> stack = new ArrayList<ExpIndData>();
234         int groupSize = 0;
235
236         for (int i = firstItem.getChildren().size() - 1; i >= 0; i--) {
237             stack.add(firstItem.getChildren().get(i));
238         }
239
240         while (!stack.isEmpty()) {
241             ExpIndData item = stack.remove(stack.size() - 1);
242             group.add(item);
243             groupSize++;
244             // stop when the item is a leaf or a group
245             if (item.getChildren() != null && !item.getChildren().isEmpty() && !item.isGroup()) {
246                 for (int i = item.getChildren().size() - 1; i >= 0; i--) {
247                     stack.add(item.getChildren().get(i));
248                 }
249             }
250
251             if (mData.contains(item)) mData.remove(item);
252         }
253
254         mGroups.put(firstItem, group);
255         firstItem.setIsGroup(true);
256         firstItem.setGroupSize(groupSize);
257
258         notifyItemChanged(position);
259         notifyItemRangeRemoved(position + 1, groupSize);
260     }
261
262     private void collapseAllTOCLinks(ArrayList<TOCLinkWrapper> tocLinkWrappers){
263         if (tocLinkWrappers == null || tocLinkWrappers.isEmpty()) return;
264
265         for (TOCLinkWrapper tocLinkWrapper:tocLinkWrappers) {
266             groupTOCLink(tocLinkWrapper);
267             collapseAllTOCLinks(tocLinkWrapper.getTocLinkWrappers());
268         }
269     }
270
271     private void groupTOCLink(TOCLinkWrapper tocLinkWrapper){
272         // group containing all the descendants of firstItem
273         List<ExpIndData> group = new ArrayList<ExpIndData>();
274         int groupSize = 0;
275         if (tocLinkWrapper.getChildren()!=null && !tocLinkWrapper.getChildren().isEmpty()) {
276             group.addAll(tocLinkWrapper.getChildren());
277             groupSize = tocLinkWrapper.getChildren().size();
278         }
279         // stack for depth first search
280         //List<ExpIndData> stack = new ArrayList<ExpIndData>();
281         //int groupSize = 0;
282
283         /*for (int i = tocLinkWrapper.getChildren().size() - 1; i >= 0; i--)
284             stack.add(tocLinkWrapper.getChildren().get(i));
285
286         while (!stack.isEmpty()) {
287             ExpIndData item = stack.remove(stack.size() - 1);
288             group.add(item);
289             groupSize++;
290             // stop when the item is a leaf or a group
291             if (item.getChildren() != null && !item.getChildren().isEmpty() && !item.isGroup()) {
292                 for (int i = item.getChildren().size() - 1; i >= 0; i--)
293                     stack.add(item.getChildren().get(i));
294             }
295         }*/
296
297         mGroups.put(tocLinkWrapper, group);
298         tocLinkWrapper.setIsGroup(true);
299         tocLinkWrapper.setGroupSize(groupSize);
300     }
301     /**
302      * Collpase/expand the item at position "position"
303      * @param position The position (range [0,n-1]) of the element that has to be collapsed/expanded
304      */
305     public void toggleGroup(int position) {
306         if (getItemAt(position).isGroup()){
307             expandGroup(position);
308         } else {
309             collapseGroup(position);
310         }
311     }
312
313     /**
314      * In onSaveInstanceState, you should save the groups' indices returned by this function
315      * in the Bundle so that later they can be restored using {@link #restoreGroups(List)}.
316      * saveGroups() expand all the groups so you should call this function only inside onSaveInstanceState.
317      * @return A list of indices of items that are groups.
318      */
319     public ArrayList<Integer> saveGroups() {
320         boolean notify = mNotifyOnChange;
321         mNotifyOnChange = false;
322         ArrayList<Integer> groupsIndices = new ArrayList<Integer>();
323         for (int i = 0; i < mData.size(); i++) {
324             if (mData.get(i).isGroup()) {
325                 expandGroup(i);
326                 groupsIndices.add(i);
327             }
328         }
329         mNotifyOnChange = notify;
330         return groupsIndices;
331     }
332
333     /**
334      * Call this function to restore the groups that were collapsed before the configuration change
335      * happened (e.g. screen rotation). See {@link #saveGroups()}.
336      * @param groupsNum The list of indices of items that are groups and should be collapsed.
337      */
338     public void restoreGroups(List<Integer> groupsNum) {
339         if (groupsNum == null)
340             return;
341         boolean notify = mNotifyOnChange;
342         mNotifyOnChange = false;
343         for (int i = groupsNum.size() - 1; i >= 0; i--) {
344             collapseGroup(groupsNum.get(i));
345         }
346         mNotifyOnChange = notify;
347     }
348 }