마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

리사이클러뷰 뷰홀더 질문

0 추천

<LinearLayout>
   <ImageView />
   <Button />
</LinearLayout>

이렇게 구현중인데

public class ViewHolder extends RecyclerView.ViewHolder {
        Button button;
        ImageView image;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            image = itemView.findViewById(R.id.image);
            button = itemView.findViewById(R.id.btn);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (i == true) {
                        i = false;
                    } else {
                        i = true;
                    }
                }
            });
        }
    }

 

리스트가 하나씩 추가되면 버튼이 생길건데 이거를 각 개로 클릭이벤트 구현하려면 어떻게 해야할까요? 저거대로 하면은 다른거 버튼 클릭시 boolean i가 중첩되어 두번 눌러야 원하는 기능이 됩니다.

 

https://goatlab.tistory.com/1157

enerigpy (2,110 포인트) 님이 2023년 2월 16일 질문

2개의 답변

+1 추천
 
채택된 답변

작성하신 코드를 적게 건드리는 범위에서 올리신 코드를 수정해 봤습니다.

public class MainActivity extends AppCompatActivity implements MyRecyclerAdapter.MyRecyclerViewClickListener {

    private List<ItemData> dataList = new ArrayList<>();

    private MyRecyclerAdapter adapter;

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

        RecyclerView recyclerView = findViewById(R.id.recyclerView);

        dataList = getItems();
        adapter = new MyRecyclerAdapter(dataList);
        recyclerView.setAdapter(adapter);
        adapter.setOnClickListener(this);
    }

    private List<ItemData> getItems() {
        return Arrays.asList(new ItemData(R.drawable.ic_1), new ItemData(R.drawable.ic_2));
    }

    public void removeItem(int position) {
        dataList.remove(position);
        adapter.notifyItemRemoved(position);
    }

    @Override
    public void onItemClicked(int position) {
        dataList.get(position).toggleChecked();
        adapter.notifyItemChanged(position);
    }

    @Override
    public void onItemLongClicked(int position) {

    }

    @Override
    public void onImageViewClicked(int position) {

    }
}

 

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

    private final List<ItemData> itemData;

    public MyRecyclerAdapter(List<ItemData> itemData) {
        this.itemData = itemData;
    }

    public interface MyRecyclerViewClickListener {
        void onItemClicked(int position);

        void onItemLongClicked(int position);

        void onImageViewClicked(int position);
    }

    private MyRecyclerViewClickListener mListener;

    public void setOnClickListener(MyRecyclerViewClickListener listener) {
        this.mListener = listener;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_list, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
        final ItemData item = getItem(position);
        holder.bind(item);

        holder.button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) mListener.onItemClicked(holder.getAdapterPosition());
            }
        });

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) mListener.onItemClicked(holder.getAdapterPosition());
            }
        });

        holder.image.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) mListener.onImageViewClicked(holder.getAdapterPosition());
            }
        });

        holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if (mListener != null) {
                    mListener.onItemLongClicked(holder.getAdapterPosition());
                    return true;
                }
                return false;
            }
        });
    }

    @Override
    public int getItemCount() {
        return itemData.size();
    }

    private ItemData getItem(int position) {
        return itemData.get(position);
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        Button button;
        ImageView image;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            image = itemView.findViewById(R.id.image);
            button = itemView.findViewById(R.id.rebtn);

        }

        public void bind(ItemData item) {
            image.setImageResource(item.getImage());
            button.setBackgroundResource(item.getDrawableId());
        }
    }
}

 

public class ItemData {
    private int image;
    private boolean checked;

    public ItemData(int image) {
        this(image, false);
    }

    public ItemData(int image, boolean checked) {
        this.image = image;
        this.checked = checked;
    }

    public int getImage() {
        return image;
    }

    public void setImage(int image) {
        this.image = image;
    }

    public void setChecked(boolean checked) {
        this.checked = checked;
    }

    public void toggleChecked() {
        this.checked = !this.checked;
    }

    public int getDrawableId() {
        return checked ? R.drawable.ic_sc_btn1 : R.drawable.ic_sc_btn;
    }
}


님의 코드와 크게 다르지 않으므로, 일해사는데 문제는 없으실 거라고 생각합니다.

 

