четверг, 26 февраля 2015 г.

Checkable ListView. Как добавить CheckBox или RadioButton в ListView

Часто возникает необходимость сделать простой список с чекбоксами вида

При этом хочется выбирать элемент списка кликом по всему элементу, а не только по чекбоксу.

Из коробки класс ListView поддерживает множественный, или единичный выбор - для этого существует XML-атрибут android:choiceMode и соответствующее ему свойство. Мы будем использовать android:choiceMode="multipleChoice".
Однако это свойство работает только с наследниками класса Checkable. CheckBox наследует интерфейс Checkable, однако он спрятан в LinearLayout и ListView его просто "не видит". Поэтому мы сделаем свой лэйаут

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class CheckableLinearLayout extends LinearLayout 
        implements Checkable, ViewGroup.OnHierarchyChangeListener {

    private boolean checked = false;
    private OnHierarchyChangeListener externalHierarchyChangeListener;
    private Checkable checkableChild;

    public CheckableLinearLayout(Context context) {
        super(context);
        init();
    }

    public CheckableLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CheckableLinearLayout(Context context, AttributeSet attrs, 
            int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CheckableLinearLayout(Context context, AttributeSet attrs, 
            int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        super.setOnHierarchyChangeListener(this);
    }

    @Override
    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
        externalHierarchyChangeListener = listener;
    }

    @Override
    public void onChildViewAdded(View parent, View child) {
        if (child instanceof Checkable) {
            checkableChild = (Checkable) child;
        }
        if (externalHierarchyChangeListener != null) {
            externalHierarchyChangeListener.onChildViewAdded(parent, child);
        }
    }

    @Override
    public void onChildViewRemoved(View parent, View child) {
        if (checkableChild != null && child == checkableChild) {
            checkableChild = null;
        }
        if (externalHierarchyChangeListener != null) {
            externalHierarchyChangeListener.onChildViewRemoved(parent, child);
        }
    }

    @Override
    public void toggle() {
        checked = !checked;
        if (checkableChild != null) {
            checkableChild.setChecked(checked);
        }
    }

    @Override
    public void setChecked(boolean checked) {
        this.checked = checked;
        if (checkableChild != null) {
            checkableChild.setChecked(checked);
        }
    }

    @Override
    public boolean isChecked() {
        return checked;
    }

}

Этот LinearLayout при добавлении любого Checkable-элемента становится для него фасадом.

OnHierarchyChangeListener сообщает о всех добавленных и удаленных элементах. С помощью него мы отслеживаем появление Checkable-элемента и сохраняем его во внутренней переменной. Теперь мы можем перенаправить ему все действия, связанные с интерфейсом Checkable.

Этот класс умеет управляться только с одним элементом Checkable, в данном примере нам это и нужно. Однако, если у вас несколько чекбоксов в одном элементе списка, можно просто хранить их в массиве - логика останется та же.

Теперь осталось заменить LinearLayout полученным классом

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<mobi.pawpaw.example.checkablelayout.CheckableLinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?android:attr/listPreferredItemHeight"
    android:gravity="center_vertical"
    android:background="@drawable/list_item_bg">

    <CheckBox
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:clickable="false"
        android:focusable="false"
        android:focusableInTouchMode="false" />

    <TextView
        android:id="@+id/text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textAppearance="?android:textAppearanceLarge"/>

</mobi.pawpaw.example.checkablelayout.CheckableLinearLayout>

У чекбокса необходимо выставить свойства clickable, focusable и focusableInTouchMode в false, чтобы он не перехватывал нажатия на элемент списка.

Готово! Теперь можно выбирать элементы списка нажатием в любую область, а не только на чекбокс.


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

RadioButton тоже имплементирует интерфейс Checkable, поэтому её легко можно использовать таким же способом. Только в этом случае нужно будет выставить android:choiceMode="singleChoice" у ListView.