使用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视图不会突然上移。
- 把B从原滚动视图里移除,添加到
- 当滚动回退,
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
相关产品推荐
相关产品推荐