spark (227,470 포인트) 님이 2023년 2월 16일 답변
enerigpy님이 2023년 2월 16일 채택됨
어댑터의 생성자가
public MyRecyclerAdapter(List<ItemData> itemData) 이고 dataList도 List<!ItemData>타입인데 왜 에러가 나죠?
그리고 제가 올린 코드는 테스트를 해 본 코드이고 잘 동작하는 코드예요. 코드를 가져다 쓰실 때 실수가 없었는지 확인해 보세요.
아 네 그건 짜신거 보고 고쳤고 다시 본론으로 들어가면

main에서

List<ItemData> items = getItems(); 로 받아서
dataList.add(items.get(0)); 하고

adapter = new MyRecyclerAdapter(dataList);
recyclerView.setAdapter(adapter);
adapter.setOnClickListener(this);

리스트가 추가가 안되고 있는데 문제가 뭘까요..!


public class ItemData 에서

public ItemData(int image) {
        this(image, false);
    } 두 번 정의돼서 지웠습니다
public ItemData(int image)를 지우시면 다른 곳에서 사용하기 때문에 코드가 동작을 안할텐데요. 그리고 생성자는 두번 정의된게 아니라 생성자 오버로딩을 한거예요.
프로젝트를 새로 만드셔서 제가 만든 코드를 복사해서 실행을 해보세요. 아니면 기존코드 백업을 하시거나 git stash하시고 제코드를 덮어쓰기를 해보세요.
네 이젠 짜주신 코드로 거의 바꼈는데 여전히 add가 안되고 있네요 뭐가 문제인지..
작업하신 코드를 별도의 질문으로 올려보세요.
0 추천

리사이클러뷰에서 이벤트처리는 내부에서 하는 것이 아니라 콜백형태로 처리해서 결과를 외부에서 받아오는게 일반적인 처리방법입니다. 예를 뷰홀더에 있는 버튼클리이벤트를 처리하려고 하면, 버튼클릭 이벤트가 발생할 때, 이 이벤트를 어댑터를 통해 액티비티/프래그먼트로 전달하는 방식이 됩니다. 버튼 클래스가 setOnClickListener를 통해 View.OnClickListner를 처리하는 것과 같은 방식이라고 보면 됩니다.

다른 건 보지 마시고 먼저 어댑터와 액티비티 사이에 어떻게 콜백을 설정하고 처리하는지만 보시면 구조가 단순해 집니다.

public MyAdpater extends RecyclerView.Adaptger<MyViewHolder> {
     interface Listener {
          void onImageChcked(ImageInfo imageInfo):
     }

     private Listener listener;

     public void setListener(Listener listener) {
            this.listener = listener.
    }
}

보시다시피 Button.setOnClickListener를 호출하는 것과 똑같습니다. 다른 점은 실제 뷰에 대한 처리는 뷰홀더에서 하므로 Listener가 뷰홀더에 전달되어야 한다는 부분이죠. 이 분도 어댑터-뷰홀더로 좁혀놓고 보면 다른 클래스에 리스너를 설정하는 것과 똑같습니다. 결국 Button에 View.OnClickListener를 설정해서 사용하실 줄 알면, 코드를 조그만 더 주의 깊게 보시면, 이해가 갈 수 있는 부분입니다.

리사이클러뷰 어댑터에 대해서 알고 계셔야 하는 중요한 부분은 리사이클러뷰라는 이름그대로, 재활용을 한다는 겁니다. 즉, '뷰'를 재활용하는 겁니다. '뷰'는 뷰홀더에 존재하므로 결국 뷰홀더 오브젝트를 재활용하는 겁니다. 재활용 목적은 매번 클래스를 생성하는 것보다 재활용이 성능상 유리하기 때문입니다. 따라서 리사이클러뷰를 스크롤하게 되면 이전 생성되었던 뷰홀더가 재사용이 되므로, 뷰홀더에 있던 데이터도 남아 있게 됩니다. 따라서 뷰홀더에 멤버변수같은 것을 사용할 때는 주의를 해서 사용하셔야 합니다. 님의 경우는 리사이클러뷰를 스크롤하게 되면 i 변수가 엉뚱한 아이템의 버튼 상태를 가리키게 될 수가 있는거죠.

따라서 i변수는 사용하지 마시고 ImageInfo의 checked변수를 이용해서 리사이클러뷰를 업데이트 하세요. 아래와 같은 형태가 됩니다.

