作者:丨小夕
前言
滚轮经常在选择中用到,主要包括类型选择、省市区联动选择、年月日联动选择等。
项目中的WheelView
一般都是ScrollView+LinearLayout
组合完成的。
但是自定义起来比较复杂,也有一些优秀的第三方库DateSelecter 通过Adapter
的思想来灵活解决自定义的问题。
但是既然用到了Adapter
的思想,那为啥不利用RecyclerView
来实现呢?,毕竟我们比较熟悉RecyclerView.Adapter
也方便和项目中现有的Adapter
复用。
于是我基于RecyclerView
实现了一个滚轮模块,这些是他的基础功能:
- 对
RecyclerView
,Adapter
低侵入性,逻辑单独封装成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
,从另外一个角度来看可以将留白看成offset
个Header
和offset
个Footer
这里的每个留白的高度一般就是Item
的高度。
一般情况WheelView
的每个Item
的高度是一致的,我们取第一个Item
的高度作为留白的高度。
以下是Header/Footer
的Adapter
实现:
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 也可以通过这种方式。
然后重新设置RecyclerView
的Adapter
。
fun setAdapter(adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>?) {
val startOffsetAdapter = OffsetAdapter(adapter)
val endOffsetAdapter = OffsetAdapter(adapter)
recyclerView.adapter = ConcatAdapter(
startOffsetAdapter,
adapter,
endOffsetAdapter
)
}
一般情况下我们的WheelView
的高度都是中间选中Item
的高度加上上下额外显示offset
个Item
的高度。
也就是一共显示offset+1+offset
个Item
。
所以一般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)