You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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.

1. Layout Structure Setup

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.

2. Vertical RecyclerView Setup

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>
3. Horizontal RecyclerView with Lazy Loading

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>
4. Key Optimizations & Notes
  • Scroll Performance: Using NestedScrollView + disabling nested scrolling on the vertical RecyclerView ensures smooth scrolling without conflicts.
  • Prevent Duplicate Loads: The isLoading flag 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 10:39:27