리사이클러뷰 아이템 클릭 이벤트 처리. (RecyclerView Item Click Event)

2019. 6. 11. 11:26


1. 리사이클러뷰(RecyclerView) 아이템 클릭.

지난 글 [안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView)]와 [안드로이드 리사이클러뷰 사용 예제. (Android RecyclerView Example)]에서 리사이클러뷰의 기본 사용법과 예제를 살펴봤는데요, 이제, 리사이클러뷰 아이템 클릭 이벤트를 처리하는 방법에 대해 알아보도록 하겠습니다.


리스트뷰를 사용해 본 경험이 있다면, 리스트뷰의 아이템 클릭 이벤트를 OnItemClickListner 리스너를 통해 처리할 수 있다는 것을 알고 있을 것입니다. [안드로이드 리스트뷰 기본 사용법. (Android ListView) - 2.5 ListView 클릭 이벤트 처리]에서도 설명했듯이, ListViewsetOnItemClickListener()를 사용해 이벤트 리스너를 지정할 수 있죠.


그래서 리사이클러뷰에서도 setOnItemClickListener() 사용과 유사한 방법을 통해 아이템 클릭 이벤트를 처리할 수 있을 것 같은데요. 하지만, [안드로이드 개발 참조문서. RecyclerView] 문서를 아무리 찾아봐도, setOnItemClickListener() 또는 그와 유사한 기능을 제공하는 메서드를 찾을 수가 없습니다.


그렇다면 리사이클러뷰에서는 아이템을 표시만 할 수 있고 아이템 클릭 이벤트는 사용할 수 없는 걸까요? 아니면 아이템 클릭 이벤트를 처리하는, 리스트뷰에서의 아이템 클릭 이벤트 처리 방법과는 다른 방법이 있는 걸까요?

2. 아이템 클릭 이벤트 처리의 주체.

기본적으로 리스트뷰는 유사한 형태와 크기를 가진 아이템뷰를 세로 방향 한 줄로 나열합니다. 이러한 표시 형태의 단순함으로 인해, 화면에 표시된 아이템을 클릭했을 때, 몇 번째 아이템이 클릭되었는지 계산하는 과정이 비교적 간단합니다. 그래서 아이템 클릭 이벤트 처리 기능을 리스트뷰가 직접 제공합니다.


이에 반해 리사이클러뷰는, 리스트뷰에 비해 훨씬 유연하고 다양한 형태로 아이템을 표시하게 만들어 줍니다. [안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView)]에서도 설명했듯이, 레이아웃매니저(LayoutManager)를 통해 아이템을 배치하는 형태를 다양하게 구성할 수 있고, 아직 언급하지는 않았지만, 애니메이션(animation) 효과 등을 손 쉽게 적용해 다이나믹한 화면을 구성할 수 있게 만들어줍니다. 하지만 이러한 장점들이, 아이템 클릭 이벤트 처리를 복잡하게 만드는 요인이 됩니다.


그래서 리사이클러뷰는 아이템 클릭 이벤트 리스너를 자신이 직접 다루지 않고, 아이템 뷰에서 OnClickListener를 통해 처리하게 만들어놓았습니다.

3. 리사이클러뷰 뷰홀더(ViewHolder)에서 아이템 클릭 이벤트 처리하기.

"아이템 뷰에서 OnClickListener를 통해 아이템 클릭 이벤트를 처리한다"는 문장이 어떻게 코드로 매핑되는지 바로 떠오르나요?


잠시, 이전 글 [안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView)]에서 설명했던 내용을 다시 한번 상기시켜 보겠습니다.


어댑터를 통해 만들어진 각 아이템 뷰는 "뷰홀더(ViewHolder)"객체에 저장되어 화면에 표시되고, 필요에 따라 생성 또는 재활용(Recycle)됩니다.


그럼 이제 머리 속에 코드의 형태가 그려지나요?


  • 아이템 뷰에서 클릭 이벤트를 직접 처리하고,
  • 아이템 뷰는 뷰홀더 객체가 가지고 있으니,
  • 아이템 클릭 이벤트는 뷰홀더에서 작성.


아래 코드처럼, 뷰 홀더가 만들어지는 시점에 클릭 이벤트를 처리하면 됩니다.


    public class ViewHolder extends RecyclerView.ViewHolder {

        ViewHolder(View itemView) {
            super(itemView) ;

            // 아이템 클릭 이벤트 처리.
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // TODO : process click event.
                }
            });
        }
    }

