Custom Expandable ListView – Android
Android has its own expandable listview ready-to-use but if you want to create your own expandable listview, follow this tutorial. All items will be read from a .txt file which contains a tree in form of Json.
Add .txt file to assets folder. If there is no assets folder in your project, you can add it by clicking File>New>Folder>Assets Folder.
The idea is that adding-removing lists to expand a list item. When you click on a list item:
*If item has at least one child and already expanded, then collapse it,
*If item has at least one child and collapsed, then expand it,
*Otherwise, do nothing.
Also, we have navigation buttons to move next item and previous item.
Let’s start with layout. We need a TextView to show current item label, a RecyclerView to list items and two buttons for navigation. Open activity_main.xml.
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorLightGrey" tools:context="com.coffeebreakcodes.customexpandedlist.MainActivity"> <TextView android:id="@+id/textViewLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:background="@color/colorWhite" android:padding="16dp" android:textAlignment="center" android:textSize="18sp" /> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_above="@id/layoutNavHolder" android:layout_below="@+id/textViewLabel" android:layout_marginBottom="2dp" android:scrollbars="vertical" /> <RelativeLayout android:id="@+id/layoutNavHolder" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:background="@color/colorGrey" android:padding="4dp"> <ImageView android:id="@+id/imageViewLeft" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:src="@drawable/ic_keyboard_arrow_left_white_48dp" /> <ImageView android:id="@+id/imageViewRight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:src="@drawable/ic_keyboard_arrow_right_white_48dp" /> </RelativeLayout> </RelativeLayout> |
We also need a layout for list item which has a TextView for item label and an ImageView for “+,-” icons. Create an XML file named item_list.xml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/layoutHolder" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorGrey" android:padding="4dp"> <TextView android:id="@+id/textViewTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:textColor="@color/colorWhite" /> <ImageView android:id="@+id/imageViewIcon" android:layout_width="wrap_content" android:layout_height="16dp" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" /> </RelativeLayout> |
That’s all for layouts. You can define some colors. Before start coding, open app level gradle file and add these two libraries:
1 2 |
compile 'com.android.support:recyclerview-v7:26.+' compile 'com.google.code.gson:gson:2.8.2' |
We will use RecyclerView library for list and Gson library for parsing .txt file.
Item class contains id, title, child list, isExpanded, isSelected and hierarchy fields.
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
public class Item { private String identifier; private String title; private List<Item> item = new ArrayList<>(); private boolean isExpanded = false; //Is this item expanded? private boolean isSelected = false; //Is selected item? private int hierarchy = 0; //Used for deciding indent by rank of item public String getIdentifier() { return identifier; } public void setIdentifier(String identifier) { this.identifier = identifier; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public List<Item> getItemList() { return item; } public void setItemList(List<Item> itemList) { this.item = itemList; } public boolean isExpanded() { return isExpanded; } public void setExpanded(boolean expanded) { isExpanded = expanded; } public int getHierarchy() { return hierarchy; } public void setHierarchy(int hierarchy) { this.hierarchy = hierarchy; } public boolean isSelected() { return isSelected; } public void setSelected(boolean selected) { isSelected = selected; } } |
Creating a constant class may help you to get constant variables easily. We will define navigation constants and indent values in this class.
1 2 3 4 5 6 7 8 9 |
public class Const { public static final int NAV_BASIC = 0; //Click list item public static final int NAV_LEFT = 1; //Left navigation button public static final int NAV_RIGHT = 2; //Right navigation button public static final int MARGIN_START = 16; public static final int MARGIN_INDENT = 48; } |
Now, just think about what we need for listing all items. Create a java class named ItemRWAdapter. We will handle changing of icon and background color.
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
public class ItemRWAdapter extends RecyclerView.Adapter<ItemRWAdapter.ViewHolder> { private MainActivity activity; private List<Item> itemList; public ItemRWAdapter(MainActivity activity, List<Item> itemList) { this.activity = activity; this.itemList = itemList; } @Override public ItemRWAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_list, parent, false); return new ItemRWAdapter.ViewHolder(view); } @Override public void onBindViewHolder(final ItemRWAdapter.ViewHolder holder, final int position) { Item item = itemList.get(position); holder.textViewTitle.setText(item.getTitle()); //Places and changes images of items with children if (item.getItemList().size() > 0) { if (item.isExpanded()) { holder.imageViewIcon.setImageResource(R.drawable.ic_remove_white_48dp); } else { holder.imageViewIcon.setImageResource(R.drawable.ic_add_white_48dp); } } else { holder.imageViewIcon.setVisibility(View.GONE); } //Arranges the indent of rows in order to hierarchy RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.textViewTitle.getLayoutParams(); params.setMargins(Const.MARGIN_START + (item.getHierarchy() * Const.MARGIN_INDENT), 0, 0, 0); holder.textViewTitle.setLayoutParams(params); //Changes background color of rows by selection if (item.isSelected()) { holder.layoutHolder.setBackgroundColor( activity.getResources().getColor(R.color.colorRed)); } else { holder.layoutHolder.setBackgroundColor( activity.getResources().getColor(R.color.colorGrey)); } holder.layoutHolder.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { activity.selectItem(position, Const.NAV_BASIC); } }); } @Override public int getItemCount() { return itemList.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView textViewTitle; ImageView imageViewIcon; RelativeLayout layoutHolder; public ViewHolder(View view) { super(view); textViewTitle = (TextView) view.findViewById(R.id.textViewTitle); imageViewIcon = (ImageView) view.findViewById(R.id.imageViewIcon); layoutHolder = (RelativeLayout) view.findViewById(R.id.layoutHolder); } } } |
Now open MainActivity.java and write the code below.
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
public class MainActivity extends AppCompatActivity { private List<Item> itemList; private Item selectedItem; private int lastSelectedPosition = 0; private boolean isRecentlyClosed = false; //Flag for collapsed list item private RecyclerView recyclerView; private TextView textViewLabel; private ImageView imageViewLeft; private ImageView imageViewRight; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { recyclerView = (RecyclerView) findViewById(R.id.recyclerView); textViewLabel = (TextView) findViewById(R.id.textViewLabel); imageViewLeft = (ImageView) findViewById(R.id.imageViewLeft); imageViewRight = (ImageView) findViewById(R.id.imageViewRight); final Item item = getItemListFromAsset(); if (item != null) { itemList = new ArrayList<>(item.getItemList()); //Initial positioning selectedItem = itemList.get(0); itemList.get(0).setSelected(true); textViewLabel.setText(selectedItem.getTitle()); imageViewLeft.setVisibility(View.GONE); recyclerView.setLayoutManager(new LinearLayoutManager(this)); //Divider for RecyclerView recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL)); recyclerView.setAdapter(new ItemRWAdapter(this, itemList)); imageViewLeft.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (lastSelectedPosition > 0) { selectItem(lastSelectedPosition - 1, Const.NAV_LEFT); } } }); imageViewRight.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (lastSelectedPosition < itemList.size() - 1) { selectItem(lastSelectedPosition + 1, Const.NAV_RIGHT); } } }); } else { AlertDialog.Builder builder = new AlertDialog.Builder(this); final AlertDialog alertDialog = builder.create(); alertDialog.setTitle("Error!"); alertDialog.setMessage("Code: 1108"); alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, "Try Again", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { alertDialog.dismiss(); init(); } }); alertDialog.show(); } } /** * Controls the selected item and updates the interface * * @param position position of selected item (int) * @param navType type of navigation source (int) */ public void selectItem(int position, int navType) { //Checks whether the current selected item has children to expand/collapse if (navType == Const.NAV_RIGHT && itemList.get(position - 1).getItemList().size() > 0 && !itemList.get(position - 1).isExpanded()) { position = position - 1; } else if (navType == Const.NAV_LEFT && itemList.get(position + 1).getItemList().size() > 0 && itemList.get(position + 1).isExpanded()) { position = position + 1; } //Visibility of navigation buttons on top of screen imageViewLeft.setVisibility(position == 0 ? View.GONE : View.VISIBLE); imageViewRight.setVisibility(position == itemList.size() - 1 ? View.GONE : View.VISIBLE); isRecentlyClosed = false; selectedItem = itemList.get(position); textViewLabel.setText(selectedItem.getTitle()); itemList.get(position).setSelected(true); if (position != lastSelectedPosition) { itemList.get(lastSelectedPosition).setSelected(false); } lastSelectedPosition = position; //Delete children of collapsed list item from list if (selectedItem.getItemList().size() > 0) { if (selectedItem.isExpanded() && navType != Const.NAV_RIGHT) { deleteItemChildren(selectedItem, position); } } List<Item> updatedList = new ArrayList<>(); //Add selected item and items till the selected item for (int i = 0; i < position + 1; i++) { updatedList.add(itemList.get(i)); } //Add items if selected item has children if (selectedItem.getItemList().size() > 0 && !selectedItem.isExpanded() && !isRecentlyClosed && navType != Const.NAV_LEFT) { updatedList.get(updatedList.size() - 1).setExpanded(true); for (int i = 0; i < selectedItem.getItemList().size(); i++) { Item item = selectedItem.getItemList().get(i); item.setExpanded(false); item.setHierarchy(selectedItem.getHierarchy() + 1); updatedList.add(item); } } //Add items coming after selected item for (int i = position + 1; i < itemList.size(); i++) { updatedList.add(itemList.get(i)); } itemList = updatedList; recyclerView.setAdapter(new ItemRWAdapter(this, updatedList)); if (navType != Const.NAV_BASIC) { recyclerView.getLayoutManager().scrollToPosition(position); } } /** * Gets JSON data from .txt file and converts to Item object * * @return */ private Item getItemListFromAsset() { String json = ""; JSONObject objTable = null; try { InputStream inputStream = getAssets().open("tree.txt"); int size = inputStream.available(); byte[] buffer = new byte[size]; inputStream.read(buffer); inputStream.close(); json = new String(buffer, "UTF-8"); } catch (IOException ex) { ex.printStackTrace(); Toast.makeText( this, "File Error!", Toast.LENGTH_SHORT).show(); return null; } try { JSONObject obj = new JSONObject(json); objTable = obj.getJSONObject("tableofcontents"); } catch (JSONException e) { e.printStackTrace(); Toast.makeText( this, "FileError!", Toast.LENGTH_SHORT).show(); return null; } return new Gson().fromJson(objTable.toString(), Item.class); } /** * Deletes all related children of selected parent recursively * in n-leveled parent-child list * * @param parentItem selected item * @param position position of selected item */ private void deleteItemChildren(Item parentItem, int position) { for (int i = 0; i < parentItem.getItemList().size(); i++) { Item innerItem = itemList.get(position + 1); itemList.remove(position + 1); if (innerItem.isExpanded() && innerItem.getItemList().size() > 0) { deleteItemChildren(innerItem, position); } } itemList.get(position).setExpanded(false); isRecentlyClosed = true; } } |
You can find this project files on Github. Ask your questions as comment this page.
©Coffee Break Codes – Custom Expandable ListView – Android