// Activity
private ImageAdapter imageAdapter;
private List<ImageInfo> dataSet;

// onCreate
dataSet = getImageData();
imageAdapter = new ImageAdapter(dataSet, new ImageAdapter.Listener() {
    @Override
     void onImageChcked(ImageInfo imageInfo) {
             int index = dataSet.indexOf(imageInfo.getId());
             imageInfo.setChecked(!imageInfo.isChecked());
             dataSet.set(index, imageInfo);
             imageAdapter.setDataSet(dataSet);
     }
});

위처럼 어댑터의 데이터소스를 변경하여 어댑터를 업데이트 해주는 형태로 작성하세요.

시간내셔서 개발자가이드에서 리사이클러뷰를 어떻게 사용하는지 읽어보시기 바랍니다. 자주 사용하기 때문에 어느정도는 잘 알고 계셔야하는 뷰입니다.

spark (227,470 포인트) 님이 2023년 2월 16일 답변
ImageInfo에 checked 상태를 저장하지 않으려면 어댑터 내부에 SparseArray같은 구조를 사용하여 토글정보를 저장해서 처리해도 됩니다. 그리고 데이터 하나만 업데이트할 때는 notifyDataSetChanged보다는 데이터 한개 또는 특정 범위만 업데이트할 수 있는 메소드가 있으니 그걸 사용하는게 효율적입니다. 최적화를 원하시면 DiffUtil을 사용하시구요.
데이터 리스트를
private ArrayList<ItemData> itemData;
이렇게 처음에 정의했고

이미지는 int[] 리스트로 정의해서 img 에 하나씩 저장해서
itemData에 add 할땐 ItemData(img[0]) 이런식으로 구현중인데

위에 짜주신 코드로 넣으려고 할때는
dataSet에다가 R.drawable이나 int로 못넣고 imageinfo로 넣는거 같은데 어떻게 해야하나요?

제가 구현한 코드는 https://goatlab.tistory.com/1157 여기 참고했구요!
작성하신 코드를 올려보세요.
public class MyRecyclerAdapter extends RecyclerView.Adapter<MyRecyclerAdapter.ViewHolder> {
    private ArrayList<ItemData> itemData;
    boolean i = false;

    public MyRecyclerAdapter(ArrayList<ItemData> itemData) {
        this.itemData = itemData;
    }

    public interface MyRecyclerViewClickListener{
        void onItemClicked(int position);
        void onItemLongClicked(int position);
        void onImageViewClicked(int position);
    }

    private MyRecyclerViewClickListener mListener;

    public void setOnClickListener(MyRecyclerViewClickListener listener) {
        this.mListener = listener;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_list, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
        ItemData item = itemData.get(position);
        holder.image.setImageResource(item.getImage());

        if (mListener != null) {
            final int pos = position;
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mListener.onItemClicked(pos);
                }
            });
            holder.image.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mListener.onImageViewClicked(pos);
                }
            });
            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    mListener.onItemLongClicked(holder.getAdapterPosition());
                    return true;
                }
            });
        }
    }

    @Override
    public int getItemCount() {
        return itemData.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        Button button;
        ImageView image;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            image = itemView.findViewById(R.id.image);
            button = itemView.findViewById(R.id.rebtn);
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (i == true) {
                        button.setBackgroundResource(R.drawable.ic_sc_btn);
                        i = false;
                    } else {
                        button.setBackgroundResource(R.drawable.ic_sc_btn1);
                        i = true;
                    }
                }
            });
        }
    }

    //리스트 삭제 이벤트
    public void remove(int position){
        try {
            itemData.remove(position);
            notifyDataSetChanged();
        } catch (IndexOutOfBoundsException e){
            e.printStackTrace();
        }
    }
}
위는 어뎁터이고

아래는

ArrayList<ItemData> dataList = new ArrayList<>();

    int[] img = {R.drawable.ic_1, R.drawable.ic_2};

    final MyRecyclerAdapter adapter = new MyRecyclerAdapter(dataList);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       
        RecyclerView recyclerView = findViewById(R.id.recyclerView);
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
       
       // 데이터리스트 추가
       dataList.add(new ItemData(img[0]));
       dataList.add(new ItemData(img[1]));

        recyclerView.setAdapter(adapter);
        adapter.setOnClickListener(this);
...