Android如何实现滚动时可折叠展开的水平RecyclerView/ViewPager
RecyclerView滚动联动展开/折叠列表项实现方案
实现这个交互不需要引入第三方库,按以下三步做就能跑通:
第一步:适配列表项双状态布局
给你的列表项数据结构增加一个布尔类型的状态标记,比如isExpanded:- 展开态:展示完整内容,包括大图、多行文本、操作按钮等元素
- 折叠态:只展示核心缩略信息,比如小尺寸头像、单行标题
两种状态的控件放在同一个item布局文件里即可,通过控制控件的visibility属性切换,不需要写两套独立布局。
两种状态的效果参考:

第二步:监听滚动方向
给RecyclerView添加滚动监听器,通过滚动的垂直偏移量判断滚动方向,注意加滚动阈值防抖,避免轻微触碰就触发状态切换:// 先获取系统默认的触摸滑动阈值,用来做防抖 val touchSlop = ViewConfiguration.get(context).scaledTouchSlop // 标记当前全局列表项的状态,默认展开 private var isAllItemExpanded = true recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) // 滑动距离小于阈值不触发状态切换 if (abs(dy) < touchSlop) return when { // dy>0 代表手指向上滑,屏幕内容向下滚动,此时切折叠态 dy > 0 && isAllItemExpanded -> { updateItemExpandState(false) } // dy<0 代表手指向下滑,屏幕内容向上滚动,此时切展开态 dy < 0 && !isAllItemExpanded -> { updateItemExpandState(true) } } } })Java版本逻辑完全一致,只是语法写法不同。
第三步:高效更新列表状态+过渡动画
不要每次状态变化都调用notifyDataSetChanged()全量刷新,会造成卡顿和闪烁。只需要更新当前屏幕内可见的列表项即可,屏幕外的item在滑入的时候会在绑定方法里自动读取最新状态。配合TransitionManager可以自动生成顺滑的展开折叠动画:// 定义payload标记,用来做局部刷新 val PAYLOAD_EXPAND_STATE = "expand_state_change" private fun updateItemExpandState(targetExpanded: Boolean) { isAllItemExpanded = targetExpanded // 更新所有数据源的状态 dataList.forEach { it.isExpanded = targetExpanded } val lm = recyclerView.layoutManager as LinearLayoutManager val firstVisiblePos = lm.findFirstVisibleItemPosition() val lastVisiblePos = lm.findLastVisibleItemPosition() // 只刷新当前可见范围内的item,用payload做局部刷新 adapter.notifyItemRangeChanged( firstVisiblePos, lastVisiblePos - firstVisiblePos + 1, PAYLOAD_EXPAND_STATE ) }之后在Adapter里重写带payload参数的
onBindViewHolder方法,收到状态更新的payload时,只做控件显隐切换,同时开启动画过渡即可:override fun onBindViewHolder(holder: MyViewHolder, position: Int, payloads: MutableList<Any>) { if (payloads.contains(PAYLOAD_EXPAND_STATE)) { // 开启动画过渡 TransitionManager.beginDelayedTransition(holder.itemView as ViewGroup) // 根据当前状态切换控件显隐 val item = dataList[position] holder.bigCover.visibility = if (item.isExpanded) View.VISIBLE else View.GONE holder.descText.visibility = if (item.isExpanded) View.VISIBLE else View.GONE holder.actionBtnGroup.visibility = if (item.isExpanded) View.VISIBLE else View.GONE return } // 没有payload走正常的全量绑定逻辑 super.onBindViewHolder(holder, position, payloads) }
注意事项
- 快速滑动时如果频繁触发状态更新,可以加一个时间节流阀,比如两次状态切换间隔不短于300ms,避免动画冲突
- 如果列表项里有图片,折叠态和展开态尽量用同一张图片资源做缩放,不要切不同的图,避免切换时闪烁
- 如果用的是GridLayoutManager或者StaggeredGridLayoutManager,找可见位置的方法换成对应LayoutManager的方法即可,逻辑不变
内容的提问来源于stack exchange,提问作者Mehrzad
相关产品推荐
相关产品推荐

