如何为RecyclerView实现自定义下拉刷新(非原生Swipe to refresh)
Android自定义下拉刷新布局实现方案
核心实现思路
基于AndroidX的NestedScrolling系列接口实现,无需依赖原生SwipeRefreshLayout,可兼容所有实现了嵌套滚动规范的可滚动子组件(RecyclerView、NestedScrollView等),完全匹配动图的交互逻辑。
第一步:明确交互对齐规则
先确认所有和演示效果一致的交互点:
- 子View滚动到顶部后才响应下拉手势,滚动过程无卡顿
- 下拉时头部跟随手势同步显示,拖动存在阻尼感,不会跟着手指距离等比例滑动
- 下拉距离未达刷新阈值时,松手直接回弹收起头部,不触发刷新
- 下拉距离超过刷新阈值时,松手后头部固定在刷新位置,触发刷新回调
- 刷新过程中屏蔽下拉手势,刷新完成后自动平滑收起头部
- 头部动画跟随拖动进度实时更新,刷新阶段播放循环加载动画
第二步:自定义刷新容器核心代码实现
2.1 基础布局结构
自定义ViewGroup包含两个固定子View:刷新头部、内容容器,初始状态下头部偏移到屏幕外不可见
class CustomRefreshLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ViewGroup(context, attrs, defStyleAttr), NestedScrollingParent3 { // 基础配置参数,可根据需求调整 private val headerHeight = dp2px(60) // 刷新头部高度 private val refreshThreshold = dp2px(50) // 触发刷新的阈值 private val maxDragDistance = dp2px(90) // 最大拖动距离 private val dragRate = 0.5f // 拖动阻尼系数,拉2px显示1px提升手感 private var currentOffset = 0 // 当前头部偏移量 private var isRefreshing = false // 刷新状态标记 var onRefreshListener: (() -> Unit)? = null // 刷新回调 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) // 测量内容区域 val contentView = getChildAt(1) measureChild(contentView, widthMeasureSpec, heightMeasureSpec) // 测量刷新头部 val headerView = getChildAt(0) measureChild(headerView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(headerHeight, MeasureSpec.EXACTLY)) setMeasuredDimension(measuredWidth, measuredHeight) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { val header = getChildAt(0) val content = getChildAt(1) // 头部初始布局在内容区域上方,屏幕外不可见 header.layout(0, -headerHeight, r, 0) content.layout(0, 0, r, b - t) } }
2.2 嵌套滚动处理
接管子View滚动到顶部后的下拉手势,消费滚动距离更新头部位置
override fun onNestedPreScroll( target: View, dx: Int, dy: Int, consumed: IntArray, type: Int ) { // dy<0代表下拉手势,仅当未在刷新、子View已经滚动到顶部时处理 if (dy < 0 && !isRefreshing && !target.canScrollVertically(-1)) { val newOffset = (currentOffset - dy * dragRate).toInt().coerceAtMost(maxDragDistance) if (newOffset > 0) { consumed[1] = dy // 消费当前滚动距离,子View不再滚动 currentOffset = newOffset updateHeaderPosition() updateHeaderProgress() } } } // 更新头部位置 private fun updateHeaderPosition() { val header = getChildAt(0) header.translationY = currentOffset.toFloat() } // 跟随拖动进度更新头部UI,比如箭头旋转、进度条填充等 private fun updateHeaderProgress() { val progress = (currentOffset.toFloat() / refreshThreshold).coerceAtMost(1f) (getChildAt(0) as RefreshHeader).updateProgress(progress) }
2.3 手势松开逻辑处理
根据当前拖动距离判断是触发刷新还是回弹取消
override fun onStopNestedScroll(target: View, type: Int) { if (isRefreshing) return if (currentOffset >= refreshThreshold) { // 超过刷新阈值,触发刷新 setRefreshing(true) onRefreshListener?.invoke() } else { // 未到阈值,回弹收起头部 animateOffsetTo(0) } } // 平滑滚动到目标偏移量 private fun animateOffsetTo(targetOffset: Int) { ValueAnimator.ofInt(currentOffset, targetOffset).apply { duration = 300 interpolator = DecelerateInterpolator() addUpdateListener { anim -> currentOffset = anim.animatedValue as Int updateHeaderPosition() } start() } } // 对外暴露的刷新状态设置方法 fun setRefreshing(refreshing: Boolean) { if (isRefreshing == refreshing) return isRefreshing = refreshing if (refreshing) { animateOffsetTo(refreshThreshold) (getChildAt(0) as RefreshHeader).startLoadingAnim() } else { (getChildAt(0) as RefreshHeader).stopLoadingAnim() animateOffsetTo(0) } }
第三步:刷新头部实现
自定义头部View,根据进度更新UI即可,示例逻辑如下:
class RefreshHeader @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { private val arrowIv: ImageView private val progressBar: ProgressBar init { // 加载头部布局,包含箭头、加载动画等元素 inflate(context, R.layout.layout_refresh_header, this) arrowIv = findViewById(R.id.arrow_iv) progressBar = findViewById(R.id.progress_bar) } // 拖动阶段更新进度 fun updateProgress(progress: Float) { arrowIv.rotation = progress * 180 // 箭头随进度旋转180度 progressBar.progress = (progress * 100).toInt() } // 刷新阶段启动循环动画 fun startLoadingAnim() { arrowIv.visibility = GONE progressBar.visibility = VISIBLE // 启动旋转动画 } // 刷新结束停止动画 fun stopLoadingAnim() { arrowIv.visibility = VISIBLE progressBar.visibility = GONE // 停止旋转动画 } }
第四步:使用方式
布局中直接嵌套即可:
<com.example.CustomRefreshLayout android:id="@+id/refresh_layout" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.RefreshHeader android:layout_width="match_parent" android:layout_height="wrap_content"/> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent"/> </com.example.CustomRefreshLayout>
代码中设置刷新监听:
refresh_layout.onRefreshListener = { // 执行业务刷新逻辑,比如请求网络数据 // 刷新完成后调用 refresh_layout.setRefreshing(false) 收起头部 }
内容的提问来源于stack exchange,提问作者Mustufa Ansari
相关产品推荐
相关产品推荐

