Search code examples
androidandroid-custom-view

Should I use SurfaceView or View for a frequently updated view (15fps)


I am building an application where I need to display signal traces in real time (think the kind you see on cardiac monitors in hospitals). For my line animation to appear smoothly I'd like the frame rate to be around 15 frames per second (I did some experimenting to come to the conclusion this was the lowest acceptable frame rate). It probably does not matter in the context of this post but I potentially have numerous such view in a ListView (~20 is common with about 5 being displayed each time).

Until now I've been doing this using a custom view, extending the View class. I have a thread in the containing fragment that calls invalidate() on the view every ~70ms that redraws the view. This is not causing problems per se as I've optimized my onDraw() function to run in under 2ms most of the time.

Now I have added a Spinner to the fragment and while debugging it I noticed that once I opened the Spinner the adapter was constantly hitting getView() calls, even though I was not touching the Spinner (i.e. open but not scrolling) and also lagging a bit. This led me to realize that my whole fragment was being redrawn every ~70ms which to me sounds bad. Now the questions:

  • Is there a way to trigger onDraw() on a child view without it causing a redraw of the complete hierachy?
  • Should I just resort to a SurfaceView? (that would not cause a complete view hierarchy redraw, right?)
    • I've noticed that the SurfaceView is not HW accelerated. Does that matter if I'm only using basic draw functions (drawLine() and drawText())?
    • Would GLSurfaceView be any better in my case?

Solution

  • I'll try to answer my question so that it might be useful to other people who might encounter the same.

    Containing a bunch of SurfaceViews inside a ListView is a fail. It seems to me since the SurfaceView drawing is not synced with the rest of the UI you will get black lines flickering between views when you scroll (which is probably since the SurfaceView is reassigned to new data source via getView() and displayed before it gets a chance to redraw itself). Triggering a redraw inside getView() was not enough, apparently.

    Delyan's first comment to my question was valid even though it might not always apply. If I go through each view in the ListView by hand and call invalidate() on it the redraw will not bubble up through the hierarchy (I did not need the invalidate(l,t,r,b) signature, but it's probably a smart idea to try it out if you're having problems with excessive redraws). So, even though Romain Guy mentions in his talk that invalidate() will bubble up to the root it's apparently not always the case. Delyan's comment about different implementation of HW/SW might be the reason - I'm rendering my views in HW.

    Anyhow, to work around the issue I created this method inside my adapter:

    public void redrawTraces(ListView lv) {
        final int viewCount = lv.getChildCount();
        for(int i=0; i < viewCount; i++) {
            final View stv = lv.getChildAt(i);
            if(stv != null) {
                stv.invalidate();
            }
        }
    }
    

    Where the views were not SurfaceViews. Finally, to make it clear what I was doing wrong, I was doing this:

    adapter.notifyDataSetChanged();
    

    This call did bubble up and caused a redraw through the entire hierarchy. My assumption was that this was roughly the equivalent of calling listView.invalidate() in terms of updating the whole hierarchy; perhaps that's correct I didn't look into it.