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

확장 가능한 RecyclerView 하고 싶습니다 도와주세요 [closed]

0 추천

안녕하세요 RecyclerView로 확장 가능한 listview를 만들고 싶어 질문합니다.

이런식의 RecyclerView를 만들고 싶은데 Header 부분은 1월 - 12월로 나누고 child 는 각각 달에 맞는 일수로(1~30/31일) 로 구현하고 싶습니다. 현재 Mysql DB에서 저장된 데이터를 불러와 그냥 일반적인 recyclerview로 구현은 해놨습니다. 구글 검색을 통해서 알아봤지만 갈피를 못잡겠어서 도움 요청 드립니다. 필요하신 코드나 정보가 있으시면 말씀해주세요 바로바로 올려 드리겠습니다.

 

 

+ child(1~31) 부분에 들어갈 데이터는 mysql 에 있는 데이터를 php 파일로 받아 서버에서 json 형태로 받아서 recyclerView로 뿌려주었습니다. 이 json 데이터를 받아와서 날짜, 이름, 메모내용을 child 아이템 각 하나당 header(Month)에 해당하는 날짜에 뿌리고 싶습니다.

여러 expandable recyclerview 구현이 되어있는걸 찾아봤는데 그냥 header에 들어갈 텍스트를 입력하고 child 에 들어갈 텍스트 또한 직접 입력을 해놓은 상태에서 그냥 불러오기만 하여 해결을 못하고 있습니다.

 

질문을 종료한 이유: 해결
사자의사자 (200 포인트) 님이 2023년 7월 19일 질문
사자의사자님이 2023년 7월 21일 closed

1개의 답변

+1 추천

해당 기능을 구현하는데는 여러가지 접근방법이 있을 수 있습니다.

1. 월 헤더를 펼칠 때, 월에 해당하는 날짜 데이터를 바로 밑에 추가하고 어댑터를 갱신한다.

2. 월 어댑터 아이템에 리사이클러뷰를 포함시키고 일 어댑터를 사용한다. NestedRecyclerView의 접근방법과 동일합니다.

3. ConcatAdapter를 이용한다.

저는 2번 방법을 사용하겠습니다. 참고로 예제는 코틀린으로 되어있으니, 참고하세요. 그리고 예제는 예제일 뿐 100% 완벽하게 동작하지 않을 수 있으니, 참고하셔서 필요한 부분은 보완하시기 바랍니다.

먼저 월을 보여줄 어댑터와 아이템 클래스를 만듭니다. 월 뷰홀더가 내부적으로 일 데이터를 보여줄 RecyclerView를 포함하도록 할 겁니다.

 

data class MonthItem(
    val text: String,
    val days: Int,
    var expanded: Boolean = false,
    var dayItems: List<DayItem> = emptyList()
) {

    val iconResId: Int
        get() = if (expanded) R.drawable.ic_expand_more else R.drawable.ic_expand_less
}

 

//item_moth.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
    android:orientation="vertical"
    android:paddingHorizontal="8dp"
    android:paddingVertical="8dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:id="@+id/titleTxt"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
            tools:text="@tools:sample/first_names" />

        <ImageView
            android:id="@+id/moreImg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:srcCompat="@drawable/ic_expand_less" />
    </LinearLayout>

    <!-- 일 아이템들을 보여줄 리사이클러뷰 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:text="@string/hello_blank_fragment"
        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" />

</LinearLayout>

 

class MonthAdapter : RecyclerView.Adapter<MonthViewHolder>() {

    private val viewPool = RecyclerView.RecycledViewPool()

    private var items = listOf<MonthItem>()

    fun submitList(list: List<MonthItem>) {
        this.items = list
        notifyDataSetChanged()
    }

