背景
最近公司有个需求,小程序需要展示用户账单明细。要求:title滑动至顶部时固定;日期可筛选。
效果
实现
思路
页面滚动时要知道窗口最高点在页面的什么地方,也就是scrollTop的位置,还需要知道每个月份账单(即下面的bill-details-column)的top和bottom所处的位置,如果scrollTop在这top和bottom之间,则需要固定title。
组件
这里使用的ScrollView作为容器,不用View的原因是弹出日历组件时会有滑动穿透问题;相应的useReachBottom等api也无法使用。
页面结构
父组件-容器
<ScrollView className='bill-list-column'>
{bills.map((item, index) => (
<View key={item.title}>
<BillDetails />
</View>
)}
</ScrollView>
子组件 - BillDetails
<View className='bill-details-column'>
// 需要固定的title
<View className='details-title' >
</View>
<View className='details-content'>
// 月份下账单内容
</View>
// 日期组件
<DatePicker />
</View>
获取当月账单的高度
可以使用createSelectorQuery方法返回一个, SelectorQuery 对象实例,容器高度、top、left、right、bootom位置等信息都可以拿到。
// 先定义一个方法
const selectorQueryArray = (
selector: string,
): Promise<TaroGeneral.IAnyObject> =>
new Promise(resolve => {
Taro.createSelectorQuery()
.selectAll(selector)
.boundingClientRect(res => {
resolve(res)
})
.exec()
})
// 调用,这里必须要使用 Taro.nextTick,很重要,否则拿不到数据
const [nodes, setNodes] = useState<TaroGeneral.IAnyObject>([])// 节点信息
Taro.nextTick(async () => {
const detailsNode = await selectorQueryArray('.bill-details-column')
const titleNode = await selectorQueryArray('.details-title') // title信息也需要,这里后面再说
const nodesArr =
detailsNode.map((item, index) => ({
columnTop: item.top,
columnBottom: item.bottom,
columnHeight: item.height,
titleHeight: titleNode[index].height,
}))
//赋值
setNodes(nodesArr)
})
获取scrollTop的位置
ScrollView的onScroll方法监听页面滚动时触发,返回的res.detail.scrollTop 就是我们需要的值
const [fixedIndex, setFixedIndex] = useState() // 需要固定的title下标
const onScroll = res => {
if (!nodes.length) return
const index = nodes.findIndex(
({ columnTop, columnBottom }) =>
res.detail.scrollTop >= columnTop &&
res.detail.scrollTop <= columnBottom,
)
setFixedIndex(index)
}
固定title
- 再判断需要固定的下标和bills map生成BillDetails的下标是否相等,相等则需要固定。这里两个是一样的,即nodes的个数和BillDetails的个数是相等的。
- details-title固定后会脱离文档流,下面的content内容区域会被title遮挡,这里加个padding就好了,padding的高度就是前面获取到的titleNode
// 子组件内部-BillDetail
// isTitleFixed:是否需要固定title,父组件传入
// node.titleHeight
const titleStyle: CSSProperties = useMemo(() => {
if (isTitleFixed) {
return { position: 'fixed', top: 0, width: '100%' }
}
return { position: 'relative' }
}, [isTitleFixed])
return
<View className='details-title' style={titleStyle}></View>
<View
className='details-content'
style={{ paddingTop: isTitleFixed ? node?.titleHeight : 0 }}
></View>
到这里核心功能就都已经完成了,其余是一些优化就放在下面的整体代码里了
所有代码
// 父组件
const [nodes, setNodes] = useState<TaroGeneral.IAnyObject>([])
const [fixedIndex, setFixedIndex] = useState() // 需要固定的title下标
const [scrollTop, setScrollTop] = useState<number | undefined>(undefined)
useEffect(() => {
Taro.nextTick(async () => {
const detailsNode = await selectorQueryArray('.bill-details-column')
const titleNode = await selectorQueryArray('.details-title')
const nodesArr =
detailsNode.map((item, index) => ({
columnTop: item.top,
columnBottom: item.bottom,
columnHeight: item.height,
titleHeight: titleNode[index].height,
}))
setNodes(nodesArr)
})
}, [])
const selectorQueryArray = (
selector: string,
): Promise<TaroGeneral.IAnyObject> =>
new Promise(resolve => {
Taro.createSelectorQuery()
.selectAll(selector)
.boundingClientRect(res => {
resolve(res)
})
.exec()
})
const pageScroll = throttle(res => {
if (!nodes.length) return
setScrollTop(undefined) // 滚动时设置为undefined
const index = nodes.findIndex(
({ columnTop, columnBottom }) =>
res.detail.scrollTop >= columnTop &&
res.detail.scrollTop <= columnBottom,
)
setFixedIndex(index)
}, 500)
const updateData = () => {
// 可以替代 useReachBottom
console.log('onScrollToLower', pageHeight)
}
return (
<ScrollView
className='bill-list-column'
scrollY
enhanced
enableFlex
scrollWithAnimation
scrollTop={scrollTop}
onScroll={pageScroll}
onScrollToLower={updateData}
>
{
bills.map((item, index) => (
<View key={item.title}>
<BillDetails
node={nodes[index]}
isTitleFixed={fixedIndex === index}
billItem={item}
onDateConfirm={start => {
setScrollTop(0) // 切换日期是回到顶部
}}
/>
</View>
))
}
</ScrollView>
)
// 子组件 BillDetails
interface BillDetailsProps {
billItem: BillItem
isTitleFixed: boolean
node: any
onDateConfirm: (start: string) => void
}
const titleStyle: CSSProperties = useMemo(() => {
if (isTitleFixed) {
return { position: 'fixed', top: 0, width: '100%' }
}
return { position: 'relative' }
}, [isTitleFixed])
return (
<View className='bill-details-column'>
<View className='details-title' style={titleStyle}>
</View>
<View
className='details-content'
style={{ paddingTop: isTitleFixed ? node?.titleHeight : 0 }}
>
</View>
<DatePicker />
</View>
)
总结
有了思路整体来说难度不算很大,核心点在于获取子组件高度并判断窗口最高点在哪个子组件中。以上就是所有内容了,如有问题或者不合理的地方欢迎指正。