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

使用Scroll View实现滚动时子视图固定在屏幕顶部

实现滚动视图子视图滚动到顶部时固定为头部的效果

要实现这种滚动时子视图固定在顶部的效果,不同平台有对应的实现方式,下面分iOS和Android两种主流场景说明:

iOS 实现方案

UIKit 方式

核心逻辑是监听UIScrollView的滚动事件,计算子视图的位置,当目标子视图顶部即将超出屏幕时,将其移到一个悬浮容器中,同时调整滚动视图的内边距避免内容跳变。

具体操作步骤:

  • 先准备一个悬浮头部容器stickyHeaderView,初始设为隐藏,放到和UIScrollView同级的父视图里,确保层级在滚动视图之上。
  • 监听scrollViewDidScroll(_:)方法,在滚动时做以下判断:
    • 获取当前滚动偏移量contentOffset.y,再拿到目标子视图(比如示例中的B)在滚动视图里的顶部位置view.frame.origin.y。
    • 当contentOffset.y >= view.frame.origin.y时,说明B快要滚出顶部:
      • 把B从原滚动视图里移除,添加到stickyHeaderView中,设置好和屏幕同宽的frame,然后显示stickyHeaderView。
      • 给滚动视图设置contentInset.top为B的高度,保证下方的C视图不会突然上移。
    • 当滚动回退,contentOffset.y小于B原来的顶部位置时,把B移回原滚动视图,恢复它的原始frame,隐藏悬浮容器,同时重置滚动视图的内边距。

关键代码片段:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offsetY = scrollView.contentOffset.y
    // 提前保存B的原始frame
    guard let originalBFrame = originalBFrame else { return }
    let bViewTop = originalBFrame.origin.y
    
    if offsetY >= bViewTop {
        if !stickyHeaderView.subviews.contains(bView) {
            bView.removeFromSuperview()
            stickyHeaderView.addSubview(bView)
            bView.frame = CGRect(x: 0, y: 0, width: scrollView.frame.width, height: originalBFrame.height)
            stickyHeaderView.isHidden = false
            scrollView.contentInset = UIEdgeInsets(top: originalBFrame.height, left: 0, bottom: 0, right: 0)
        }
    } else {
        if stickyHeaderView.subviews.contains(bView) {
            bView.removeFromSuperview()
            scrollView.addSubview(bView)
            bView.frame = originalBFrame
            stickyHeaderView.isHidden = true
            scrollView.contentInset = .zero
        }
    }
}

SwiftUI 方式

用GeometryReader监听视图的全局位置,结合状态变量动态切换目标视图的显示状态——要么在滚动视图内正常滚动,要么悬浮在屏幕顶部。

具体操作步骤:

  • 定义@State变量isBStuck记录B是否需要固定,bViewHeight保存B的高度。
  • 在滚动视图里给B添加GeometryReader,监听它的全局位置,当顶部到达屏幕顶部时,把isBStuck设为true。
  • 用ZStack在滚动视图上方叠加一个悬浮的B视图,根据isBStuck的状态显示或隐藏,同时把滚动视图内的B视图设为透明。

关键代码片段:

struct StickyHeaderDemo: View {
    @State private var isBStuck = false
    @State private var bViewHeight: CGFloat = 0
    
    var body: some View {
        ZStack(alignment: .top) {
            ScrollView {
                VStack(spacing: 0) {
                    Text("视图A")
                        .frame(height: 200)
                        .background(Color.orange)
                    
                    // 滚动视图内的原B视图,固定时隐藏
                    Text("视图B")
                        .frame(height: bViewHeight)
                        .background(Color.blue)
                        .opacity(isBStuck ? 0 : 1)
                        .background(GeometryReader { geo in
                            Color.clear
                                .onAppear { bViewHeight = geo.size.height }
                                .onChange(of: geo.frame(in: .global).minY) { newValue in
                                    isBStuck = newValue <= 0
                                }
                        })
                    
                    Text("视图C")
                        .frame(height: 800)
                        .background(Color.green)
                }
            }
            
            // 悬浮的B视图,固定时显示
            if isBStuck {
                Text("视图B")
                    .frame(height: bViewHeight)
                    .background(Color.blue)
                    .frame(maxWidth: .infinity)
            }
        }
    }
}

Android 实现方案

RecyclerView 方式

利用自定义LinearLayoutManager监听滚动位置,配合悬浮头部视图实现固定效果。

具体操作步骤:

  • 先创建悬浮头部布局stickyHeader,添加到RecyclerView的父视图中,初始隐藏。
  • 自定义LinearLayoutManager,重写滚动相关方法:
    • 在setStickyHeaderView里测量头部高度并保存。
    • 监听滚动状态,当第一个可见Item是目标位置(比如B对应的position)时,显示悬浮头部。
    • 在滚动时调整悬浮头部的translationY,确保它始终固定在顶部,或者跟随滚动回退。

关键代码片段:

class StickyHeaderLinearLayoutManager(context: Context) : LinearLayoutManager(context) {
    private var stickyHeaderView: View? = null
    private var headerHeight = 0

    fun setStickyHeaderView(view: View) {
        stickyHeaderView = view
        // 测量头部高度
        view.measure(
            MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
            MeasureSpec.UNSPECIFIED
        )
        headerHeight = view.measuredHeight
    }

    override fun onScrollStateChanged(state: Int) {
        super.onScrollStateChanged(state)
        // 当第一个可见Item是位置1(假设B是第二个Item,position从0开始)时显示头部
        stickyHeaderView?.visibility = if (findFirstVisibleItemPosition() >= 1) View.VISIBLE else View.GONE
    }

    override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
        val scrollAmount = super.scrollVerticallyBy(dy, recycler, state)
        val firstVisibleView = findViewByPosition(findFirstVisibleItemPosition())
        
        if (firstVisibleView != null && findFirstVisibleItemPosition() == 1) {
            // 调整头部位置,避免被顶上去
            val top = firstVisibleView.top
            stickyHeaderView?.translationY = max(0f, top.toFloat() - headerHeight)
        }
        return scrollAmount
    }
}

内容的提问来源于stack exchange,提问作者Rahul Surendran

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 12:17:34