이 튜토리얼에서는 중첩된 리사이클러 뷰를 생성하는 방법을 단계별로 배울 것입니다. 이것은 다른 리사이클러 뷰 내에 리사이클러 뷰를 배치하는 방법입니다. 각 리사이클러 뷰에는 데이터를 바인딩하기 위한 독립 어댑터가 필요합니다. 각각은 스크롤을 적절하게 처리해야 합니다.

아래의 예를 확인하십시오.

예제 1: Kotlin Android 중첩 RecyclerView

다음은 이 프로젝트의 데모입니다.

 코틀린 안드로이드 중첩 RecyclerView

1단계: 프로젝트 생성

빈 ‘Android Studio’ 프로젝트를 생성하여 시작합니다.

2단계: 종속성

app/build.gradle 파일에 다음 구현 문을 추가하여 GravitySnapHelper를 설치합니다.

    implementation 'com.github.rubensousa:gravitysnaphelper:2.1.0'

3단계: 레이아웃 디자인

이 프로젝트에는 세 가지 레이아웃이 있습니다.

중첩_adapter_item.xml

이것은 innert recyclerview 항목의 레이아웃입니다.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="@dimen/item_width"
    android:layout_height="@dimen/item_height"
    android:layout_margin="4dp"
    android:background="@android:color/black"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        style="@style/TextAppearance.AppCompat.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:textColor="@android:color/white"
        android:textSize="@dimen/item_text_size"
        tools:text="0" />

</FrameLayout>

(비). 중첩_어댑터_목록

내부 리사이클러 뷰의 레이아웃입니다. OrientationAwareRecyclerView 추가:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/nestedTitleTextView"
        style="@style/TextAppearance.AppCompat.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp" />

    <com.github.rubensousa.gravitysnaphelper.OrientationAwareRecyclerView
        android:id="@+id/nestedRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

(씨). nested_adapter_item.xml

외부 리사이클러 뷰의 레이아웃입니다. OrientationAwareRecyclerView를 한 번 더 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.github.rubensousa.gravitysnaphelper.OrientationAwareRecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

4단계: 데이터 클래스 생성

이 데이터 클래스는 우리의 모델 클래스가 될 것입니다:

(ㅏ). TiledList.kt

data class TitledList(
    val title: String,
    val texts: MutableList<String>
)

5단계: 스크롤 상태 홀더 만들기

스크롤 상태를 유지하는 클래스를 만듭니다.

ScrollStateHolder.kt

import android.os.Bundle
import android.os.Parcelable
import androidx.recyclerview.widget.RecyclerView

/**
 * Persists scroll state for nested RecyclerViews.
 *
 * 1. Call [saveScrollState] in [RecyclerView.Adapter.onViewRecycled]
 * to save the scroll position.
 *
 * 2. Call [restoreScrollState] in [RecyclerView.Adapter.onBindViewHolder]
 * after changing the adapter's contents to restore the scroll position
 */
class ScrollStateHolder(savedInstanceState: Bundle? = null) {

    companion object {
        const val STATE_BUNDLE = "scroll_state_bundle"
    }

    /**
     * Provides a key that uniquely identifies a RecyclerView
     */
    interface ScrollStateKeyProvider {
        fun getScrollStateKey(): String?
    }

    /**
     * Persists the [RecyclerView.LayoutManager] states
     */
    private val scrollStates = hashMapOf<String, Parcelable>()

    /**
     * Keeps track of the keys that point to RecyclerViews
     * that have new scroll states that should be saved
     */
    private val scrolledKeys = mutableSetOf<String>()

    init {
        savedInstanceState?.getBundle(STATE_BUNDLE)?.let { bundle ->
            bundle.keySet().forEach { key ->
                bundle.getParcelable<Parcelable>(key)?.let {
                    scrollStates[key] = it
                }
            }
        }
    }

    fun setupRecyclerView(recyclerView: RecyclerView, scrollKeyProvider: ScrollStateKeyProvider) {
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    saveScrollState(recyclerView, scrollKeyProvider)
                }
            }

            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                val key = scrollKeyProvider.getScrollStateKey()
                if (key != null && dx != 0) {
                    scrolledKeys.add(key)
                }
            }
        })
    }

    fun onSaveInstanceState(outState: Bundle) {
        val stateBundle = Bundle()
        scrollStates.entries.forEach {
            stateBundle.putParcelable(it.key, it.value)
        }
        outState.putBundle(STATE_BUNDLE, stateBundle)
    }

    fun clearScrollState() {
        scrollStates.clear()
        scrolledKeys.clear()
    }

    /**
     * Saves this RecyclerView layout state for a given key
     */
    fun saveScrollState(
        recyclerView: RecyclerView,
        scrollKeyProvider: ScrollStateKeyProvider
    ) {
        val key = scrollKeyProvider.getScrollStateKey() ?: return
        // Check if we scrolled the RecyclerView for this key
        if (scrolledKeys.contains(key)) {
            val layoutManager = recyclerView.layoutManager ?: return
            layoutManager.onSaveInstanceState()?.let { scrollStates[key] = it }
            scrolledKeys.remove(key)
        }
    }

    /**
     * Restores this RecyclerView layout state for a given key
     */
    fun restoreScrollState(
        recyclerView: RecyclerView,
        scrollKeyProvider: ScrollStateKeyProvider
    ) {
        val key = scrollKeyProvider.getScrollStateKey() ?: return
        val layoutManager = recyclerView.layoutManager ?: return
        val savedState = scrollStates[key]
        if (savedState != null) {
            layoutManager.onRestoreInstanceState(savedState)
        } else {
            // If we don't have any state for this RecyclerView,
            // make sure we reset the scroll position
            layoutManager.scrollToPosition(0)
        }
        // Mark this key as not scrolled since we just restored the state
        scrolledKeys.remove(key)
    }

}

