Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting rid of overdraw in a ListView with per-item backgrounds

I'm working with a ListView where the list items have a background resource. I want to get rid of as much overdraw as I can. I am aware of the Performance Case Study post on Romain Guy's blog, but I'm having trouble getting the list view completely optimized. The code for a simplified example is shown at the bottom of this post. The example is basically just the "new Activity" wizard with a list view thrown in. My question builds on this example. Here's a screenshot with and without overdraw markings of the initial, unoptimized case:

Naive case: 2x-3x overdraw for the entire screen

Witness that the page has a grey background (it's a texture in my real project) and the list items have a white background (it's a nine-patch in my real project). The overdraw is dramatic, no part of the screen is drawn just once and list items are drawn thrice before they show a letter of content.

It's easy to get rid of the decor view background in the Window and cut a complete layer of overdraw. If the list contains enough items to fill the entire screen, I can axe the ListView background as well at hit a very good place:

Optimized case: backgrounds gone, almost no overdraw

Unfortunately, this doesn't work when the list has fewer items than fill the whole screen. If I set no background whatsoever, there's nothing beneath the list items. If I set a background on the list view (or any parent), I get full overdraw for all list items that do exist:

Deoptimized case: handles partial lists correctly at the cost of some overdraw

It's the lesser evil, but dissatisfying, because most of the time my list will run off the screen. Is there a reliable way to get a background in the list view after the last item, without introducing overdraw?

Sample code used in this question:

The activity:

//MainActivity.java
public class MainActivity extends Activity {
    private static final String[] DATA = { "Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet" };

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Optimization 1: getWindow().setBackgroundDrawableResource(android.R.color.transparent);
        ListView lv = (ListView) findViewById(android.R.id.list);
        lv.setAdapter(new ArrayAdapter<String>(this, R.layout.activity_main__list_item,
                    android.R.id.text1, data));
    }
}

The layouts:

<!-- layout/activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:background="#FFDDDDDD"
        android:text="Page header information" />
    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#FFDDDDDD"
        android:divider="@null" />
</LinearLayout>

<!-- layout/activity_main__list_item.xml -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android" 
    android:id="@android:id/text1"
    android:layout_width="match_parent" 
    android:layout_height="48dp"
    android:background="@android:color/white" />
like image 723
Barend Avatar asked Mar 25 '13 22:03

Barend


2 Answers

I came up with a solution of my own that avoids the nested weights issue of j__m's answer. On the downside, it cannot be easily combined with other ListView customizations. This may be more efficient than a solution involving WRAP_CONTENT for the list view's height, because the ListView doesn't have to getView(...) its children during measurement.

Of course, the ListView#onMeasure() implementation is smart enough to stop measuring children once it has enough height to fill its parent view and its going to getView() those same positions during onDraw() anyway, so the efficiency argument appears to be moot. Nevertheless, Romain Guy is quite insistent that ListView's shouldn't use WRAP_CONTENT.

/**
 * A list view for use with list items that have their own background drawable. Such list views
 * suffer from GPU Overdraw of the item background on top of the list view (ancestor) background.
 * <p>
 * This subclass detects when its data set contains enough elements to fill all available space
 * and start scrolling. If this is the case, it sets its own background to transparent.
 * </p>
 * <p><strong>Limitation:</strong> Header and Footer views are ignored. If the list view has few
 * enough items that it wouldn't scroll, but a header and/or footer are big enough to cause it to
 * scroll anyway, the background is not hidden and overdraw is present.
 * </p>
 * <p>Source: https://stackoverflow.com/q/15625930/49489 CC-BY-SA</p>
 */
public class BackgroundListView extends ListView {

    /** We need our own instance, because it's used as a sentinel value later on. */
    private static final Drawable TRANSPARENT = new ColorDrawable(0x00000000);

    /** If true, the next call to {@code onDraw(Canvas)} evaluates whether to show or hide the background. */
    private boolean mNeedsBackgroundCheck = true;

    /** The background to restore if the list shrinks. */
    private Drawable mOriginalBackground;

    public BackgroundListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public BackgroundListView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BackgroundListView(Context context) {
        this(context, null, 0);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        this.mOriginalBackground = this.getBackground();
    }

    @Override
    public void setBackground(Drawable background) {
        super.setBackground(background);
        Drawable newBackground = getBackground();
        if (newBackground != TRANSPARENT) {
            this.mOriginalBackground = newBackground;
        }
    }

    @Override
    public void setBackgroundResource(int resid) {
        super.setBackgroundResource(resid);
        Drawable newBackground = getBackground();
        if (newBackground != TRANSPARENT) {
            this.mOriginalBackground = newBackground;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mNeedsBackgroundCheck) {
            maybeHideBackground();
        }
        super.onDraw(canvas);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mNeedsBackgroundCheck = true;
    };

    @Override
    protected void handleDataChanged() {
        super.handleDataChanged();
        mNeedsBackgroundCheck = true;
    }

    private void maybeHideBackground() {
        if (isInEditMode()) {
            return;
        }
        final int maxPosition = getAdapter().getCount() - 1;
        final int firstVisiblePosition = getFirstVisiblePosition();
        final int lastVisiblePosition = getLastVisiblePosition();
        if (firstVisiblePosition > 0 || lastVisiblePosition < maxPosition) {
            setBackground(TRANSPARENT);
        } else {
            setBackground(mOriginalBackground);
        }
        mNeedsBackgroundCheck = false;
    }
}
like image 51
Barend Avatar answered Oct 25 '22 06:10

Barend


Put the ListView in a LinearLayout, set the ListView's height to WRAP_CONTENT, and put another view beneath it with layout_weight="1". Put your background on that view.

like image 22
j__m Avatar answered Oct 25 '22 05:10

j__m