Dans ce tutoriel, vous allez apprendre, étape par étape, à créer une vue recyclée imbriquée. Cela signifie que vous placez une vue recyclée à l'intérieur d'une autre vue recyclée. Chaque vue de recyclage doit disposer de son propre adaptateur pour lier ses données. Chacune doit également gérer son défilement de manière appropriée.
Regardez les exemples ci-dessous.
Exemple 1 : Kotlin Android Nested RecyclerView
Voici la démo de ce projet :
Etape 1 : Créer le projet
Commencez par créer un projet Android Studio
vide.
Étape 2 : Dépendances
Installez GravitySnapHelper en ajoutant la déclaration d'implémentation suivante dans votre fichier app/build.gradle
:
implementation 'com.github.rubensousa:gravitysnaphelper:2.1.0'
Étape 3 : Conception des mises en page
Il y a trois mises en page pour ce projet :
nested_adapter_item.xml
Il s'agit de la mise en page pour l'élément 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>
(b). nested_adapter_list
La mise en page pour la vue recyclée intérieure. Ajoutez 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>
(c). nested_adapter_item.xml
La mise en page pour la vue recyclée extérieure. Ajoutez une fois de plus l'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>
Étape 4 : Créer la classe de données
Cette classe de données sera notre classe de modèle :
(a). TiledList.kt
data class TitledList(
val title: String,
val texts: MutableList<String>
)
Étape 5 : Créer le support de l'état de défilement
Créez une classe pour contenir l'état de défilement :
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)
}
}
Étape 6 : Création d'adaptateurs
Il y a deux adaptateurs :
(a). ChildAdapter.kt
C'est l'adaptateur pour la vue recyclée intérieure, ou la vue recyclée imbriquée :
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
}
}
}
(b). ParentAdapter.kt
C'est l'adaptateur pour la vue recyclée externe ou parent :
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])
}
}
}
}
}
}
Étape 7 : Création de la MainActivity
Voici le code de l'activité principale (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)
}
}
Exécuter
Copier le code ou le télécharger dans le lien ci-dessous, construire et exécuter.
Référence
Voici les liens de référence :
Télécharger Exemple