6

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.

Community
  • 1
  • 1
John Leehey
  • 22,052
  • 8
  • 61
  • 88

3 Answers3

21

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.

Leri
  • 12,367
  • 7
  • 43
  • 60
hackbod
  • 90,665
  • 16
  • 140
  • 154
  • ohhhh yes i see now. So right now, my adapter retrieves its data from a database (which does not have a "clicked" column, nor should it I think). Is best practice to keep the state of currently checked items inside of some sort of collection in the adapter? Also, when I use the `ListView.setItemChecked()` method, does that also check the item in the adapter? – John Leehey Jul 20 '11 at 21:45
  • Do consider using ListView's check state if it does what you want -- it can do either single select or multi-select. It takes care of what you would need to do -- keep track of the state associated with each row id (NOT row index, because those can change if the data in the database changes). You will need to have your UI correctly interact with the list view to show this state by implementing Checkable on the top-level view in the list item. There are samples of this in ApiDemos. – hackbod Jul 21 '11 at 19:54
  • Thanks hackbod, but I think this actually confused me further, mainly because that is exactly what I am doing. My ListView top-level list item view is the checkable entry class that I posted code for above. I've avoided overriding the performclick() method in the past, and used to rely on the ListView.setItemChecked() method on my instance of listview. This did not work, however, and the views would not change their background to that of my state_checked item (even though the other states displayed correctly). Is this a separate issue, or is it related? – John Leehey Jul 21 '11 at 23:23
  • Got it! Turns out I needed the code to add the extra checkable state in my view as well as all the adapter changes. My views knew that they were in the `state_checked` state, they just weren't able to pull the background from the state list drawable. – John Leehey Jul 22 '11 at 20:10
  • Hey John, I am having the exact same problem you were having for this (sorry to dig up an old question). Could you expand a bit on what was required to get it to work properly? – compuguru Apr 11 '12 at 05:00
3

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.

Community
  • 1
  • 1
A. Abiri
  • 10,750
  • 4
  • 30
  • 31
0

When doing something similar with a fairly complex set of ListViews I added an extra list to the adapter and added the position of clicked items to it. getView(...) then inflates / recycles the view and just before it finishes checks the state of the item, and the internal adapter state to decide which background to apply.

I also setup the state list xml file to make the background transparent when currently pressed so the selector is visible, it works a treat.

ScouseChris
  • 4,377
  • 32
  • 38