Android - Building Layouts Programmatically - Trees

As with most operating systems, Android provides plenty of handy user interface elements. Unfortunately, tree views are not amongst these. The closest we get is with the ExpandableListView widget that shows a vertical list whereby each item can be expanded to show an extra level of detail. So what are our options? As always, we can build our own or make use of someone else's code and, as always, my preference is to build our own. Here then I'd like to show you one way to build your own that hopefully, won't have you blowing a fuse.

If third-party code is what you want then I've included a few links below, taken from a quick Google search:

The example I'll be going through relies on three classes. One for the hosting Activity, another for the adapter that will generate the necessary widget views, and lastly a class to represent our data tree. The basic principle will be to add or remove indented items, to a ListView, as the user "opens" and "closes" each parent item. For the sake of simplicity, I'm using stock standard checkboxes through which the user can open and close items (see Figure 1).

Figure 1 - Screenshot

The Data

The hierarchical data for this exercise consists of ten unimaginative strings ordered as in Figure 2. Listing 1 details a class called "TreeNode" that will represent the tree structure and I've opted to link nodes to parents rather than linking nodes to children, as is more popular. I find this makes data structures simpler and easier to persist to a database but is really just a personal choice.

Figure 2 - Data Structure

Only four functions are needed to implement the tree view. "Has_Children" (Lines 14-26) is used to determine if the checkbox widget, used to open and close an item, will be visible. "Count_Parents" (Lines 28-37) is used to determine how much indentation is required for each item. "Get_Visible_Nodes" (Lines 39-53) does most of the heavy lifting by returning all the items that should be visible.

Listing 1 - TreeNode Class

1: public class TreeNode 2: { 3: public boolean is_open; 4: public TreeNode parent; 5: public Object data; 6: 7: public TreeNode(TreeNode parent, Object data) 8: { 9: this.is_open = false; 10: this.parent = parent; 11: this.data = data; 12: } 13: 14: public boolean Has_Children(TreeNode[] nodes) 15: { 16: boolean res=false; 17: 18: for (TreeNode n: nodes) 19: if (n.parent == this) 20: { 21: res = true; 22: break; 23: } 24: 25: return res; 26: } 27: 28: public int Count_Parents() 29: { 30: int res=0; 31: TreeNode n; 32: 33: for (n=this; n.parent!=null; n=n.parent) 34: res++; 35: 36: return res; 37: } 38: 39: public static java.util.ArrayList<TreeNode> Get_Visible_Nodes(TreeNode parent, TreeNode[] nodes) 40: { 41: java.util.ArrayList<TreeNode> visible_nodes=null; 42: 43: visible_nodes=new java.util.ArrayList<TreeNode>(); 44: for (TreeNode n: nodes) 45: if (n.parent==parent) 46: { 47: visible_nodes.add(n); 48: if (n.is_open) 49: visible_nodes.addAll(Get_Visible_Nodes(n, nodes)); 50: } 51: 52: return visible_nodes; 53: } 54:}

The Tree View

The tree view is generated via an ArrayAdapter (Listing 2) called "TreeAdapter". As mentioned before I've provided a CheckBox through which the user can open and close items. This keeps the code simple and can easily be replaced with, for example, a custom view or Button.

The TreeAdapter class consists of only two functions. A constructor (Lines 7-13) and a function to generate each item in the ListView called "getView" (Lines 16-53). "getView" basically builds a horizontal LinearLayout within which it places the CheckBox, with a suitable left margin (Line 43), and the item description. For both the CheckBox and Label we need to set the OnClickListener to that provided by the hosting Activity (Lines 29, 37) or we won't be able to process any user interaction since the ListView's OnItemClickListener will not pass on interactions originating from contained controls. Also, we've attached each TreeNode object to both the Checkbox and Label widgets (Lines 27, 38) so we can refer to them in the OnClick event.

Listing 2 - TreeAdapter Class

