charts-in-listviews3

Back to Blog

Charts in ListViews – Part 1: Displaying Charts Copy

Posted on 21 Jan 2015 Written by Joel Magee

This post is the first in a series describing how to add ShinobiCharts to your ListView in Android. You can check out part 2 and part 3 now.

As you work through the tutorial you might want to take a look at our sample project: you can browse the code on GitHub, or download the zip. You’ll also need a copy of our Android Charts to follow the tutorial – if you don’t have one, then download a trial version.

Finished example

Our aim in this first post is to be able to add static charts to a ListView which isn’t going to prove much of a challenge.

First things first lets create our ListActivity. This is a specific Activity that comes with a default layout that has a ListView, however for this demo I have provided a custom layout so that we can have a visible scrollbar.

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_alignParentBottom="true"
    android:layout_alignParentTop="true"
    android:scrollbarStyle="outsideInset"
    android:fastScrollAlwaysVisible="true" />

Warning: If you are providing a custom layout then your ListView must have the id @android:id/list and the height set to match_parent rather than wrap_content. Using wrap_content will mean that getView will be called multiple times which can produce unexpected behaviour.

If we just wanted a list of strings then we could also use a default layout from the Android platform, however our case is a bit more elaborate so we will need to create another custom xml layout. This layout will have the Views that you want on each row of the list, which in our case is a single ChartView.

Now that we have our xml layouts and ListActivity we need a way of linking them together and for that we use an adapter. Create a custom adapter that extends ArrayAdapterwith a constructor that calls super and assign member variables.

public class ChartArrayAdapter extends ArrayAdapter<DataAdapter>{

    private int rowLayoutId;
    private Context context;
    private List<DataAdapter> values;

    public ChartArrayAdapter(Context context, int row_layout, List<DataAdapter> values) {
        super(context, row_layout, values);
        this.row_layout = row_layout;
        this.context = context;
        this.values = values;
    }

}

Then in our ListActivity we can instantiate our custom ArrayAdapter and add it to the ListActivity.

ChartArrayAdapter adapter = new ChartArrayAdapter(this, R.layout.list_item, dataAdapters);
setListAdapter(adapter);

We have passed the ListActivity context, the resource id of our custom row layout and the data that we will use to populate each row into our Adapter. In our example we have just created mock data which we have put into an ArrayList of DataAdapters, but this could just as easily be data read from a file; i.e. json.

Now that we have setup our ArrayAdapter we need to tell it what to display for each row and for this we need to override the getView method. This method is called by the ListView to get the view for each item in the list and it’s in here that we create our view.

Naive Adapter Implementation

First, I am going to show you a naive implementation which will achieve the desired result, however doing so in an inefficient way. This is to show how a bit of careful design can massively improve performance and to help you avoid many of the common pitfalls surrounding ListViews.

The first step is to inflate the view from our custom row layout using a LayoutInflater which we get from the context given to the constructor. Once the view has been inflated we get hold of the ChartView by using the findViewById method.

Then we get the ShinobiChart from the ChartView and set it up as we normally would; we give the line series a DataAdapter from the ArrayList based on the position parameter of getView. This allocates the right data to the appropriate chart based on our position in the ListView.

In our example we only want to initially show a portion of the data points so the user has to horizontally pan to view the all the data points. For this purpose we call requestCurrentDisplayedRange on the X axis, which has to be called after setting the data adapter. Finally, we return the view that we have inflated from our row layout.

@Override
public View getView(int position, View convertView, ViewGroup parent) {

    LayoutInflater inflater = LayoutInflater.from(context);

    convertView = inflater.inflate(rowLayoutId, parent, false);

    ChartView chart = (ChartView) convertView.findViewById(R.id.chart);

    ShinobiChart shinobiChart = chart.getShinobiChart();
    // insert license if neccessary  - shinobiChart.setLicenseKey("insert license here");

    shinobiChart.setTitle("Chart #" + (position + 1));
    NumberAxis xAxis = createXAxis();
    shinobiChart.setXAxis(xAxis);
    NumberAxis yAxis = new NumberAxis(yAxisRange);
    shinobiChart.setYAxis(yAxis);

    LineSeries lineSeries = new LineSeries();
    shinobiChart.addSeries(lineSeries);

    lineSeries.setDataAdapter(values.get(position));

    xAxis.requestCurrentDisplayedRange(currentRange.getMinimum(), currentRange.getMaximum(), false, false);

    return convertView;
}

