charts-in-listviews3

Back to Blog

Charts in ListViews – Part 2: Panning Multiple Charts

Posted on 9 Feb 2015 Written by Joel Magee

This post is part of a series describing how to add ShinobiCharts to your ListView in Android. You can read this post independently but it might help to read part 1 first if you haven’t already done so. You can also check out 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

In our last post we managed to populate a list view with static charts and we will be using that as a base for this version. Our aim in this post is to be able to sync the panning gestures for each chart so that panning in one chart pans the others.

If you have used gestures with ShinobiCharts before then you will be used to allowing gestures for a single chart. In our example we use the enableGesturePanning method on the X axis and set it to true. In order to apply the same panning action from one chart to another, we have to be able to catch the effects of those gestures before we can apply them to the other charts. To do this we use the OnAxisRangeChangeListener interface.

Listening for changes

We are going to implement this in our ChartArrayAdapter so we change its class declaration to read:

public class ChartArrayAdapter extends ArrayAdapter<DataAdapter> implements ShinobiChart.OnAxisRangeChangeListener {

After doing this our IDE will most likely complain that we need to implement the abstract method onAxisRangeChange so make sure you override that method:

@Override
public void onAxisRangeChange(Axis<?, ?> axis) {

}

The final step is to hook this listener up to the chart. As the ChartArrayAdapter is acting as the listener, we call the following in the getView method where the chart is created:

shinobiChart.setOnAxisRangeChangeListener(this);

Our adapter now listens to axis range changes on the charts, even though it currently doesn’t act on those changes.

Acting on those changes

Now that we are receiving notifications that an axis range has changed we need to devise a strategy for applying that change across all charts. The way we go about this is by capturing the currently displayed range when the axis range changes.

When we have the new range we iterate through the rows in the list that are visible and, for each view, we use the ViewHolder to get the stored chart. Once we have the chart we just apply the captured range by calling the requestCurrentDisplayedRange method on the X axis and this should change the range for all those views.

We haven’t finished yet though because we are going to want to optimise this and remove any potential causes of unexpected behaviour. The first thing we need to be aware of is that the onAxisRangeChange method will be called whenever any range changes. This includes charts that we aren’t moving with a gesture, but are instead programmatically changing the displayed range. This is an obvious redundancy so in order to eliminate calls to this method from those charts that aren’t being manually panned we add a conditional check. This check involves querying the motion state of an axis by using the getMotionState method – the axis from the chart that the user is manipulating will be the only one with the motion state of GESTURE or MOMENTUM.

The next thing to be aware of is that by calling requestCurrentDisplayedRange on all the visible charts, we will include the chart that we are actually panning. This highlights another redundancy that we can guard against by only calling this method on the other charts. These changes look like so:

@Override
public void onAxisRangeChange(Axis<?, ?> axis) {
    if (axis.getMotionState() == Axis.MotionState.GESTURE || axis.getMotionState() == Axis.MotionState.MOMENTUM) {
        this.currentRange = (NumberRange) axis.getCurrentDisplayedRange();

        int start = this.listView.getFirstVisiblePosition();
        int end = this.listView.getLastVisiblePosition();
        ViewHolder holder;
        for (int i = start; i <= end; i++) {
            holder = (ViewHolder) this.listView.getChildAt(i - start).getTag();
            if (holder.chart.getShinobiChart() != axis.getChart()) {
                ((NumberAxis) holder.chart.getShinobiChart().getXAxis()).requestCurrentDisplayedRange(this.currentRange.getMinimum(), this.currentRange.getMaximum(), false, false);
            }
        }
    }
}

Bouncing at limits has been turned off for this demo app, but it is possible to turn it on if you wish. It’s worth noting, however, that if the user pulls a chart past its limit and holds it there, the other charts will return to their default range before the user releases the gesture.

Conclusion

Syncing all our charts together isn’t that hard once you devise and apply a sensible strategy. We have been able to do this, while still maintaining the efficiency of recycling views, by knowing how our strategy can be refined.

Joel

Back to Blog