트리 구조로 리사이클러뷰를 사용하고 싶은 경우가 있다.
각 가게들의 쿠폰을 저장하기 위해서, 각 가게 - 각 쿠폰의 트리 구조로 리사이클러뷰를 구현하고자 했다.
가장 간단한 방법은 리사이클러뷰에 붙이는 아이템의 종류를 헤더(가게) / 자식(쿠폰)으로 나눠서 역할을 분리하는 것이다.
하나의 리사이클러뷰에 [동일 클래스] 가게-쿠폰-쿠폰-쿠폰-가게-쿠폰-쿠폰.... 로 해버리는 것이다.
쿠폰 객체 구성
일단 사용할 쿠폰에 관련된 데이터를 들고 있는 클래스를 선언하자. 쿠폰 id, 가게명, 쿠폰명 등등을 필드로 갖고 있다.
@Getter
@Setter
@AllArgsConstructor
@Builder
public class CouponModel{
long couponId;
String storeName;
String couponName;
String couponCode;
int minimumOrderPrice;
String startDate;
String endDate;
String expiryDate;
CouponType couponType;
int fixAmount;
Double rateAmount;
}
그리고 이 쿠폰 클래스를 포함하여 리사이클러뷰에 붙어 역할을 수행할 클래스를 선언한다.
dataType이라는 변수를 통해 객체가 헤더인지, 자식인지 구분해준다.
또한, ArrayList를 선언하여 만약 헤더(가게)면 자식(쿠폰들)의 리스트를 들고 있게 한다.
이는 접었다 - 폈다를 구현할 때, 접은 경우 사라지는 자식들을 들고 있기 위함이다.
@Getter
@Setter
@AllArgsConstructor
public class ExpandableCouponData {
//0 - 헤더, 1 - 자식
int dataType;
//쿠폰 데이터
CouponModel couponModel;
ArrayList<ExpandableCouponData> invisibleChild;
public ExpandableCouponData(int dataType, String storeName){
this.dataType =dataType;
this.couponModel = new CouponModel(storeName);
}
public ExpandableCouponData(int dataType, CouponModel couponModel){
this.dataType =dataType;
this.couponModel = couponModel;
}
}
뷰 구성
역시 헤더(가게) 타입과 자식(쿠폰)타입이 보여질 각 아이템 뷰도 만들어놔야 한다.
(XML 코드 보기 ▽)
<!--부모 (헤더) -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:paddingLeft="13dp"
android:paddingStart="13dp"
android:paddingRight="10dp"
android:paddingEnd="10dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/header_title"
android:textColor="#045CB1"
android:textSize="18sp"
android:gravity="center_vertical"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageView
android:id="@+id/btn_expand_toggle"
android:src="@drawable/ic_baseline_arrow_drop_down_24"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_weight="0"/>
</LinearLayout>
<!--자식 (쿠폰) -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/drawable_round_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout3"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:background="@drawable/drawable_round_background_stroke"
android:backgroundTint="@color/main_400"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/button_watchCode"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:minWidth="0dp"
android:text="코드"
app:icon="@drawable/ic_baseline_sync_24"
app:iconGravity="textTop"
app:iconTint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/linearLayout_coupon_code"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="@color/white"
android:gravity="center"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:paddingBottom="10dp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/constraintLayout3"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView_couponCode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="쿠폰코드"
android:textColor="@color/text_400"
android:textSize="24sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/linearLayout_coupon_info"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical"
android:paddingLeft="20dp"
android:paddingTop="5dp"
android:paddingRight="10dp"
android:paddingBottom="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/constraintLayout3"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textView_couponName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="쿠폰명"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/textView_couponAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="20"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=" 할인"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/linerlayout_due"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/textView_couponExpire"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2000/00/00"
android:textSize="@dimen/search_shop_description" />
</LinearLayout>
<LinearLayout
android:id="@+id/linerlayout_minimum"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/textView_minimum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="10000"
android:textSize="@dimen/search_shop_description" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="원 이상 결제 시 사용 가능"
android:textSize="@dimen/search_shop_description" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
데이터 정리
결국 하나의 리사이클러뷰에 두 타입의 데이터를 가게-쿠폰-쿠폰-가게-쿠폰-쿠폰
처럼 하나의 리스트로 넣기 때문에 데이터 역시 미리 위와 같은 타입으로 정리해놔야 한다. (미리 해놨으면 안해도 됨)
public ArrayList<ExpandableCouponData> parser(){
LinkedHashMap<String, ArrayList<CouponModel>> map = new LinkedHashMap<>();
ArrayList<ExpandableCouponData> parse = new ArrayList<>();
//1. 가게 이름별에 따라 쿠폰 정리
for (CouponModel c : myCoupons.getValue()){
if(!map.containsKey(c.getStoreName())){
map.put(c.getStoreName(), new ArrayList<>());
}
map.get(c.getStoreName()).add(c);
}
//2. 리스트로 정리
for(String storeName : map.keySet()){
//0
parse.add(new ExpandableCouponData(MysosoCouponAdapter.HEADER,storeName));
for(CouponModel c : map.get(storeName)){
//1
parse.add(new ExpandableCouponData(MysosoCouponAdapter.CHILD, c));
}
}
return parse;
}
리사이클러뷰 어댑터
이제 어댑터에 ExpandableCouponData를 붙이되, 각각의 객체를 헤더인지 / 자식인지 구분해서 붙여주면 된다.
이를 위해, ViewHolder를 상속받는 두 타입의 HeaderViewholder, ChildViewholder를 따로 선언해서 역할을 분리시켜주면 된다.
바인딩 역시 위에 선언한 두 타입에 뷰에 맞는 바인딩객체로 각각의 뷰 홀더에 맞게 바인딩하면 된다.
이때 헤더타입의 데이터는 자식타입에서 요구하는 데이터가 없기 때문에 잘 나눠주지 못하면 null오류가 터지게 된다.
public class MysosoCouponAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public static final int HEADER = 0;
public static final int CHILD = 1;
ArrayList<ExpandableCouponData> items;
ItemMysosoCouponHeaderBinding headerBinding;
ItemMysosoCouponBinding childBinding;
public MysosoCouponAdapter(ArrayList<ExpandableCouponData> items) {
this.items = items;
}
public void setItems(ArrayList<ExpandableCouponData> items){
this.items = items;
}
@NonNull
@NotNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
//두 가지 타입의 뷰홀더 리턴
switch (viewType){
case HEADER:{
headerBinding = ItemMysosoCouponHeaderBinding.inflate(inflater, parent, false);
return new HeaderViewHolder(headerBinding);
}
case CHILD:{
childBinding = ItemMysosoCouponBinding.inflate(inflater, parent, false);
return new ChildViewHolder(childBinding);
}
}
return null;
}
@Override
public void onBindViewHolder(@NonNull @NotNull RecyclerView.ViewHolder holder, int position) {
final ExpandableCouponData item = items.get(position);
//아이템 바인딩도 헤더/자식 구분해서 다른 뷰 홀더로 바인딩
switch (item.getDataType()){
case HEADER:{
((HeaderViewHolder) holder).bindItem(items.get(position), position);
break;
}
case CHILD:{
((ChildViewHolder) holder).bindItem(items.get(position));
break;
}
}
}
@Override
public int getItemCount() {
return items.size();
}
@Override
public int getItemViewType(int position) {
return items.get(position).getDataType();
}
//헤더 ViewHolder
public class HeaderViewHolder extends RecyclerView.ViewHolder{
ItemMysosoCouponHeaderBinding binding;
public HeaderViewHolder(ItemMysosoCouponHeaderBinding binding) {
super(binding.getRoot());
this.binding =binding;
}
//나중에 표시 / 숨김 처리를 위해 포지션 int 값도 받기
public void bindItem(ExpandableCouponData item, int pos){
//가게명
binding.headerTitle.setText(item.getCouponModel().getStoreName());
}
}
//자식 ViewHolder
public class ChildViewHolder extends RecyclerView.ViewHolder{
ItemMysosoCouponBinding binding;
boolean state = true;
public ChildViewHolder(ItemMysosoCouponBinding binding) {
super(binding.getRoot());
this.binding = binding;
binding.buttonWatchCode.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(state){
binding.linearLayoutCouponCode.setVisibility(View.VISIBLE);
binding.linearLayoutCouponInfo.setVisibility(View.INVISIBLE);
}
else{
binding.linearLayoutCouponCode.setVisibility(View.INVISIBLE);
binding.linearLayoutCouponInfo.setVisibility(View.VISIBLE);
}
state = !state;
}
});
}
//자식 바인딩
public void bindItem(ExpandableCouponData item){
CouponModel couponModel = item.getCouponModel();
binding.textViewCouponCode.setText(couponModel.getCouponCode());
binding.textViewCouponAmount.setText(couponModel.amount());
//코드, 이름
binding.textViewCouponName.setText(couponModel.getCouponName());
//제한조건
binding.textViewMinimum.setText(Integer.toString(couponModel.getMinimumOrderPrice()));
if(couponModel.getExpiryDate() != null){
binding.textViewCouponExpire.setText("저장 후 "+DateFormatMethod.dateFormatDay(couponModel.getExpiryDate())+"까지 사용가능");
}
}
}
}
접었다 펴기 (숨김 / 표시) 처리
이제 헤더(가게)에서 접었다-폈다 할 수 있도록 구성해보자.
어차피 다 같은 리사이클러뷰에 붙어 있는 애들이니깐 원리는 간단하다.
- 접어두기를 하면 -> 해당 헤더의 좌표 이후의 자식들을 다 지우고(따로 저장해두고)
- 펼치기를 하면 -> 해당 헤더 좌표 이후의 자식들을 다 다시 붙이면 된다.
//헤더 뷰홀더
public class HeaderViewHolder extends RecyclerView.ViewHolder{
ItemMysosoCouponHeaderBinding binding;
public HeaderViewHolder(ItemMysosoCouponHeaderBinding binding) {
super(binding.getRoot());
this.binding =binding;
}
public void bindItem(ExpandableCouponData item, int pos){
binding.headerTitle.setText(item.getCouponModel().getStoreName());
public void bindItem(ExpandableCouponData item, int pos){
binding.headerTitle.setText(item.getCouponModel().getStoreName());
//버튼 설정 (아래삼각형, 위삼각형)
if(item.getInvisibleChild() == null){
binding.btnExpandToggle.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24);
}else{
binding.btnExpandToggle.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24);
}
binding.btnExpandToggle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//숨은자식 x에서 눌림 -> 접어두기
if (item.getInvisibleChild() == null) {
item.setInvisibleChild(new ArrayList<ExpandableCouponData>());
int count = 0;
//다음 헤더 이전까지 모두 getInvisibleChild()에 저장
while (items.size() > pos + 1 && items.get(pos + 1).dataType == CHILD) {
item.getInvisibleChild().add(items.remove(pos + 1));
count++;
}
//이 사이 아이템 바뀌었다고 통지
notifyItemRangeRemoved(pos + 1, count);
binding.btnExpandToggle.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24;
}
//펼치기
else {
int index = pos + 1;
//현재 좌표 이후로 다시 아이템 붙이기
for (ExpandableCouponData i : item.getInvisibleChild()) {
items.add(index, i);
index++;
}
//이 사이 아이템 바뀌었다고 통지
notifyItemRangeInserted(pos + 1, index - pos - 1);
binding.btnExpandToggle.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24);
//숨긴 자식 목록 비우기
item.setInvisibleChild(null);
}
}
});
}
}
}
결과
요로코롬 각 가게에 맞게 쿠폰을 접었다 폈다 할 수 있다.
전체적인 어플리케이션 코드는 다음 레포를 확인하면 된다.
전문가가 아니라 정확하지 않은 지식이 담겨있을 수 있습니다.
언제든지 댓글로 의견을 남겨주세요!
'코딩 > 안드로이드' 카테고리의 다른 글
안드로이드 리사이클러뷰 아이템 스와이프해서 버튼 띄우기 [Android, JAVA] (0) | 2022.09.10 |
---|---|
안드로이드 이메일, 비밀번호 입력 폼 (머테리얼 디자인) [Android, JAVA] (1) | 2022.03.12 |
안드로이드 Daum 주소 검색 API 활용하기 [Android] (6) | 2022.02.18 |
JetPack 직전 DialogFragment 값 받아오기 (BackStackEntry, SavedStateHandle) [Android (0) | 2022.02.11 |
RecyclerView 아이템 클릭 이벤트 구현 [Android, JAVA] (4) | 2022.01.29 |
Comment