Friday 16 January 2015

Animated Accordion for Android 4+. Part 1: Static Version


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 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 TextViews, for section headers, and three nested vertical LinearLayouts, 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 LinearLayouts 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