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:
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:
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:
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?
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" />
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;
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With