관리 메뉴

나만을 위한 블로그

[Android] 리사이클러뷰 클릭 이벤트 2 본문

Android

[Android] 리사이클러뷰 클릭 이벤트 2

참깨빵위에참깨빵_ 2019. 11. 7. 21:46
728x90
반응형

참고한 사이트 : https://recipes4dev.tistory.com/168

 

2022.08.12) 이 글은 자바로 돼 있으니 코틀린 버전을 보려면 아래 링크 확인

https://onlyfor-me-blog.tistory.com/525

 

[Android] 코틀린으로 리사이클러뷰 클릭 이벤트를 구현하는 방법 정리

리사이클러뷰 클릭 이벤트에 대해선 예전에 자바로 작성한 적이 있다. https://onlyfor-me-blog.tistory.com/40 [Android] 리사이클러뷰 클릭 이벤트 2 참고한 사이트 : https://recipes4dev.tistory.com/168 리사..

onlyfor-me-blog.tistory.com

 

리사이클러뷰에서도 리스트뷰처럼 setOnItemClickListener() 사용과 유사한 방법을 통해 아이템 클릭 이벤트를 처리할 수 있나? 디벨로퍼에는 이와 같은 or 유사한 기능을 하는 메서드가 없다. 전에 공부했을 때도 못 봤다.

그럼 리사이클러뷰는 아이템을 표시만 할 수 있고 클릭 이벤트는 못 쓰나? 아니면 다른 방법이 있나?

 

 

기본적으로 리스트뷰는 유사한 형태, 크기를 가진 아이템뷰를 세로 방향 한 줄로 나열한다.

이런 단순한 표시 형태로 인해, 화면에 표시된 아이템 클릭 시 몇 번째 아이템이 선택됐는지 계산하는 과정이 비교적 간단하다. 그래서 클릭 이벤트 처리 기능을 리스트뷰가 직접 제공한다.

 

이에 반해 리사이클러뷰는 더 유연하고 다양한 형태로 아이템을 표시하게 만들어 주지만, 이런 장점이 아이템 클릭 이벤트 처리를 복잡하게 꼬아버린다. 그래서 리사이클러뷰는 클릭 이벤트 리스너를 자기가 직접 다루지 않고 아이템 뷰에서 onClickListener()를 통해 처리하게 만들어놓았다.

 

- 리사이클러뷰 뷰홀더에서 아이템 클릭 이벤트 처리하기

 

아이템 뷰에서 onClick 리스너를 통해 클릭 이벤트를 처리한다는 문장이 어떻게 코드로 매핑되는지 머리속에 그려지나?

https://recipes4dev.tistory.com/154 이 곳의 설명 중 '어댑터를 통해 만들어진 각 아이템 뷰는 "뷰홀더" 객체에 저장돼 화면에 표시되고 필요에 따라 생성 or 재활용한다' 라는 게 있다.

그럼 코드 진행을 머리속으로 그려보면

1. 아이템 뷰에서 클릭 이벤트를 직접 처리하는데

2. 아이템 뷰는 뷰홀더 객체가 갖고 있으니

3. 아이템 클릭 이벤트는 뷰홀더에서 작성하면 되지 않을까?

 

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

 

 

- 아이템 위치(position) 알아내기

 

여기까지 했다면 아이템 클릭 이벤트를 처리할 수 있게 됐다. onClick()에서 로그를 출력하는 코드를 넣어보면 아이템 클릭 메시지가 표시되는 걸 확인할 수 있다.

다음으로 할 일은 현재 클릭 이벤트가 발생한 아이템 위치(position)을 알아내는 것이다.

 

보통 앱에서 리사이클러뷰 아이템 클릭 이벤트를 쓸 때는 현재 선택된 아이템에서 다른 액션을 실행하거나, 아이템과 연결된 데이터를 확인, 수정, 삭제하는 등의 기능을 실행하려고 클릭 이벤트를 구현한다.

그래서 아이템 클릭 이벤트에서 가장 처음으로 할 일은 현재 클릭 이벤트가 발생한 아이템의 위치를 알아내는 것이다.

 

그럼 그 위치를 어떻게 알아내나? onClick()에는 뷰 객체에 대한 참조만 전달되고 position에 대한 정보는 없다.

그럼 뷰 객체의 태그를 통해 position을 직접 저장하고 관리해야 하나?

 

그럴 필요는 없다. 이 때를 위해 리사이클러뷰 뷰홀더에는 현재 자신의 위치를 확인할 수 있는 getAdapterPosition()이란 메서드가 존재한다.

 

 

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

 

