해당 기능을 구현하는데는 여러가지 접근방법이 있을 수 있습니다.
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
}
}
}
님의 경우는 데이터를 서버에서 가져온 다음, 어댑터에 필요한 타입으로 맞춰주시면 되겠지요.
월 뷰홀더 안에 리사이클러뷰를 넣고 조작하는 부분만 잘 이해하시면 크게 어려울 게 없다고 봅니다. 도움이 되시기 바랍니다.