RecyclerView 实现WheelView和省市区多级联动

本文介绍了如何基于RecyclerView实现滚轮模块,包括对RecyclerView的低侵入改造、自定义样式、方向支持以及边框绘制。此外,还详细讲解了如何实现联动滚轮,包括数据加载、联动逻辑和位置设置,展示了如何应用于省市区选择和年月日选择的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者:丨小夕

前言

滚轮经常在选择中用到,主要包括类型选择省市区联动选择年月日联动选择等。

项目中的WheelView一般都是ScrollView+LinearLayout组合完成的。

但是自定义起来比较复杂,也有一些优秀的第三方库DateSelecter 通过Adapter的思想来灵活解决自定义的问题。

但是既然用到了Adapter的思想,那为啥不利用RecyclerView来实现呢?,毕竟我们比较熟悉RecyclerView.Adapter 也方便和项目中现有的Adapter复用。

于是我基于RecyclerView实现了一个滚轮模块,这些是他的基础功能:

  • RecyclerViewAdapter低侵入性,逻辑单独封装成RecyclerWheelViewModule
  • 支持通过Adapter自定义WheelView样式
  • 支持横向和竖向
  • 支持自定义WheelView边框

同时在滚轮模块的基础上,实现了联动滚轮View的封装。

效果

滚轮模块的用法也很简单,同时侵入性极低,使用拓展就能recyclerView.setupWheelModule()RecyclerView改造成WheelView

它会返回RecyclerWheelViewModule 里面包含了操作滚轮模块的各种API

class XxxActivity {
    val recyclerView: RecyclerView
    val wheelAdapter =
        BindingAdapter<String, ItemWheelVerticalBinding>(ItemWheelVerticalBinding::inflate) { _, item ->
            itemBinding.text.text = item
            itemBinding.text.setTextColor(if (isWheelItemSelected) Color.BLACK else Color.GRAY)
        }

    fun onCreate() {
        recyclerView.adapter = wheelAdapter

        val wheelModule = recyclerView.setupWheelModule()
        wheelModule.apply {
            offset = 1
            orientation = RecyclerWheelViewModule.VERTICAL
            setWheelDecoration(DefaultWheelDecoration(10.dp, 10.dp, 2.dp, "#dddddd".toColorInt()))
            onSelectChangeListener = {

            }
        }
    }

}

原理

WheelView的功能本身并不复杂,布局滚动都是RecyclerView已经处理好的。

因此我们只需要解决一些WheelView的特性即可。

本着代码越少,Bug越上的原则。绝大多数特性都尽量使用RecyclerView提供的API,或者官方已有的模块去实现。

实现一个WheelView的功能主要完成以下实现:

  • 选中的Item居中显示,在滚动后自动居中选中的位置
  • Item的最上和最下有一定滚动间距,来使得最边缘的Item可以居中,同时让WheelView 的尺寸恰好显示3个或5个Item
  • 支持绘制上下边界线来标识给用户滚轮选中区域
  • 用户滑动时,更新选中位置,并刷新Item数据,如加粗或者设置字体颜色为黑色
  • 支持代码设置和获取当前选中位置

Item居中

WheelView在滚动停止后,会自动使得当前最靠近中间的Item滚动到布局的中心位置。

官方提供了 SnapHelpe 来帮助我们处理Item的对齐。

而它的子类 LinearSnapHelper 可以监听RecyclerView的滚动,在滚动结束后,使得最接近RecyclerView视图中心的那个Item的中心对齐到视图中心,简单描述就是它能使Item居中

同时它也提供了很多有用的API的可重写的方法,我们通过重写onFling可以控制一下滚动速度。

class WheelLinearSnapHelper : LinearSnapHelper() {
    override fun onFling(velocityX: Int, velocityY: Int): Boolean =
        super.onFling(
            (velocityX * flingVelocityFactor).toInt(),
            (velocityY * flingVelocityFactor).toInt()
        )
}

上下留白

这里的留白是指,在WheelView的开始和结束位置有一定的空白,以便于最后一个Item能滚动到中心。

因为留白的存在,当RecyclerView滚动到最上面时,第一个Item 刚好处于中间位置

上下留白的数量我们定义为offset ,从另外一个角度来看可以将留白看成offsetHeaderoffsetFooter

这里的每个留白的高度一般就是Item的高度。

一般情况WheelView的每个Item的高度是一致的,我们取第一个Item的高度作为留白的高度。

以下是Header/FooterAdapter实现:

class OffsetAdapter(private val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        adapter.createViewHolder(parent, viewType)

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (adapter.itemCount > 0) {
            adapter.onBindViewHolder(holder, 0)
        }
        holder.itemView.visibility = View.INVISIBLE
        measureItemSize(holder.itemView) //测量和记录一下留白的高度
    }

    override fun getItemCount(): Int = offset
}

通过ConcatAdapter 依次连接HeaderAdapter+数据Adapter+FooterAdapter 就实现了留白功能。

在项目中,我们想给RecyclerView 的开始和结束加padding 也可以通过这种方式。

然后重新设置RecyclerViewAdapter

fun setAdapter(adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>?) {
    val startOffsetAdapter = OffsetAdapter(adapter)
    val endOffsetAdapter = OffsetAdapter(adapter)
    recyclerView.adapter = ConcatAdapter(
        startOffsetAdapter,
        adapter,
        endOffsetAdapter
    )
}

一般情况下我们的WheelView的高度都是中间选中Item的高度加上上下额外显示offsetItem的高度。

也就是一共显示offset+1+offsetItem

所以一般WheelView高度为(offset+1+offset)*itemSize

这里我们在OffsetAdapter测量出Item尺寸后顺便设置一下WheelView的高度。

fun measureItemSize(itemView: View) {
    //....
    itemSize = itemView.measuredHeight + margin
    recyclerView.layoutParams = recyclerView.layoutParams.apply {
        width = (offset + offset + 1) * itemSize
    }
}

绘制边界线

边框是一般指WheelView中间有2条边界线,用来标识WheelView 选中区域。用来告知用户,滚动到这2个边界线中间的是选中的Item

RecyclerView中绘制在Item之上的内容我们可以使用RecyclerView.ItemDecoration,所以几乎也不需要我们实现。

我们主要主要根据itemSize 去计算当前上下边框位置来绘制即可。

class DrawableWheelDecoration(
    val drawable: Drawable,
    @Px private val size: Int,
) : WheelDecoration() {
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值