getAdapterPosition()으로 아이템의 position을 알아냈으니 어댑터가 참조하고 있는 데이터 리스트로부터 데이터를 가져와야 한다.

 

 

 

- 리사이클러뷰 바깥(액티비티, 프래그먼트 등)에서 아이템 클릭 이벤트 처리하기

 

 

위의 리사이클러뷰 아이템 이벤트 처리 코드는 어댑터 안에서만 유효한 방법이다.

그런데 어떤 경우에는 어댑터 바깥(액티비티, 프래그먼트)에서 아이템 클릭 이벤트를 처리하고 싶은 경우가 있다.

 

이 경우 가장 쉬운 방법은 어댑터에 직접 리스너 인터페이스를 정의하고 액티비티 or 프래그먼트에서 해당 리스너 객체를 생성하고 어댑터에 전달해 호출되게 하는 것이다.

보통 커스텀 리스너라고 부르는데 자식(여기선 어댑터)이 부모(액티비티)의 이벤트 핸들러를 호출해야 할 필요가 있을 때 쓰는 방법이다.

아래의 절차를 거치면 커스텀 리스너를 만들 수 있다.

 

커스텀 리스너를 만드는 방법 그림 설명

 

 

- 커스텀 리스너 인터페이스 정의

 

 

가장 먼저 해야 할 일은 자식 요소 안에서 새 리스너 인터페이스를 정의하는 것이다.

리스너에서 선언되는 메서드명, 파라미터 형식은 필요에 따라 정하면 된다.

 

 

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

 

 

다음으로 어댑터 바깥(액티비티 or 프래그먼트)에서 리스너 객체 참조를 어댑터에 전달하는 메서드를 추가한다.

그리고 해당 메서드를 통해 전달된 리스너 객체 참조를 저장하는 변수도 추가한다.

 

 

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

 

 

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

 

 

 

- 액티비티 or 프래그먼트에서 커스텀 리스너 객체 생성, 전달

 

 

마지막으로 액티비티 or 프래그먼트에서 커스텀 이벤트 리스너 객체를 만들어 어댑터에 전달한다.

 

 

다음 부분부터는 예제라 블로그 참고하기.

TEXT 1~n 까지 숫자를 출력하고 아이템을 클릭하면 "item clicked. pos=n"이란 문장이 나오도록 하는 예제다.

 

 

+ 20.06.11 추가) 위 예제와 같은 내용의 예제다. 실제 코드로는 이런 식으로 쓸 수 있다는 걸 보여주기 위해 추가했다.

코드블럭 별 어떤 파일인지 주석 모양으로 써놨으니 참고하자.

 

// activity_main.xml

<androidx.constraintlayout.widget.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"
    tools:context=".MainActivity">

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

</androidx.constraintlayout.widget.ConstraintLayout>
// recyclerview_item.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

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

</LinearLayout>
// SimpleTextAdapter

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;

public class SimpleTextAdapter extends RecyclerView.Adapter<SimpleTextAdapter.ViewHolder> {

    public interface OnItemClickListener {
        void onItemClick(int pos);
    }

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

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

    private ArrayList<String> mData = null ;

    // 아이템 뷰를 저장하는 뷰홀더 클래스.
    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) {
                        if (mListener != null) {
                            mListener.onItemClick(pos);
                        }
//                        mData.set(pos, "item clicked. pos=" + pos) ;
//
//                        notifyItemChanged(pos) ;
                    }
                }
            });

            // 뷰 객체에 대한 참조. (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() ;
    }
}
// MainActivity.java

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    private SimpleTextAdapter adapter;
    private static final String TAG = "MainActivity";

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

        // 리사이클러뷰에 표시할 데이터 리스트 생성.
        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)) ;

        adapter = new SimpleTextAdapter(list);
        adapter.setOnItemClickListener(new SimpleTextAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(int pos)
            {
                if (pos == 0) {
                    Toast.makeText(MainActivity.this, String.valueOf(pos), Toast.LENGTH_SHORT).show();
                    Log.e(TAG, "메인에서 건드린 포지션 값 : " + pos);
                } else if (pos == 1) {
                    Toast.makeText(MainActivity.this, String.valueOf(pos), Toast.LENGTH_SHORT).show();
                    Log.e(TAG, "메인에서 건드린 포지션 값 : " + pos);
                }
            }
        });

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

코드 안의 주석들은 테스트 용으로 넣은 것이니 당연히 없애도 무관하다.

반응형
Comments