1 package com.folioreader.util;
3 import android.support.v7.widget.RecyclerView;
5 import com.folioreader.model.TOCLinkWrapper;
7 import java.util.ArrayList;
8 import java.util.Collection;
9 import java.util.HashMap;
10 import java.util.List;
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.
18 * To collapse an item and all its descendants or expand a group at a certain position
19 * you can call {@link #toggleGroup(int)}.
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.
28 * To see an example of how to extend this abstract class see MyAdapter.java in sampleapp.
30 public abstract class MultiLevelExpIndListAdapter extends RecyclerView.Adapter {
32 * Indicates whether or not the observers must be notified whenever
33 * {@link #mData} is modified.
35 private boolean mNotifyOnChange;
38 * List of items to display.
40 private List<ExpIndData> mData;
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}
46 private HashMap<ExpIndData, List<? extends ExpIndData>> mGroups;
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.
55 public interface ExpIndData {
57 * @return The children of this item.
59 List<? extends ExpIndData> getChildren();
62 * @return True if this item is a group.
67 * @param value True if this item is a group
69 void setIsGroup(boolean value);
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.
76 void setGroupSize(int groupSize);
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
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.
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
90 * @return The level of indentation in the range [0, n-1]
92 //int getIndentation();
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
97 * @param indentation The level of indentation in the range [0, n-1]
99 //int setIndentation(int indentation);
102 public MultiLevelExpIndListAdapter() {
103 mData = new ArrayList<ExpIndData>();
104 mGroups = new HashMap<ExpIndData, List<? extends ExpIndData>>();
105 mNotifyOnChange = true;
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);
116 public void add(ExpIndData item) {
120 notifyItemChanged(mData.size() - 1);
124 public void addAll(int position, Collection<? extends ExpIndData> data) {
125 if (data != null && data.size() > 0) {
126 mData.addAll(position, data);
128 notifyItemRangeInserted(position, data.size());
132 public void addAll(Collection<? extends ExpIndData> data) {
133 addAll(mData.size(), data);
136 public void insert(int position, ExpIndData item) {
137 mData.add(position, item);
139 notifyItemInserted(position);
143 * Clear all items and groups.
145 public void clear() {
146 if (mData.size() > 0) {
147 int size = mData.size();
151 notifyItemRangeRemoved(0, size);
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.
161 public boolean remove(ExpIndData item) {
162 return remove(item, false);
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.
175 public boolean remove(ExpIndData item, boolean expandGroupBeforeRemoval) {
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)
182 mGroups.remove(item);
185 notifyItemRemoved(index);
190 public ExpIndData getItemAt(int position) {
191 return mData.get(position);
195 public int getItemCount() {
200 * Expand the group at position "posititon".
201 * @param position The position (range [0,n-1]) of the group that has to be expanded
203 public void expandGroup(int position) {
204 ExpIndData firstItem = getItemAt(position);
206 if (!firstItem.isGroup()) {
210 // get the group of the descendants of firstItem
211 List<? extends ExpIndData> group = mGroups.remove(firstItem);
213 firstItem.setIsGroup(false);
214 firstItem.setGroupSize(0);
216 notifyItemChanged(position);
217 addAll(position + 1, group);
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
224 public void collapseGroup(int position) {
225 ExpIndData firstItem = getItemAt(position);
227 if (firstItem.getChildren() == null || firstItem.getChildren().isEmpty())
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>();
236 for (int i = firstItem.getChildren().size() - 1; i >= 0; i--) {
237 stack.add(firstItem.getChildren().get(i));
240 while (!stack.isEmpty()) {
241 ExpIndData item = stack.remove(stack.size() - 1);
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));
251 if (mData.contains(item)) mData.remove(item);
254 mGroups.put(firstItem, group);
255 firstItem.setIsGroup(true);
256 firstItem.setGroupSize(groupSize);
258 notifyItemChanged(position);
259 notifyItemRangeRemoved(position + 1, groupSize);
262 private void collapseAllTOCLinks(ArrayList<TOCLinkWrapper> tocLinkWrappers){
263 if (tocLinkWrappers == null || tocLinkWrappers.isEmpty()) return;
265 for (TOCLinkWrapper tocLinkWrapper:tocLinkWrappers) {
266 groupTOCLink(tocLinkWrapper);
267 collapseAllTOCLinks(tocLinkWrapper.getTocLinkWrappers());
271 private void groupTOCLink(TOCLinkWrapper tocLinkWrapper){
272 // group containing all the descendants of firstItem
273 List<ExpIndData> group = new ArrayList<ExpIndData>();
275 if (tocLinkWrapper.getChildren()!=null && !tocLinkWrapper.getChildren().isEmpty()) {
276 group.addAll(tocLinkWrapper.getChildren());
277 groupSize = tocLinkWrapper.getChildren().size();
279 // stack for depth first search
280 //List<ExpIndData> stack = new ArrayList<ExpIndData>();
283 /*for (int i = tocLinkWrapper.getChildren().size() - 1; i >= 0; i--)
284 stack.add(tocLinkWrapper.getChildren().get(i));
286 while (!stack.isEmpty()) {
287 ExpIndData item = stack.remove(stack.size() - 1);
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));
297 mGroups.put(tocLinkWrapper, group);
298 tocLinkWrapper.setIsGroup(true);
299 tocLinkWrapper.setGroupSize(groupSize);
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
305 public void toggleGroup(int position) {
306 if (getItemAt(position).isGroup()){
307 expandGroup(position);
309 collapseGroup(position);
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.
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()) {
326 groupsIndices.add(i);
329 mNotifyOnChange = notify;
330 return groupsIndices;
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.
338 public void restoreGroups(List<Integer> groupsNum) {
339 if (groupsNum == null)
341 boolean notify = mNotifyOnChange;
342 mNotifyOnChange = false;
343 for (int i = groupsNum.size() - 1; i >= 0; i--) {
344 collapseGroup(groupsNum.get(i));
346 mNotifyOnChange = notify;