Android listSelector все еще частично отображается, когда элемент прокручивается

Изображение показывает проблему: элемент Venus прокручивается, но этот вид видимый.

Элемент Venus прокручивается, но его выбор видимый

Я просмотрел Hanged listSelector в ListView, и я использую фигуры/градиент, как было рекомендовано, но все равно не повезло.

Устройство: эмулятор MS VS для Android, Android 4.2 XHDPI API-17
IDE: Android Studio 2. на Windows 7 WM.

Макет, основная деятельность

<?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>
Селектор списка

<?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>

И код операции

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);
    }
}

где Planets - всего лишь массив строк (Солнце, Меркурий, Венера, Земля, Марс, Юпитер...) и myitem - это всего лишь TextView с пользовательской высотой.

Где я ошибаюсь, пожалуйста?

ИЗМЕНИТЬ Чтобы прояснить вопрос, это касается поведения выбора по умолчанию. Речь идет о элементе выбора списка без атрибутов android:state_xxx. Не обращайте особого внимания на соответствующее имя. Я готов переименовать @drawable/item_selected в @drawable/item_default. Дайте мне знать, если это поможет прояснить проблему, и я переименую ее.

Ответ 1

tl; dr Не устанавливайте по умолчанию Drawable в селекторе списка.

Эта проблема возникает, когда вы предоставляете селектору списка Defaultableable. Под этим я подразумеваю, что в определении селектора списка есть тег item без требования к состоянию, что делает это item непреднамеренно по умолчанию. Подробнее о селекторах здесь.

Код выбора вашего списка:

<?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>

Исправить

Проблема, с которой вы столкнулись, вызвана тем, что селектор списков всегда рисован (даже если селектор не находится на экране). Обычно это не проблема, поскольку селектор списков прозрачен (и, следовательно, невидим). Однако, поскольку вы указали селектор списка фона по умолчанию, это означало, что всякий раз, когда переключатель селектора был на экране, он был бы видимым, что вызывало бы странное поведение, которое вы наблюдали. Вместо этого вы действительно хотите только показать этот фон, когда элемент действительно выбран.

Для этого сначала нужно удалить фон по умолчанию из селектора списков. Затем нам нужен новый способ указания выбранных элементов. Поскольку вы указали android:choiceMode="singleChoice" в своем ListView, ListView будет обрабатывать ваши элементы списка, как список флажков. Таким образом, когда пользователь проверяет один из элементов, это активированное состояние будет установлено в true. Тем не менее, TextViews не будет показывать никаких визуальных эффектов при активации по умолчанию. Чтобы отобразить определенный фон при выборе, нам нужно использовать макет элемента списка, который может отображать активированное состояние. Один из способов сделать это - изменить фон представления элемента ListView на селектор и определить Drawable, который вы хотите использовать для активированного состояния.

Например:

Код адаптера:

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>

Или, если вы ленивы:

ArrayAdapter adapter = ArrayAdapter.createFromResource(this, 
    R.array.planets_array, android.R.layout.simple_list_item_activated_1);

Дальнейшее чтение

Из документации для Android не совсем очевидно, что то, что я сказал, верно, поэтому вы можете спросить, действительно ли мой ответ правдоподобен. Этот раздел посвящен тем, кто ищет надежный ответ и очень любопытно.

Чтобы понять, как работает селектор списков и как он запрограммирован, нам нужно будет погрузиться в исходный код Android. Для начала мясо логики ListView фактически хранится в классе под названием AbsListView (в случае, если у вас нет загруженного источника, вы можете обратиться к это). Копаясь в источнике этого класса, мы найдем несколько полезных полей/функций, относящихся к селектору:

  • mSelector: Это Drawable селектора (тот, который вы указываете с помощью android:listSelector)
  • mSelectorRect: В этом поле определяется, где выполняется селектор, и насколько большой селектор
  • mSelectedPosition: хранит индекс выбранного элемента (это поле фактически объявлено еще глубже в классе AdapterView)
  • positionSelector(...): Обновления, в которых должен быть выведен селектор
  • drawSelector(...): Рисует селектор
  • trackMotionScroll(...): содержит логику поведения прокрутки ListView

Теперь, когда у нас есть понимание среды, мы можем, наконец, понять основную логику для поведения селектора списков. Все это сводится к этим небольшим строкам кода в 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();
    }
    ...
}

Исходный фрагмент выше был отредактирован из оригинала для включения комментариев.

Здесь мы, наконец, находим логику, объясняющую наблюдаемое поведение: селектор списка только скрыт при mSelectorPosition == INVALID_POSITION или, на английском языке, когда нет выбранных элементов. В противном случае он позиционируется в выбранном элементе, если элемент находится на экране, иначе никаких изменений в нем не будет.

Итак, когда вы прокручиваете ListView, и выбранный элемент выключается, селектор списков просто остается в последнем месте, где выбранный элемент объяснял наблюдаемый селектор списка призраков.

Заключительные мысли

От работы с ListViews с момента его введения я должен сказать, что все это не очень хорошо спроектировано, и оно может быть чрезвычайно ошибкой. Я настоятельно рекомендую использовать его преемник RecyclerView, когда вы можете.

Ответ 2

Возможно, вы можете попробовать добавить android: layerType = "software" в ListView.

<ListView
    android:layerType="software"
    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>