Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weird behavior of ListView item and state selector background drawable

I am having some very strange ListView behavior when using a StateListDrawable as the background. I've tried to follow the answer to this post, as I wasn't getting the state_checked state to work, but now my ListView is going crazy.

When I click on an item, it doesn't immediately change color to the state_checked item in the selector. After clicking around a bit though, many of the views will suddenly switch to the state_checked background. It's seemingly random.

Here is my state selector xml code:

<?xml version="1.0" encoding="utf-8"?>
<selector
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_pressed="true" >
        <shape>
            <gradient
                android:startColor="@color/grey"
                android:endColor="@color/darkgrey"
                android:angle="270" />
            <stroke
                android:width="0dp"
                android:color="@color/grey05" />
            <corners
                android:radius="0dp" />
            <padding
                android:left="10sp"
                android:top="10sp"
                android:right="10sp"
                android:bottom="10sp" />
        </shape>
    </item>

    <item android:state_focused="true" >
        <shape>
            <gradient
                android:endColor="@color/orange4"
                android:startColor="@color/orange5"
                android:angle="270" />
            <stroke
                android:width="0dp"
                android:color="@color/grey05" />
            <corners
                android:radius="0dp" />
            <padding
                android:left="10sp"
                android:top="10sp"
                android:right="10sp"
                android:bottom="10sp" />
        </shape>
    </item>

    <item android:state_checked="true">
        <shape>
            <gradient
                android:endColor="@color/brown2"
                android:startColor="@color/brown1"
                android:angle="270" />
            <stroke
                android:width="0dp"
                android:color="@color/grey05" />
            <corners
                android:radius="0dp" />
            <padding
                android:left="10sp"
                android:top="10sp"
                android:right="10sp"
                android:bottom="10sp" />
        </shape>
    </item>

    <item android:state_selected="true">
        <shape>
            <gradient
                android:endColor="@color/brown2"
                android:startColor="@color/brown1"
                android:angle="270" />
            <stroke
                android:width="0dp"
                android:color="@color/grey05" />
            <corners
                android:radius="0dp" />
            <padding
                android:left="10sp"
                android:top="10sp"
                android:right="10sp"
                android:bottom="10sp" />
        </shape>
    </item>

    <item>        
        <shape>
            <gradient
                android:startColor="@color/white"
                android:endColor="@color/white2"
                android:angle="270" />
            <stroke
                android:width="0dp"
                android:color="@color/grey05" />
            <corners
                android:radius="0dp" />
            <padding
                android:left="10sp"
                android:top="10sp"
                android:right="10sp"
                android:bottom="10sp" />
        </shape>
    </item>

</selector>

And here is my .java class for my Custom view implementing checkable:

public class Entry extends LinearLayout implements Checkable {

    public Entry(Context context) {
        super(context, null);

        // Inflate this view
        LayoutInflater temp = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        temp.inflate(R.layout.entry, this, true);

        initViews();
    }

    private static final int[] CheckedStateSet = {
        android.R.attr.state_checked
    };

    private void initViews() {
        this.setBackgroundResource(R.drawable.listview_row);
    }

    public boolean isChecked() {
        return _checked;
    }

    public void toggle() {
        _checked = !_checked;
    }

    public void setChecked(boolean checked) {
        _checked = checked;
    }

    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
        if (isChecked()) {
            mergeDrawableStates(drawableState, CheckedStateSet);
        }
        return drawableState;
    }

    @Override
    public boolean performClick() {
        toggle();
        return super.performClick();
    }
}

I've poked around for a few hours trying to figure it out, but unfortunately must concede to asking for help. Can anyone see something wrong with the code above that would cause the ListView to behave strangely on the items? I can post more code as well, if needed.

like image 649
John Leehey Avatar asked Jul 08 '11 21:07

John Leehey


2 Answers

When working with ListView it is very important to always keep in mind that the views are the presentation and the adapter is the data model.

This means that all of your state should be in the adapter (the data model), not in the views.

From what I can tell of your code, you have a view that is showing a check state, and that state is in the view not in the adapter. That is, when the user clicks on that item in the list, the view being used to display its item has its internal checked state changed to toggle what is shown to the user.

But since the view is not the data model, this state you are playing with here is transient and not actually associated with the adapter item being clicked.

The most obvious problem this causes comes in with view recycling. When you scroll through a ListView, when items scroll off the end and new ones appear at the bottom, the views used to display the old items are re-used to display the new ones. This is much more efficient than having to inflate a new item view hierarchy every time a new item is shown.

Because you have your state in the view, when this recycling happens that state in the view gets randomly re-associated with some new item. This can happen in many cases, not just scrolling.

The solution is to put your check state in the adapter, and implement Adapter.getView() to set the checked state of the view based on the state you now have in the adapter. That way whenever a view is recycled (and getView() called to bind the new data row to it), you will update its checked state to correctly follow the new data it is displaying.

like image 87
hackbod Avatar answered Nov 15 '22 06:11

hackbod


I don't think the problem is coming from the code above. I have had this problem before and it had to do with recycling of the views in the listview. This may be the case for you if your list continues off the screen. If this is the case, a good way to fix it is to store the states of the items in a list so you can keep track of them and base their states off the list you created. Take a look at this and this for more information on recycling of the views.

like image 34
A. Abiri Avatar answered Nov 15 '22 08:11

A. Abiri