пятница, 13 марта 2015 г.

OnItemClickListener для RecyclerView

    В Android L впервые появился новый крутой виджет RecyclerView. Разработчики спустились на одну ступеньку вниз по иерархии, решив вместо конкретных однофункциональных виджетов, типа ListView, GridView и т.п. реализовать полноценный фреймворк на замену AdapterView. Это решение позволило одним махом разобраться с множеством проблем, таких как стесненные возможности наследования и кастомизации, плохая производительность из коробки, сложнореализуемые анимации и т.д.

    Однако RecyclerView - это палка о двух концах. Часто нам просто нужен старый добрый ListView, только с новыми удобными фишками от RecyclerView. И тут мы понимаем, что некоторые вещи, которые воспринимались как данность в ListView, нам придется делать руками. Например, RecyclerView не предоставляет выставить обработчик клика по элементу списка, аналогичный ListView#setOnItemClickListener. Ну что ж, сделаем сами.

Как это делается

 
    У RecyclerView есть интерфейс OnItemTouchListener, который является отражением стандартных обработчиков нажатий у любого ViewGroup. Воспользуемся GestureDetector, чтобы понять, что произошел клик. Для определения нажатого элемента используем RecyclerView#findChildViewUnder, для определения позиции RecyclerView#getChildPosition. Теперь соберем все вместе

 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
public abstract class RecyclerClickListener implements RecyclerView.OnItemTouchListener {

    private GestureDetector gestureDetector;
    private GestureDetector.OnGestureListener gestureListener =
            new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return true;
        }
    };

    public RecyclerClickListener(Context context) {
        gestureDetector = new GestureDetector(context, gestureListener);
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        if (gestureDetector.onTouchEvent(e)) {
            View clickedChild = rv.findChildViewUnder(e.getX(), e.getY());
            if (clickedChild != null && !clickedChild.dispatchTouchEvent(e)) {
                int clickedPosition = rv.getChildPosition(clickedChild);
                if (clickedPosition != RecyclerView.NO_POSITION) {
                    onItemClick(rv, clickedChild, clickedPosition);
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    }

    public abstract void onItemClick(RecyclerView recyclerView, View itemView, int position);
}


Важный момент

   
    Осталось прояснить еще один важный момент. Нажатия перехватываются по всей области элемента списка. Но внутри него могут быть кнопки, или другие кликабельные компоненты. Поэтому прежде, чем обрабатывать клик, нужно убедиться, что он не принадлежит другому компоненту. Для этого прокидываем нажатие внутрь элемента списка через ViewGroup#dispatchTouchEvent. Теперь, если клик произошел по кнопке внутри элемента списка, обработчик не будет перехватывать нажатие.

Как это работает


Теперь можно выставлять обработчик кликов почти как раньше

 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
public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final MyAdapter adapter = new MyAdapter();

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(adapter);

        recyclerView.addOnItemTouchListener(new RecyclerClickListener(this) {
            @Override
            public void onItemClick(RecyclerView recyclerView, View itemView, 
                    int position) {

                Toast.makeText(MainActivity.this, adapter.getItem(position), 
                        Toast.LENGTH_SHORT).show();

            }
        });
    }
}

А вот так это выглядит. Иконку сердца я взял на Icons8.




четверг, 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.