пятница, 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.