4. 아이템 위치(position) 알아내기.

자, 이제 아이템 클릭 이벤트를 처리할 수 있게 되었습니다. onClick() 메서드에서 로그 메시지를 출력하는 코드를 넣어보면, 아이템 클릭 시 메시지가 표시되는 것을 확인할 수 있는데요. 그런데 아직 해야 할 일이 남아 있습니다. 바로, 현재 클릭 이벤트가 발생된 아이템 위치(position)를 알아내는 것이죠.


보통, 앱에서 리사이클러뷰 아이템 클릭 이벤트를 사용할 때는, 단순히 로그 메시지 확인이나 하려고 이벤트 처리 코드를 작성하지 않습니다. 현재 선택된 아이템에 따라 다른 액션을 실행하거나, 아이템과 연결된 데이터를 확인, 수정, 삭제하는 등의 기능을 실행하기 위해 클릭 이벤트를 구현하죠. 그래서 아이템 클릭 이벤트에서 가장 처음으로 해야 할 일은, 현재 클릭 이벤트가 발생한 아이템의 위치를 알아내는 것입니다.


자, 어떻게 자신의 위치를 알아낼 수 있을까요? onClick() 메서드에는 단지 View 객체에 대한 참조만 전달될 뿐, 위치(position)에 대한 정보는 없네요. 그럼 View 객체의 태그(tag)를 통해 위치(position)를 직접 저장하고 관리해야 할까요?


그럴 필요는 없습니다. 이런 경우를 위해, 리사이클러뷰의 뷰홀더(android.support.v7.widget.RecyclerView.ViewHolder)에는 현재 자신의 위치(position)를 확인할 수 있는 getAdapterPosition() 이라는 메서드가 제공되고 있습니다.

뷰홀더의 getAdapterPosition()


    public class ViewHolder extends RecyclerView.ViewHolder {

        ViewHolder(View itemView) {
            super(itemView) ;

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int pos = getAdapterPosition() ;
                    if (pos != RecyclerView.NO_POSITION) {
                        // TODO : use pos.
                    }
                }
            });
        }
    }

ViewHolder.getAdatperPosition() 메서드가 리턴하는 값은 어댑터 내 아이템의 위치(position)이지만, 리턴 값이 NO_POSITION인지에 대한 검사는 해줘야 합니다. notifyDataSetChanged()에 의해 리사이클러뷰가 아이템뷰를 갱신하는 과정에서, 뷰홀더가 참조하는 아이템이 어댑터에서 삭제되면 getAdapterPosition() 메서드는 NO_POSITION을 리턴하기 때문입니다.

5. 아이템 위치(position)로 데이터 리스트 접근하기.

자, 클릭 이벤트가 발생한 아이템 위치(position)를 알아냈으니, 어댑터가 참조하고 있는 데이터 리스트로부터 데이터를 가져오는 것은 간단하죠.


    public class ViewHolder extends RecyclerView.ViewHolder {

        ViewHolder(View itemView) {
            super(itemView) ;

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int pos = getAdapterPosition() ;
                    if (pos != RecyclerView.NO_POSITION) {
                        // 데이터 리스트로부터 아이템 데이터 참조.
                        RecyclerItem item = mData.get(pos) ;

                        // TODO : use item.
                    }
                }
            });
        }
    }

6. 리사이클러뷰 외부(액티비티, 프래그먼트, ...)에서 아이템 클릭 이벤트 처리하기.

위에서 리사이클러뷰 아이템 이벤트 처리 코드를 작성했지만, 이는 어댑터 범주 안에서만 유효한 방법입니다. 그런데 어떤 경우에는 어댑터의 외부, 즉, 액티비티 또는 프래그먼트에서 아이템 클릭 이벤트를 처리하고 싶은 경우가 있습니다.


이런 경우 가장 쉽게 구현할 수 있는 방법은, 어댑터에 직접 리스너 인터페이스를 정의한 다음, 액티비티 또는 프래그먼트에서 해당 리스너 객체를 생성하고 어댑터에 전달하여 호출되도록 만드는 것입니다. 보통 "커스텀 리스너(Custom Listener)"라고 부르는데, 자식(여기서는 어댑터가 해당)이 부모(액티비티)의 이벤트 핸들러를 호출해야 할 필요가 있을 때 사용하는 방법입니다.


아래와 같은 절차를 통해 커스텀 리스너(Custom Listener)를 작성할 수 있습니다.

