In the company where I work, designers strongly favour accordion-like interfaces, so I have to implement them in mobile apps time and again. On iOS, it is all about inserting cells into UITableView or removing them — a process that can become quite tedious, especially when more than one level of accordion is needed. On Android, however, creating an accordion doesn't take much effort.
This is only true for Android 4+. A few years ago, when working on the BP UK app, I did manage to create an animated accordion-like menu working on Android 2.1 and above, but the code was horrible. If you are interested, let me know and I'll show how I solved the problem. Thankfully, these days many Android developers can forget about the older versions, so here I am going to show what it takes to create a simple accordion interface for Android 4+.
This is what a typical accordion looks like:
There are several sections. Tapping on a section header toggles that section: opens if it was closed, exposing the items contained in that section, or closes if it was opened. Typically, if one of the sections was opened, all the others should close; however, your UX guys might wish to have more than one section opened at a time.
There is a standard Android component, ExpandableListView, that offers this kind of UI solution, but, in my opinion, it has some deficiencies:
- Most importantly, it is not animated, changes happen abruptly.
- You are limited to a two-level list.
- Arguably, (I am not an expert in the usage of this particular component but I saw a fellow developer struggling with it) you might not have sufficient control over which section is open and when.
Now let's see how we can achieve a similar, or even better result, without using complex components. All we are going to do is manipulate nested LinearLauouts.
I created an Android Studio project and checked it into a Github repository. I am going to tag that code as we go along.
At first, let's create the simplest possible, static version of the accordion. It can be useful in those cases when the whole structure of the accordion is known at the outset. The key thing here is the layout file, which is
Here is the code that manipulates this layout:
At the moment, you can expand all three sections. Can you change the code so that when one of the sections expands two other collapse?
The more realistic scenario is when the sections are known but their content has to be created dynamically — perhaps because it has to be retrieved from the server. I will show how I am dealing with that scenario in the next post.
I created an Android Studio project and checked it into a Github repository. I am going to tag that code as we go along.
At first, let's create the simplest possible, static version of the accordion. It can be useful in those cases when the whole structure of the accordion is known at the outset. The key thing here is the layout file, which is
activity_main.xml
in the initial project:<ScrollView 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" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:animateLayoutChanges="true" android:orientation="vertical" > <TextView android:id="@+id/header1" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_green_dark" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/section_1" android:textColor="@android:color/white" android:textSize="33sp" /> <LinearLayout android:id="@+id/section1" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone" > <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_11" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_12" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_13" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> </LinearLayout> <TextView android:id="@+id/header2" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_green_dark" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/section_2" android:textColor="@android:color/white" android:textSize="33sp" /> <LinearLayout android:id="@+id/section2" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone" > <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_21" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_22" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_23" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> </LinearLayout> <TextView android:id="@+id/header3" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_green_dark" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/section_3" android:textColor="@android:color/white" android:textSize="33sp" /> <LinearLayout android:id="@+id/section3" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone" > <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_31" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_32" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/white" android:gravity="center_vertical" android:minHeight="?android:attr/listPreferredItemHeight" android:padding="20dp" android:text="@string/item_33" android:textColor="@android:color/holo_green_dark" android:textSize="22sp" /> </LinearLayout> </LinearLayout> </ScrollView>At the root is a
ScollView
, to make sure that the content can scroll when necessary. It contains a vertical LinearLayout
which, in its turn, contains three TextView
s, for section headers, and three nested vertical LinearLayout
s, for section contents. The essential details are that the main LinearLayout
has the animateLayoutChanges
attribute set to true, and that the visibility of all nested LinearLayout
s is set to GONE
.Here is the code that manipulates this layout:
View section1, section2, section3; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); section1 = findViewById(R.id.section1); section2 = findViewById(R.id.section2); section3 = findViewById(R.id.section3); View header1 = findViewById(R.id.header1); header1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (section1.getVisibility() == View.GONE) { section1.setVisibility(View.VISIBLE); } else { section1.setVisibility(View.GONE); } } }); View header2 = findViewById(R.id.header2); header2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (section2.getVisibility() == View.GONE) { section2.setVisibility(View.VISIBLE); } else { section2.setVisibility(View.GONE); } } }); View header3 = findViewById(R.id.header3); header3.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (section3.getVisibility() == View.GONE) { section3.setVisibility(View.VISIBLE); } else { section3.setVisibility(View.GONE); } } }); }The code is so simple that I doubt it needs any comments. Run the app, try tapping on the section headers and see how the sections expand or collapse as appropriate. Here is how it looked on my Nexus 4, with only the first section open:
At the moment, you can expand all three sections. Can you change the code so that when one of the sections expands two other collapse?
The more realistic scenario is when the sections are known but their content has to be created dynamically — perhaps because it has to be retrieved from the server. I will show how I am dealing with that scenario in the next post.
No comments:
Post a Comment