    override fun getItemCount(): Int {
        return this.items.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MonthViewHolder {
        val binding = ItemMonthBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )

         // 일 데이터를 리사이클러뷰를 통해 보여준다. 
        val childAdapter = DayAdapter()
        binding.apply {
            recyclerView.layoutManager = GridLayoutManager(parent.context, 7)
            recyclerView.adapter = childAdapter
            // NestedRecycler 사용시 성능개선을 위해 리사이클러뷰 간 뷰를 공유하기 위해 ViewPool을 사용한다.
            recyclerView.setRecycledViewPool(viewPool)
        }
        return MonthViewHolder(binding) { position ->
            val item = items[position]
            item.expanded = !item.expanded // 토클 
            //  토글 상태를 반영하기 위해 토글이 발생한 row만 리프레시 하게 만든다. (결과적으로 onBindViewHolder가 호출된다)
            this.notifyItemChanged(position)
        }
    }

    override fun onBindViewHolder(holder: MonthViewHolder, position: Int) {
        val item = items[position]
        holder.bind(item)
    }
}


class MonthViewHolder(
    private val binding: ItemMonthBinding,
    private var onHeaderItemClick: ((Int) -> Unit)?
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item: MonthItem) {
        binding.apply {
            titleTxt.text = item.text
            moreImg.setImageResource(item.iconResId)
            moreImg.setOnClickListener {
                onHeaderItemClick?.invoke(absoluteAdapterPosition)
            }
            
            // 월 아이템이 펼쳐진 상태이면 일 아이템을, 접힌상태면 빈 리스트를 보여준다.
            val childItems = if (item.expanded)
                item.dayItems
            else
                emptyList()
            (recyclerView.adapter as DayAdaptrer).submitList(childItems)
        }
    }
}

 

DayAdapter 구현입니다.

data class DayItem(
    val text: String
)

 

class DayAdapter : RecyclerView.Adapter<DayViewHolder>() {

    private var items = listOf<DayItem>()

    fun submitList(list: List<DayItem>) {
        this.items = list
        notifyDataSetChanged()
    }

    override fun getItemCount(): Int {
        return this.items.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DayViewHolder {
        val binding = ItemDayBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return DayViewHolder(binding)
    }

    override fun onBindViewHolder(holder: DayViewHolder, position: Int) {
        holder.bind(items[position])
    }
}

class DayViewHolder(
    private val binding: ItemDayBinding
) : RecyclerView.ViewHolder(binding.root) {
    fun bind(item: DayItem) {
        binding.apply {
            titleTxt.text = item.text
        }
    }
}

 

이제 프래그먼트에서 MonthAdapter를 호출하여 사용해 보겠습니다.

class ExpandedListFragment : Fragment() {

    private var mBinding: FragmentRecyclerViewWithHeaderBinding? = null
    private val binding: FragmentRecyclerViewWithHeaderBinding get() = mBinding!!

    override fun onDestroyView() {
        super.onDestroyView()
        mBinding = null
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        mBinding = FragmentRecyclerViewWithHeaderBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupViews()
    }

    private fun setupViews() {
        initItems()

        binding.apply {
            val adapter = MonthAdapter()
           // layoutManager는 xml에 정의되어 있음. 
           // app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            recyclerView.adapter = adapter
            adapter.submitList(monthItemList)
        }
    }

    private val monthItemList = listOf(
        MonthItem("January",31),
        MonthItem("February", 28),
        MonthItem("March", 31),
        MonthItem("January", 31),
        MonthItem("April", 30),
        MonthItem("May", 31),
        MonthItem("June", 30),
        MonthItem("July", 31),
        MonthItem("August", 31),
        MonthItem("September", 30),
        MonthItem("October", 31),
        MonthItem("November", 30),
        MonthItem("December", 31)
    )

    private fun initItems() {
        monthItemList.forEach { item ->
            val dayItems = (1..item.days)
                .map { day ->
                    DayItem(day.toString())
                }
            item.dayItems = dayItems
        }
    }
}

 

님의 경우는 데이터를 서버에서 가져온 다음, 어댑터에 필요한 타입으로 맞춰주시면 되겠지요.

월 뷰홀더 안에 리사이클러뷰를 넣고 조작하는 부분만 잘 이해하시면 크게 어려울 게 없다고 봅니다. 도움이 되시기 바랍니다.

 

 

spark (227,530 포인트) 님이 2023년 7월 19일 답변
spark님이 2023년 7월 19일 수정
안녕하세요. 코틀린을 잘 모르지만 spark님이 조언을 통해 해결한 상태 입니다. ^^ 덕분에 감사합니다!
...