其实 TaroUI 框架自带索引选择器 AtIndexes,但是由于 AtIndexes 组件的列表只能接收文本传参,不能自定义内容。我们的业务需求需要自定义每个 item 的内容,每个 item 里需要添加多个标签。在看了 AtIndexes 组件相关源码和咨询完 TaroUI 的官方大佬后,AtIndexes 是基于 ScrollView, 决定自己也基于 ScrollView 实现一个索引选择器。
基础版
实现
AtIndexes的功能,通过点击索引导航,快速定位到列表处。
实现思路:
- 格式化重组数据,把数据按照每个字符下的数据格式化,如:
[
{
title: 'A',
items: [...]
},
{
title: 'B',
items: [...]
},
...
]
- 格式化数据时,把字母索引单独取出并排序。之所以不用固定的 A-Z,是因为并不能保证每个字母索引下都有数据。
- 布局,字母索引定位在屏幕视口右侧并垂直居中。左侧列表按照字母为
title,并用li标签循环渲染items内的每条数据
ScrollView 组件的参数设置:
<ScrollView
className='scrollview'
scrollY
enableBackToTop
scrollWithAnimation
scrollIntoView={toView}
scrollTop={scrollTop}
style={scrollStyle}
>
···
</ScrollView>
scrollY:允许纵向滚动。enableBackToTop:iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,只支持竖向。- `scrollWithAnimation``:在设置滚动条位置时使用动画过渡。
scrollIntoView: 值应为某子元素id,设置哪个方向可滚动,则在哪个方向滚动到该元素。scrollTop:设置竖向滚动条位置。style:使用竖向滚动时,需要给ScrollView一个固定高度。
左侧每个索引列表区块的 id 和 className 设置:
id={`data-${item.title}`}
className={`list-items-${item.title}`}
主要事件:
- 右侧索引导航的点击事件,获取点击的导航内容,设置列表区块对应的
id:
clickLetter(key, index) {
const keyText = `data-${key}`
this.setState({
toView: keyText
})
}
- 左侧
ScrollView组件通过设置scrollIntoView={toView},列表滚动到该id区块元素,达到快速定位的效果。
进阶版
基础版只是实现了简单的点击索引导航,快速定位的效果,显得有些生硬,在项目 1.5 版本迭代的时候,我增加了一些效果。
1、通过 onScroll 事件监听 ScrollView 的纵向滚动位置,使右侧对应索引高亮。
2、手指在屏幕上点击右侧索引导航并移动时(touchMove),左侧实现滚动到对应导航的列表区块。
在原有功能上的实现思路:
- 获取左侧每个列表区块的高度(获取
DOM元素高度的兼容方法:H5、微信小程序Taro.createSelectorQuery(),支付宝小程序my.createSelectorQuery()):
// 通过 class 或者 id 获取 DOM 元素的 height
querySelect(nodeSelect) {
const query =
process.env.TARO_ENV === 'alipay'
? // eslint-disable-next-line no-undef
my.createSelectorQuery()
: Taro.createSelectorQuery().in(this.$scope)
return new Promise(function(resolve) {
query
.select(nodeSelect)
.boundingClientRect()
.exec(rect => {
if (rect && rect[0]) {
resolve(rect[0].height)
}
})
})
}
- 计算每个列表区块的高度区间:
// stationListToJS 是格式化重组后的数据
_getListItemsHeight(stationListToJS) {
const listItemsPromise = new Promise(resolve => {
if (stationListToJS.length > 0) {
let arr = []
let listHeights = []
// 通过 querySelect 方法获取所有 class 为 .list-items-${item.title} 的 DOM 高度
stationListToJS.forEach(item => {
arr.push(this.querySelect(`.list-items-${item.title}`))
})
Promise.all([...arr]).then(res => {
// 高度区间从 0 开始计算
let height = 0
listHeights.push(height)
for (let i = 0; i < res.length; i++) {
// 高度累加,根据 纵向滚动的值
height += res[i]
listHeights.push(height)
}
resolve(listHeights)
})
}
})
return listItemsPromise
}
- 在
componentDidMount生命周期中计算右侧每个索引导航的高度:
if (toJS(letter).length > 0) {
this.querySelect('.letter').then(res => {
this.setState({
letterDOMHeight: res
})
})
}
- 在
componentDidMount生命周期中调用计算列表区块的高度区间的方法:
// 在保证有数据的情况下,渲染 DOM 元素后获取列表区块的高度区间
const stationListToJS = toJS(stationList)
if (stationListToJS.length > 0) {
this._getListItemsHeight(stationListToJS).then(res => {
this.setState({
DOMHeight: [...res]
})
})
}
onScroll监听左侧ScrollView组件纵向滚动的值e.detail.scrollTop,计算e.detail.scrollTop落在DOMHeight数据的哪个区间,获取索引值,设置右侧索引导航的高亮:
// 滚动列表,高亮对应的字母
onScroll(e) {
const { DOMHeight } = this.state
for (let i = 0; i < DOMHeight.length - 1; i++) {
let height1 = DOMHeight[i]
let height2 = DOMHeight[i + 1]
if (e.detail.scrollTop >= height1 && e.detail.scrollTop < height2) {
// currentLetterIndex 右侧索引导航的高亮
this.setState({
currentLetterIndex: i
})
return
}
}
}
- 右侧索引导航添加
touchStart、touchMove事件,监听并计算出在索引导航上移动的距离和移动了几个索引值。 touchStart事件:
// 监听开始滑动的位置
touchStartLetter(e) {
// 由对应的 id 获取到该位置的字母和索引值
// 索引导航设置的 id id={`letter-${item}-${index}`}
const targetArr = e.target.id.split('-')
const letterText = targetArr[1]
const index = targetArr[2]
const keyText = `data-${letterText}`
// e.touches[0].pageY 记录开始滑动的位置
// startIndex touchStart 事件接触到的索引值
// currentLetterIndex 当前索引导航的索引值
this.setState({
toView: keyText,
currentLetterIndex: index,
startIndex: index,
touchStartPageY: e.touches[0].pageY
})
}
touchMove事件:
// 监听滑动并高亮对应的索引和滑动到对应的列表
touchMoveLetter(e) {
e.stopPropagation()
e.preventDefault()
const { touchStartPageY, letterDOMHeight, startIndex, DOMHeight } = this.state
const letter = toJS(this.props.letter)
// 由 滑动的距离 / 每个索引的额高度,计算滑动了多少个索引
let diff = ((e.touches[0].pageY - touchStartPageY) / letterDOMHeight) | 0
// 处理滑动到最上和最下的边界值
// eslint-disable-next-line radix
let curIndex = parseInt(startIndex) + diff
if (curIndex < 0) {
curIndex = 0
} else if (curIndex > DOMHeight.length - 2) {
curIndex = DOMHeight.length - 2
}
// 根据计算出的索引值从 letter 字母数组中找到对应的 字母
let keyText = `data-`
letter.forEach((item, index) => {
if (curIndex === index) {
keyText = keyText + item
}
})
this.setState({
toView: keyText,
currentLetterIndex: curIndex
})
}
DOM 元素设置 id 或 className
- 右侧索引导航设置的属性:
<View className='letter-wrapper'>
{letter.length > 0 &&
letter.map((item, index) => {
return (
<View
className={
index === Number(currentLetterIndex) ? 'letter active' : 'letter'
}
key={item}
id={`letter-${item}-${index}`}
onClick={this.clickLetter.bind(this, item, index)}
onTouchStart={this.touchStartLetter}
onTouchMove={this.touchMoveLetter}
>
{item}
</View>
)
})}
</View>
- 左侧列表区块 DOM 设置的属性:
<View
key={item.title + index}
className={`list-items list-items-${item.title}`}
id={`data-${item.title}`}
>
···
</View>
END
记录一下实现索引选择器的解题和一些值的计算思路。