tl;dr Don't set a default Drawable on your list selector.
This problem arises when you give the list selector a default Drawable. By this I mean within your list selector definition, you have an item
tag with no state requirement which makes that item
inadvertently the default Drawable. You can read more about selectors here.
Your list selector code:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/item_pressed" />
<item
android:drawable="@drawable/item_selected" /> <-- this is what I'm referring to
</selector>
The fix
The issue you were having is caused by the list selector always being drawn (even if the selector isn't on screen). Normally this isn't an issue since the list selector is transparent (and thus invisible). However since you gave the list selector a default background, this meant that whenever the list selector was on screen, it would be visible causing the weird behavior you observed. Instead what you really want is to only show this background when an item is actually selected.
To do this, first we must remove the default background from the list selector. Then we need a new way to indicate selected items. Since you specified android:choiceMode="singleChoice"
in your ListView, the ListView will treat your list items like a list of checkboxes. Thus, when the user checks one of the items, it's activated state will be set to true. However, TextViews will not show any visual effects when activated by default. To show a specific background when selected we need to use a list item layout that can display the activated state. One way to do this is to change the background of the ListView item view to a selector and define a Drawable you want to use for the activated state.
For instance:
Adapter code:
ArrayAdapter adapter = ArrayAdapter.createFromResource(this,
R.array.Planets, R.layout.myitem);
myitem.xml:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
style="?android:attr/spinnerItemStyle"
android:singleLine="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/item_background"
android:paddingTop="16dp"
android:paddingBottom="16dp"/>
item_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/item_selected" android:state_activated="true" />
</selector>
Or if you are lazy:
ArrayAdapter adapter = ArrayAdapter.createFromResource(this,
R.array.planets_array, android.R.layout.simple_list_item_activated_1);
Further reading
From Android's documentation it is not entirely obvious that what I said is true, so you might ask if my answer is credible. This section is dedicated to those who seek a credible answer and to the very curious.
To understand how the list selector works and how it is programmed, we will need to dive into the Android source code. To begin, the meat of the logic of the ListView
is actually held in a class called AbsListView
(in case you do not have the source downloaded you can refer to this). Digging into the source of this class we will find a few useful fields/functions pertaining to the selector:
mSelector
: This is the Drawable of the selector (the one you specify with android:listSelector
)
mSelectorRect
: This field determines where the selector is drawn and how big the selector is
mSelectedPosition
: Stores the index of the selected item (this field is actually declared even deeper down in the class AdapterView
)
positionSelector(...)
: Updates where the selector should be drawn
drawSelector(...)
: Draws the selector
trackMotionScroll(...)
: Contains the logic of the ListView
's scrolling behavior
Now that we have a understanding of the environment, we can finally understand the core logic to the list selector's behavior. It all comes down to these few lines of code in trackMotionScroll(...)
:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
...
if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
// if we are not in touch mode and there is a selected item then
// we do a quick check if the selected item is on screen
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
// if the selected item is on screen, we move the selector to
// where the selected item is
positionSelector(mSelectedPosition, getChildAt(childIndex));
}
} else if (mSelectorPosition != INVALID_POSITION) {
// if we are in touch mode and there is a selected item then
// we do a quick check if the selected item is on screen
final int childIndex = mSelectorPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
// if the selected item is on screen, we move the selector to
// where the selected item is
positionSelector(INVALID_POSITION, getChildAt(childIndex));
}
} else {
// otherwise, if nothing is selected, hide the selector (don't draw it)
mSelectorRect.setEmpty();
}
...
}
The source snippet above has been edited from the original to include comments.
It is here where we finally find the logic that explains the behavior observed: The list selector is only hidden when mSelectorPosition == INVALID_POSITION
or, in English, when there are no selected items. Otherwise it is positioned at the selected item if the item is on screen, otherwise no changes are made to it's position.
So when you scroll the ListView
and the selected item goes off screen, the list selector just stays put in the last location the selected item was explaining the ghost list selector observed.
Final thoughts
From working with ListViews since it's introduction, I have to say that the entire thing is not very well designed and it can be extremely buggy. I highly recommend using it's successor, the RecyclerView
, whenever you can.