Android动态加载视图开发:ScrollView内嵌套含Lazy Loading水平RecyclerView的垂直RecyclerView
Alright, let's walk through building this nested scrollable layout with lazy loading step by step. I'll cover the layout structure, scroll conflict fixes, and the lazy loading implementation for both the vertical and horizontal RecyclerViews.
First, we'll use NestedScrollView instead of a regular ScrollView—it's designed to handle nested scrollable components better and avoids common scroll conflicts. Inside it, we'll stack the ViewPager, AdView, and vertical RecyclerView in a vertical LinearLayout.
Here's the core layout code:
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="8dp"> <!-- Top ViewPager --> <androidx.viewpager2.widget.ViewPager2 android:id="@+id/mainViewPager" android:layout_width="match_parent" android:layout_height="200dp" android:layout_marginBottom="12dp"/> <!-- AdView --> <com.google.android.gms.ads.AdView android:id="@+id/mainAdView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="12dp"/> <!-- Vertical RecyclerView (holds horizontal lists) --> <androidx.recyclerview.widget.RecyclerView android:id="@+id/verticalRecyclerView" android:layout_width="match_parent" android:layout_height="wrap_content" android:nestedScrollingEnabled="false"/> </LinearLayout> </androidx.core.widget.NestedScrollView>
Key note: Setting android:nestedScrollingEnabled="false" on the vertical RecyclerView lets the NestedScrollView handle scrolling, preventing jank and conflict between the two scrollable views.
The vertical RecyclerView will act as a container for sections, each containing a horizontal RecyclerView. We'll start with its adapter and view holder:
Vertical Adapter Code
class VerticalSectionAdapter(private val sectionList: List<SectionData>) : RecyclerView.Adapter<VerticalSectionAdapter.SectionViewHolder>() { inner class SectionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val sectionTitle: TextView = itemView.findViewById(R.id.sectionTitle) val horizontalRecyclerView: RecyclerView = itemView.findViewById(R.id.sectionHorizontalRV) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_vertical_section, parent, false) return SectionViewHolder(view) } override fun onBindViewHolder(holder: SectionViewHolder, position: Int) { val currentSection = sectionList[position] holder.sectionTitle.text = currentSection.sectionName // Configure horizontal RecyclerView for this section val horizontalLayoutManager = LinearLayoutManager( holder.itemView.context, LinearLayoutManager.HORIZONTAL, false ) holder.horizontalRecyclerView.layoutManager = horizontalLayoutManager val horizontalAdapter = HorizontalItemAdapter(currentSection.items) holder.horizontalRecyclerView.adapter = horizontalAdapter // Attach lazy loading listener to the horizontal RecyclerView setupHorizontalLazyLoading(holder.horizontalRecyclerView, horizontalAdapter, currentSection) } override fun getItemCount() = sectionList.size // Handle lazy loading trigger for horizontal lists private fun setupHorizontalLazyLoading( recyclerView: RecyclerView, adapter: HorizontalItemAdapter, section: SectionData ) { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) val layoutManager = recyclerView.layoutManager as LinearLayoutManager val visibleItemCount = layoutManager.childCount val totalItemCount = layoutManager.itemCount val firstVisibleItemPos = layoutManager.findFirstVisibleItemPosition() // Trigger load more if we're near the end and not already loading if (!adapter.isLoading && (visibleItemCount + firstVisibleItemPos) >= totalItemCount - 3 && // Load 3 items before end firstVisibleItemPos >= 0) { adapter.showLoading() // Simulate API call to fetch more items section.loadMoreItems { newItems -> adapter.addNewItems(newItems) adapter.isLoading = false } } } }) } }
Vertical Section Item Layout (item_vertical_section.xml)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingVertical="8dp"> <TextView android:id="@+id/sectionTitle" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="18sp" android:textStyle="bold" android:marginBottom="8dp"/> <androidx.recyclerview.widget.RecyclerView android:id="@+id/sectionHorizontalRV" android:layout_width="match_parent" android:layout_height="140dp"/> </LinearLayout>
Each horizontal list needs to handle lazy loading, including showing a loading state when fetching more items. Here's the adapter implementation:
Horizontal Item Adapter Code
class HorizontalItemAdapter(private var itemList: List<Any>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var isLoading = false private val VIEW_TYPE_ITEM = 0 private val VIEW_TYPE_LOADING = 1 inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val itemContent: TextView = itemView.findViewById(R.id.itemContent) } inner class LoadingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val progressBar: ProgressBar = itemView.findViewById(R.id.loadingProgress) } override fun getItemViewType(position: Int): Int { return if (itemList[position] is String) VIEW_TYPE_ITEM else VIEW_TYPE_LOADING } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return if (viewType == VIEW_TYPE_ITEM) { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_horizontal, parent, false) ItemViewHolder(view) } else { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_loading, parent, false) LoadingViewHolder(view) } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (holder is ItemViewHolder) { val item = itemList[position] as String holder.itemContent.text = item } else if (holder is LoadingViewHolder) { holder.progressBar.isIndeterminate = true } } override fun getItemCount() = itemList.size fun addNewItems(newItems: List<String>) { // Remove loading placeholder first, then add new items val updatedList = mutableListOf<Any>().apply { addAll(itemList.filter { it is String }) addAll(newItems) } itemList = updatedList notifyDataSetChanged() } fun showLoading() { if (!isLoading) { val updatedList = mutableListOf<Any>().apply { addAll(itemList) add("LOADING") // Dummy object for loading view } itemList = updatedList notifyItemInserted(itemList.size - 1) isLoading = true } } }
Horizontal Item Layouts
item_horizontal.xml(regular content item):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="120dp" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:marginEnd="8dp" android:background="@drawable/item_rounded_border"> <TextView android:id="@+id/itemContent" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp"/> </LinearLayout>
item_loading.xml(loading placeholder):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="120dp" android:layout_height="match_parent" android:gravity="center" android:marginEnd="8dp"> <ProgressBar android:id="@+id/loadingProgress" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
- Scroll Performance: Using
NestedScrollView+ disabling nested scrolling on the vertical RecyclerView ensures smooth scrolling without conflicts. - Prevent Duplicate Loads: The
isLoadingflag in the horizontal adapter stops multiple simultaneous load requests when scrolling quickly. - Data Management: Use a ViewModel to handle data loading for each section—this avoids memory leaks and ensures data survives configuration changes.
- Reusability: You can cache horizontal adapters in the vertical adapter if sections are reused, to reduce object creation overhead.
内容的提问来源于stack exchange,提问作者moody_user