커스텀 리스너 작성 절차


6.1 커스텀 리스너(Custom Listener) 인터페이스 정의.

가장 먼저 해야 할 일은 자식 요소 안에서 새로운 리스너(Listener) 인터페이스를 정의하는 것입니다. 리스너에서 선언되는 메서드의 이름과 파라미터의 형식은 필요에 따라 정하면 됩니다.


[STEP-1] "XxxAdapter.java" - 어댑터 내에서 커스텀 리스너 인터페이스 정의.
public class XxxAdapter extends RecyclerView.Adapter<XxxAdapter.ViewHolder> {

    public interface OnItemClickListener {
        void onItemClick(View v, int position) ;
    }

    /// ... 코드 계속.
}

6.2 리스너 객체를 전달하는 메서드와 전달된 객체를 저장할 변수 추가.

다음, 어댑터의 외부(액티비티 또는 프래그먼트)에서 리스너 객체 참조를 어댑터에 전달하는 메서드를 추가합니다. 그리고 해당 메서드를 통해 전달된 리스너 객체 참조를 저장하는 변수도 추가합니다.


[STEP-2] "XxxAdapter.java" - 리스너 객체 전달 메서드와 변수 추가.
public class XxxAdapter extends RecyclerView.Adapter<XxxAdapter.ViewHolder> {

    /// 코드 계속 ...

    // 리스너 객체 참조를 저장하는 변수
    private OnItemClickListener mListener = null ;

    // OnItemClickListener 리스너 객체 참조를 어댑터에 전달하는 메서드
    public void setOnItemClickListener(OnItemClickListener listener) {
        this.mListener = listener ;
    }

    /// ... 코드 계속.
}

6.3 아이템 클릭 이벤트 핸들러 메서드에서 리스너 객체 메서드 호출.

이제 어댑터 내 뷰홀더에서 아이템 클릭 시, 커스텀 이벤트 메서드를 호출하는 코드를 작성합니다.


[STEP-3] "XxxAdapter.java" - 리스너 객체 전달 메서드와 변수 추가.
public class XxxAdapter extends RecyclerView.Adapter<XxxAdapter.ViewHolder> {

    public class ViewHolder extends RecyclerView.ViewHolder {
 
        ViewHolder(View itemView) {
            super(itemView) ;

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int pos = getAdapterPosition() ;
                    if (pos != RecyclerView.NO_POSITION) {
                        // 리스너 객체의 메서드 호출.
                        if (mListener != null) {
                            mListener.onItemClick(v, pos) ;
                        }
                    }
                }
            });
        }
    }
}

6.4 액티비티(또는 프래그먼트)에서 커스텀 리스너 객체 생성 및 전달.

자, 이제 마지막으로, 액티비티 또는 프래그먼트에서 커스텀 이벤트 리스너 객체를 생성하여 어댑터에 전달합니다.


[STEP-4] "MainActivity.java" - 리스너 객체 전달 메서드와 변수 추가.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        /// 코드 계속 ...

        XxxAdapter adapter = new XxxAdapter(list) ;

        adapter.setOnItemClickListener(new XxxAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(View v, int position) {
                // TODO : 아이템 클릭 이벤트를 MainActivity에서 처리.
            }
        }) ;
    }

7. 리사이클러뷰 아이템 클릭 이벤트 처리 예제

그럼 이제 간단한 예제를 통해, 리사이클러뷰 아이템 클릭 이벤트를 어떻게 처리하는지 확인해보겠습니다.


예제 동작은 정말 단순합니다. [안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView)]의 예제를 그대로 가져와서, 아이템 클릭 시 아이템의 텍스트를 바꾸는 기능만 추가합니다.

7.1 메인액티비티에 리사이클러뷰 추가.

메인액티비티에 리사이클러뷰를 추가합니다.


[STEP-1] "content_main.xml" - 메인액티비티 레이아웃 리소스 XML에 리사이클러뷰 추가.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler1"
        android:scrollbars="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</android.support.constraint.ConstraintLayout>

7.2 리사이클러뷰 아이템 뷰 레이아웃 추가.

리사이클러뷰 아이템에 표시될 아이템 뷰 레이아웃을 작성합니다.


[STEP-2] "recyclerview_item.xml" - 아이템 뷰를 위한 리소스 레이아웃 XML 작성.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/text1"
        android:textSize="32sp"/>

</android.support.constraint.ConstraintLayout>

