Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android listSelector still partly visible when the item is scrolled out

The image shows the problem: Venus item is scrolled out but it's selection is visible.

Venus item is scrolled out but it's selection is visible

I've looked at Hanged listSelector in ListView and I'm using shapes/gradient as it was advised, but still no luck.

Device: MS VS Emulator for Android, Android 4.2 XHDPI API-17
IDE: Android Studio 2. on Windows 7 WM.

Layout, main activity

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:choiceMode="singleChoice"
        android:drawSelectorOnTop="false"
        android:scrollingCache="false"
        android:animationCache="false"
        android:listSelector="@drawable/list_selector">
    </ListView>

</LinearLayout>

list selector

<?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" />
</selector>

drawables, item_pressed

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <gradient
        android:angle="90"
        android:endColor="#0000ff"
        android:startColor="#0000ff" />

</shape>

item_selected

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >
    <gradient
        android:angle="90"
        android:endColor="#00ff00"
        android:startColor="#00ff00" />

</shape>

And activity code

package com.example.listdemo;

import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;

public class MainActivity extends ListActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ArrayAdapter adapter = ArrayAdapter.createFromResource(this, R.array.Planets, R.layout.myitem);
        setListAdapter(adapter);
    }
}

where Planets is just an array of strings (Sun, Mercury, Venus, Earth, Mars, Jupiter...) and myitem is just a TextView with custom height.

Where am I wrong, please?

EDIT To clarify the question, it's about default list selection behavior. That is about list selector item with no android:state_xxx attributes. Don't pay much attention to corresponding drawable name. I'm ready to rename @drawable/item_selected to @drawable/item_default. Let me know if it will help to clarify the problem and I'll rename it.

like image 263
Serg Avatar asked Apr 18 '16 14:04

Serg


1 Answers

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.

like image 96
idunnololz Avatar answered Nov 12 '22 23:11

idunnololz