At this point you should run the app and see a ListView with a chart shown in each row, just as we were expecting. Unfortunately, when it comes to scrolling through the list we notice poor performance which isn’t ideal.

Optimised Adapter Implementation

The delay in rendering charts while scrolling is obviously an undesirable result and so we need to rectify this. Thankfully there a couple of strategies that we can use to reduce the number of resource intensive calls and improve the fps.

Firstly, we need to acknowledge that getView is going to be called every time a row comes into view, which is going to happen a lot if we are scrolling. This means that we don’t want to be creating too many new objects in here that are going to be cleared up by the garbage collector.

With that in mind we should make the layout inflater a member variable and move the initialisation into the constructor. Now you may have noticed that a View called convertView is passed into getView as one of its parameters. This view is, where possible, a recycled view which has just fallen off the screen. This means that we can reuse this view, rather than inflating a new one from the xml, which should drastically improve performance. This diagram below graphically shows what I was just explaining.

shows how view recycling works

In order to reuse it we guard the layout inflation behind a null checker, so that it only needs to inflate a view from the xml if we have no view to recycle. We can combine this recycling technique with a design pattern called the ViewHolder pattern. The idea is to rather than repeatedly find the views of all the elements within the view hierarchy by using findViewById, which is quite expensive, we use this method once per view and store them in a static nested class within our Adapter class.

public static class ViewHolder {
    ChartView chart;
}

If you tried to implement only those changes then you will notice some unusual behaviour. This is because we are reusing charts and so creating more axes and series than is necessary. To correct this we again guard against their creation with checks to see whether they already exist.

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;

    if (convertView == null) {
        convertView = inflater.inflate(rowLayoutId, parent, false);

        holder = new ViewHolder();
        holder.chart = (ChartView) convertView.findViewById(R.id.chart);

        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }

    ShinobiChart shinobiChart = holder.chart.getShinobiChart();
    // insert license if neccessary  - shinobiChart.setLicenseKey("insert license here");

    shinobiChart.setTitle("Chart #" + (position + 1));
    if(shinobiChart.getXAxis() == null) {
        NumberAxis xAxis = createXAxis();
        shinobiChart.setXAxis(xAxis);
        NumberAxis yAxis = new NumberAxis(yAxisRange);
        shinobiChart.setYAxis(yAxis);
    }

    if (shinobiChart.getSeries().size() == 0) {
        LineSeries lineSeries = new LineSeries();
        shinobiChart.addSeries(lineSeries);
    }

    shinobiChart.getSeries().get(0).setDataAdapter(values.get(position));

    ((NumberAxis) shinobiChart.getXAxis()).requestCurrentDisplayedRange(currentRange.getMinimum(), currentRange.getMaximum(), false, false);

    return convertView;
}

With these changes made you should be able to see that your app runs a lot smoother than it ever did before.

Preserving charts on configuration changes

The final task needed for our demo app is to prevent it from crashing when another app tasks focus. The Open GL rendering views need to know when the Activity is paused and resumed, in order to manage themselves (and their associated threads). To do this we override the onPause and onResume methods and call the equivalent methods on the ChartViews.

In order to access the ChartViews we need to first find the views which hold the charts that are currently visible, because we don’t need to do this for charts that aren’t showing. Then we iterate through those views and get the ViewHolders that we stored in them. Now that we have the ViewHolders we can call the onPause and onResume methods on the ChartViews in them.

@Override
protected void onPause() {
    super.onPause();

    int start = getListView().getFirstVisiblePosition();
    int end = getListView().getLastVisiblePosition();
    ChartArrayAdapter.ViewHolder viewHolder;

    for (int i = start; i <= end; i++) {
        viewHolder = (ChartArrayAdapter.ViewHolder) getListView().getChildAt(i - start).getTag();
        viewHolder.chart.onPause();
    }
}

@Override
protected void onResume() {
    super.onResume();

    int start = getListView().getFirstVisiblePosition();
    int end = getListView().getLastVisiblePosition();
    ChartArrayAdapter.ViewHolder viewHolder;

    for (int i = start; i <= end; i++) {
        viewHolder = (ChartArrayAdapter.ViewHolder) getListView().getChildAt(i - start).getTag();
        viewHolder.chart.onResume();
    }
}

Conclusion

We have managed to populate a list with charts which, thanks to some optimisations, perform well, even while scrolling.

Joel

Back to Blog