1: public class TreeAdapter 2: extends android.widget.ArrayAdapter<TreeNode> 3: { 4: public TreeNode[] nodes; 5: public android.view.View.OnClickListener on_click_listener; 6: 7: public TreeAdapter(android.content.Context ctx, TreeNode[] nodes) 8: { 9: super(ctx, 0); 10: 11: this.nodes = nodes; 12: this.addAll(TreeNode.Get_Visible_Nodes(null, this.nodes)); 13: } 14: 15: @Override 16: public android.view.View getView(int idx, android.view.View view, android.view.ViewGroup parent_view) 17: { 18: android.widget.LinearLayout layout; 19: android.widget.CheckBox handle; 20: android.widget.TextView label; 21: android.widget.LinearLayout.LayoutParams layout_params; 22: TreeNode n; 23: 24: n=this.getItem(idx); 25: 26: handle = new android.widget.CheckBox(parent_view.getContext()); 27: handle.setTag(n); 28: handle.setChecked(n.is_open); 29: handle.setOnClickListener(this.on_click_listener); 30: if (!n.Has_Children(this.nodes)) 31: handle.setVisibility(android.view.View.INVISIBLE); 32: 33: label = new android.widget.TextView(parent_view.getContext()); 34: label.setText(n.data.toString()); 35: label.setClickable(true); 36: label.setGravity(android.view.Gravity.CENTER_VERTICAL); 37: label.setOnClickListener(this.on_click_listener); 38: label.setTag(n); 39: label.setPadding(0, 0, 20, 0); 40: 41: layout = new android.widget.LinearLayout(parent_view.getContext()); 42: layout_params = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT); 43: layout_params.leftMargin = n.Count_Parents() * 35; 44: layout_params.topMargin = 0; 45: layout_params.bottomMargin = 0; 46: layout_params.rightMargin = 0; 47: layout_params.gravity = android.view.Gravity.CENTER; 48: layout.addView(handle, layout_params); 49: layout_params = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.FILL_PARENT, android.widget.LinearLayout.LayoutParams.FILL_PARENT); 50: layout.addView(label, layout_params); 51: 52: return layout; 53: } 54:}

Putting It Together

The main Activity (Listing 3) is very simple and requires no XML layout as the single ListView it displays is created dynamically. Besides defining the ListView, the constructor (Lines 9-33) also instantiates an array of TreeNodes to represent the tree data (Line 14-24) and sets the TreeActivity to receive all "on click" events (Line 27). The onClick function (Line 36) has two kinds of user requests to deal with. If the onClick request comes from a CheckBox then we need to update the TreeAdapter to include or exclude the current TreeNode (Lines 44-46). If the onClick request comes from a Label then we simply display a short message to prove the message was received (Line 49).

Listing 3 - TreeActivity Class

1: public class TreeActivity 2: extends android.app.Activity 3: implements android.view.View.OnClickListener 4: { 5: public TreeNode[] nodes; 6: TreeAdapter adapter; 7: 8: @Override 9: public void onCreate(android.os.Bundle savedInstanceState) 10: { 11: super.onCreate(savedInstanceState); 12: android.widget.ListView list_view; 13: 14: nodes=new TreeNode[10]; 15: nodes[0]=new TreeNode(null, "Node 1"); 16: nodes[1]=new TreeNode(null, "Node 2"); 17: nodes[2]=new TreeNode(null, "Node 3"); 18: nodes[3]=new TreeNode(nodes[1], "Node 2.1"); 19: nodes[4]=new TreeNode(nodes[1], "Node 2.2"); 20: nodes[5]=new TreeNode(nodes[1], "Node 2.3"); 21: nodes[6]=new TreeNode(nodes[4], "Node 2.2.1"); 22: nodes[7]=new TreeNode(nodes[4], "Node 2.2.2"); 23: nodes[8]=new TreeNode(nodes[2], "Node 3.1"); 24: nodes[9]=new TreeNode(nodes[2], "Node 3.2"); 25: 26: adapter=new TreeAdapter(this, nodes); 27: adapter.on_click_listener=this; 28: 29: list_view = new android.widget.ListView(this); 30: list_view.setAdapter(adapter); 31: 32: setContentView(list_view); 33: } 34: 35: @Override 36: public void onClick(android.view.View view) 37: { 38: TreeNode n; 39: 40: n=(TreeNode)view.getTag(); 41: 42: if (view instanceof android.widget.CheckBox) 43: { 44: n.is_open = ((android.widget.CheckBox)view).isChecked(); 45: this.adapter.clear(); 46: this.adapter.addAll(TreeNode.Get_Visible_Nodes(null, this.nodes)); 47: } 48: else 49: android.widget.Toast.makeText(this, n.data.toString(), android.widget.Toast.LENGTH_LONG).show(); 50: } 51:}

Homework

There's clearly a lot more one can do to make this code more reusable and adaptable. With better use of inheritance, interfaces, or a custom view we could further abstract the various responsibilities of each class. Also, by using an "ID" and "Parent ID" property in the TreeNode to indicate node relationships we could make this code more database friendly. The intention, however, was not to provide a full implementation for all use cases. Rather, it was to provide a basic mechanism that might inspire others to build their own implementations. My personal approach is to build custom views that hide most of the implementation details and are fairly tightly dependent on particular mid-tier "model" classes. Your approach will, no doubt, differ.

Popular Posts