6단계: 어댑터 만들기

두 가지 어댑터가 있습니다.

(ㅏ). ChildAdapter.kt

이것은 내부 리사이클러 뷰 또는 중첩된 리사이클러 뷰에 대한 어댑터입니다.

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class ChildAdapter : RecyclerView.Adapter<ChildAdapter.VH>() {

    private var items = listOf<String>()

    fun setItems(list: List<String>) {
        this.items = list
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        return VH(
            LayoutInflater.from(parent.context).inflate(
                R.layout.nested_adapter_item,
                parent,
                false
            )
        )
    }

    override fun getItemCount(): Int = items.size

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

    class VH(view: View) : RecyclerView.ViewHolder(view) {

        private val textView: TextView = view.findViewById(R.id.textView)

        init {
            view.setOnClickListener {
                it.isSelected = !it.isSelected
            }
        }

        fun bind(item: String) {
            textView.text = item
        }

    }
}

(비). ParentAdapter.kt

이것은 외부 또는 상위 리사이클러 보기용 어댑터입니다.

import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.rubensousa.gravitysnaphelper.GravitySnapHelper

class ParentAdapter(private val scrollStateHolder: ScrollStateHolder) :
    RecyclerView.Adapter<ParentAdapter.VH>() {

    private var items = listOf<TitledList>()

    fun setItems(list: List<TitledList>) {
        this.items = list
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        val view = LayoutInflater.from(parent.context).inflate(
            R.layout.nested_adapter_list,
            parent, false
        )
        val vh = VH(view, scrollStateHolder)
        vh.onCreated()
        return vh
    }

    override fun getItemCount(): Int = items.size

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

    override fun onViewRecycled(holder: VH) {
        super.onViewRecycled(holder)
        holder.onRecycled()
    }

    override fun onViewDetachedFromWindow(holder: VH) {
        super.onViewDetachedFromWindow(holder)
        holder.onDetachedFromWindow()
    }

    class VH(view: View, private val scrollStateHolder: ScrollStateHolder) :
        RecyclerView.ViewHolder(view), ScrollStateHolder.ScrollStateKeyProvider {

        private val titleTextView: TextView = view.findViewById(R.id.nestedTitleTextView)
        private val recyclerView: RecyclerView = view.findViewById(R.id.nestedRecyclerView)
        private val layoutManager = LinearLayoutManager(
            view.context,
            RecyclerView.HORIZONTAL, false
        )
        private val adapter = ChildAdapter()
        private val snapHelper = GravitySnapHelper(Gravity.START)
        private var currentItem: TitledList? = null

        override fun getScrollStateKey(): String? = currentItem?.title

        fun onCreated() {
            recyclerView.adapter = adapter
            recyclerView.layoutManager = layoutManager
            recyclerView.setHasFixedSize(true)
            recyclerView.itemAnimator?.changeDuration = 0
            snapHelper.attachToRecyclerView(recyclerView)
            scrollStateHolder.setupRecyclerView(recyclerView, this)
        }

        fun onBound(item: TitledList) {
            currentItem = item
            titleTextView.text = item.title
            adapter.setItems(item.texts)
            scrollStateHolder.restoreScrollState(recyclerView, this)
        }

        fun onRecycled() {
            scrollStateHolder.saveScrollState(recyclerView, this)
            currentItem = null
        }

        /**
         * If we fast scroll while this ViewHolder's RecyclerView is still settling the scroll,
         * the view will be detached and won't be snapped correctly
         *
         * To fix that, we snap again without smooth scrolling.
         */
        fun onDetachedFromWindow() {
            if (recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE) {
                snapHelper.findSnapView(layoutManager)?.let {
                    val snapDistance = snapHelper.calculateDistanceToFinalSnap(layoutManager, it)
                    if (snapDistance!![0] != 0 || snapDistance[1] != 0) {
                        recyclerView.scrollBy(snapDistance[0], snapDistance[1])
                    }
                }
            }
        }
    }
}

7단계: MainActivity 만들기

다음은 MainActivity에 대한 코드입니다.

MainActivity.kt

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    private lateinit var adapter: ParentAdapter
    private lateinit var recyclerView: RecyclerView
    private lateinit var scrollStateHolder: ScrollStateHolder

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        recyclerView = findViewById(R.id.recyclerView)
        scrollStateHolder = ScrollStateHolder(savedInstanceState)
        adapter = ParentAdapter(scrollStateHolder)
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this)
        loadItems()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        scrollStateHolder.onSaveInstanceState(outState)
    }

    private fun loadItems() {
        val lists = arrayListOf<TitledList>()
        repeat(20) { listIndex ->
            val items = arrayListOf<String>()
            repeat(30) { itemIndex -> items.add(itemIndex.toString()) }
            lists.add(TitledList("List number $listIndex", items))
        }
        adapter.setItems(lists)
    }

}

운영

코드를 복사하거나 아래 링크에서 다운로드하여 빌드하고 실행합니다.

참조

참조 링크는 다음과 같습니다.

다운로드 예제