7.3 리사이클러뷰 어댑터 구현.

리사이클러뷰 어댑터 구현에 관한 자세한 설명은 [안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView)]을 참고하세요.


[STEP-3] "SimpleTextAdapter.java" - 리사이클러뷰 어댑터 작성.
public class SimpleTextAdapter extends RecyclerView.Adapter<SimpleTextAdapter.ViewHolder> {

    private ArrayList<String> mData = null ;

    // 아이템 뷰를 저장하는 뷰홀더 클래스.
    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView textView1 ;

        ViewHolder(View itemView) {
            super(itemView) ;

            // 뷰 객체에 대한 참조. (hold strong reference)
            textView1 = itemView.findViewById(R.id.text1) ;
        }
    }

    // 생성자에서 데이터 리스트 객체를 전달받음.
    SimpleTextAdapter(ArrayList<String> list) {
        mData = list ;
    }

    // onCreateViewHolder() - 아이템 뷰를 위한 뷰홀더 객체 생성하여 리턴.
    @Override
    public SimpleTextAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Context context = parent.getContext() ;
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) ;

        View view = inflater.inflate(R.layout.recyclerview_item, parent, false) ;
        SimpleTextAdapter.ViewHolder vh = new SimpleTextAdapter.ViewHolder(view) ;

        return vh ;
    }

    // onBindViewHolder() - position에 해당하는 데이터를 뷰홀더의 아이템뷰에 표시.
    @Override
    public void onBindViewHolder(SimpleTextAdapter.ViewHolder holder, int position) {
        String text = mData.get(position) ;
        holder.textView1.setText(text) ;
    }

    // getItemCount() - 전체 데이터 갯수 리턴.
    @Override
    public int getItemCount() {
        return mData.size() ;
    }
}

7.4 리사이클러뷰 아이템 클릭 이벤트 처리.

이제 본문에서 설명한, 리사이클러뷰 아이템 클릭 이벤트를 처리하는 코드를 작성합니다. 앞서 작성한 어댑터의 내용 중, 뷰홀더의 생성자에 관련 코드를 작성합니다.


[STEP-4] "SimpleTextAdapter.java" - 아이템 클릭 이벤트 처리.
public class SimpleTextAdapter extends RecyclerView.Adapter<SimpleTextAdapter.ViewHolder> {

    /// 코드 계속...

    // 아이템 뷰를 저장하는 뷰홀더 클래스.
    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView textView1 ;

        ViewHolder(View itemView) {
            super(itemView) ;

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int pos = getAdapterPosition() ;
                    if (pos != RecyclerView.NO_POSITION) {
                        mData.set(pos, "item clicked. pos=" + pos) ;

                        notifyItemChanged(pos) ;
                    }
                }
            });

            // 뷰 객체에 대한 참조. (hold strong reference)
            textView1 = itemView.findViewById(R.id.text1) ;
        }
    }

    /// ... 코드 계속.
}

7.5 리사이클러뷰에 어댑터와 레이아웃매니저 지정하기.

마지막으로 어댑터와 레이아웃매니저 객체를 만들고, 각각 리사이클러뷰에 지정합니다.


[STEP-5] "MainActivity.java" - 리사이클러뷰에 어댑터와 레이아웃매니저 지정.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... 코드 계속

        // 리사이클러뷰에 표시할 데이터 리스트 생성.
        ArrayList<String> list = new ArrayList<>();
        for (int i=0; i<100; i++) {
            list.add(String.format("TEXT %d", i)) ;
        }

        // 리사이클러뷰에 LinearLayoutManager 객체 지정.
        RecyclerView recyclerView = findViewById(R.id.recycler1) ;
        recyclerView.setLayoutManager(new LinearLayoutManager(this)) ;

        // 리사이클러뷰에 SimpleTextAdapter 객체 지정.
        SimpleTextAdapter adapter = new SimpleTextAdapter(list) ;
        recyclerView.setAdapter(adapter) ;
    }

7.6 실행 화면.

위의 예제를 작성하고 실행하면, 아래와 같은 화면이 표시됩니다.

리사이클러뷰 아이템 클릭 예제 실행 화면 1


각 아이템을 클릭하면, 아이템에 표시되는 문자열이 변경되는 것을 확인할 수 있습니다.

리사이클러뷰 아이템 클릭 예제 실행 화면 2


8. 참고.

.END.


ANDROID 프로그래밍/RECYCLERVIEW