I had long wanted to write a tree view demo. Previously, I used TreeViewList (a ListView wrapper) or ExpandableListView for multi-level menus. Eventually I realized there is no need for custom views – just use RecyclerView and manage the data flattening yourself.
The core idea: recursively flatten a nested data structure into a linear list, then use notifyItemRangeInserted / notifyItemRangeRemoved to handle expand and collapse.
Key Techniques#
Convert all data sources into a tree structure with children for easy recursion:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| {
"tree": {
"children": [
{
"available": true,
"children": [],
"id": "548005da36ec3532c4a18391",
"name": "First Round Review"
}
],
"id": "gaozhongshuxue",
"name": "High School Math"
}
}
|
2. Recursion-Friendly Entity#
The entity class must be able to store all its child nodes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public class Course {
@Expose
@SerializedName("id")
public String id;
@Expose
@SerializedName("name")
public String name;
public int level;
public boolean open;
public String parentId;
public LinkedList<Course> children = new LinkedList<>();
public boolean hasChild() {
return children != null && children.size() > 0;
}
public void addChildren(LinkedList<Course> children) {
this.children.clear();
this.children.addAll(children);
}
}
|
3. Recursive Data Construction#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| private void createTree(Course container, JSONArray children,
String parentId, int level) throws JSONException {
if (children != null) {
int size = children.length();
LinkedList<Course> tree = new LinkedList<>();
for (int i = 0; i < size; i++) {
JSONObject item = children.getJSONObject(i);
Course course = new Course();
course.id = item.getString("id");
course.name = item.getString("name");
course.level = level;
course.open = false;
course.parentId = parentId;
JSONArray subChildren = item.getJSONArray("children");
createTree(course, subChildren, course.id, level + 1);
tree.add(course);
if (container == null) {
mData.add(course);
}
}
if (container != null) {
container.addChildren(tree);
}
}
}
|
4. Expand/Collapse on Click#
When an item is clicked, check if it has children, then expand or collapse. The code uses notifyItemRangeInserted / notifyItemRangeRemoved for animation.
Why these methods over notifyDataSetChanged: the range-specific notifications preserve RecyclerView’s default animations, giving users a smooth visual transition during expand/collapse. notifyDataSetChanged would refresh the entire list, losing animations and incurring higher performance cost.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public boolean dispatchClick(LinkedList<Course> container, Course course) {
if (container == null || course == null) {
return false;
}
if (course.hasChild()) {
int insertPosition = container.indexOf(course) + 1;
if (course.open) {
size = 0;
removeAllChildren(container, course);
notifyItemRangeRemoved(insertPosition, size);
} else {
course.open = true;
container.addAll(insertPosition, course.children);
notifyItemRangeInserted(insertPosition, course.children.size());
}
return true;
}
return false;
}
private void removeAllChildren(LinkedList<Course> container, Course course) {
course.open = false;
int childrenSize = course.children.size();
for (Course child : course.children) {
if (child.hasChild() && child.open) {
child.open = false;
removeAllChildren(container, child);
}
}
size += childrenSize;
container.removeAll(course.children);
}
|
5. Custom Animation (Optional)#
Default RecyclerView animations may not show the expand effect clearly. Extend SimpleItemAnimator for a custom look:
1
2
3
4
5
6
7
8
9
| @Override
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
resetAnimation(holder);
ViewCompat.setTranslationY(holder.itemView,
-(holder.itemView.getMeasuredHeight() / 2));
ViewCompat.setAlpha(holder.itemView, 0);
mPendingAdditions.add(holder);
return true;
}
|
Source Code#
Full example: GitHub: Haoxiqiang/TreeView
References#