开发 react 中,突然碰到一个效果,定制垂直滚动条,且需要在电脑端和移动端也能操作,此时发现 mobile-ant 不能用了,虽然没有适配过移动端,但尝试一下也不复杂
经过简单梳理,大致经过3个步骤:事件选择、进度处理、UI显示
事件选择
由于要同时在电脑和移动端显示,因此事件的选择至少要两种,当时就选择了下面 6 种事件,用来处理进度条
后面试用了,效果不太好,这个移动事件都是作用在组件上的,因此一旦出了组件,就结束了,因此滑动时很影响体验(至少比起 mobile-ant效果差很多),因此需要优化,考虑的结果就是:
组件触摸事件 + 全局移动、结束事件
突然间效果就好了很多,这样拖动的时候,即使出了组件区域,也能在对应方向上滑动
<div
onMouseDown={onStart}
//onMouseMove={}
//onMouseUp={}
onTouchStart={onStart}
//onTouchMove={}
//onTouchEnd={}
/>
因此,我们需要同时注册全局的移动和结束事件,并且为了避免影响其他组件使用体验,我们在触摸开始时注册事件,结束时移除事件
//注册全局事件
document.addEventListener('touchmove', onMove)
document.addEventListener('touchend', onEnd)
document.addEventListener('mousemove', onMove)
document.addEventListener('mouseup', onEnd)
//使用完毕后记得移除
document.removeEventListener('touchmove', onMove)
document.removeEventListener('touchend', onEnd)
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onEnd)
因此事件的选择解决了
下面为了节约时间,直接说明了手机端和电脑端的事件,返回的类型是不一样的,偏移值所在变量也是不一样的,因此我们取值偏移量时,需要分别处理
//电脑端,取参数的 clientY 即可,水平方向则是 clientX
//移动端,触摸事件,去参数 touches 的第一个参数的 screenY 即可,具体这里就不详解了,可查看触摸事件
let clientY = e.clientY !== undefined ? e.clientY : e.touches[0].screenY
注意:上面的获取的clientY等不是偏移量,而是距离窗口起始位置的偏移量,我们需要根据需要减去组件进度条自身的起点位置,才是在进度条内的偏移量
进度处理
前面决定了事件的问题,这里面,我就们处理进度,处理进度大致分为下面几步:
组件内偏移值计算 -> 组件偏移百分比 -> 组件实际进度(需要四舍五入到整数)
这里面直接一步介绍完,实际内容也不多
//获取进度条自身位置和高度
const y = useRef<number>(0)
const h = useRef<number>(0)
//进度的最大最小值,方便映射到实际值
const min = useRef(props.min !== undefined ? props.min : 0)
const max = useRef(props.max !== undefined ? props.max : 100)
useEffect(() => {
let element = document.getElementById('my-slider')
if (element) {
let rect = element.getBoundingClientRect()
y.current = rect.y
h.current = rect.height
}
}, [])
const onMove = (e) => {
//获取内组件偏移量
let clientY = e.clientY !== undefined ? e.clientY : e.touches[0].screenY
//我们要控制好边界,保证进度在 0 ~ 1 之间
let offsetY = clientY - y.current
const height = h.current
//控制边界,保证进度在 0 ~ 1 之间
if (offsetY < 0) offsetY = 0
if (offsetY > height) offsetY = height
let radio = offsetY / height
//获取进度条值间隔
let interval = max.current - min.current
//实际进度值
let num = radio * interval
//通过四射五入纠正到整数
let q = Math.floor(num)
let r = num - q
if (r >= 0.5 || r <= -0.5) {
q++
}
//为了渲染过渡,值变化才进行渲染
if (value.current !== q) {
//结果需要加上最小值,才是实际进度
value.current = q + min.current
//设置进度百分比
setSliderHeight(`${q / interval * 100}%`)
//对外反馈进度
props.changed && props.changed(q)
}
}
UI显示
UI显示 我想无需多说,这里是从上往下滑动,因此,如果说进度显示白色,背景黑色,那么我将背景设计成白色,黑色的才是实际进度条的话,那么其就跟我们的进度条正相关了
<div
id="my-slider"
onMouseDown={onStart}
onTouchStart={onStart}
style={{width: width, height: 100, display: 'flex',
flexDirection: 'column', backgroundColor: '#fff',
pointerEvents: 'auto', border: '2px #333333 solid', ...styles}}>
<div style={{width: width, height: sliderHeight, backgroundColor: '#1e2129'}}/>
</div>
最后附上全部代码
import {useEffect, useRef, useState } from "react"
export const LightSlider = (props: {
defaultValue?: number
value?: number
min?: number
max?: number
style?: React.CSSProperties
changed?: (value?: number) => void
completed?: (value?: number) => void
width?: number
}) => {
const value = useRef<number | undefined>(0)
const [valueS, setValueS] = useState<number | undefined>() //用于内部显示使用
const [sliderHeight, setSliderHeight] = useState<string>('0px')
const min = useRef(props.min !== undefined ? props.min : 0)
const max = useRef(props.max !== undefined ? props.max : 100)
const y = useRef<number>(0)
const h = useRef<number>(0)
useEffect(() => {
//保存基本高度,毕竟是纵向滚动条
let element = document.getElementById('my-slider')
if (element) {
let rect = element.getBoundingClientRect()
y.current = rect.y
h.current = rect.height
}
}, [])
useEffect(() => {
setValueS(value.current)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value.current])
//处理默认亮度,可能不为10的整数倍
useEffect(() => {
value.current = props.value
}, [props.value])
const addListener = () => {
document.addEventListener('touchmove', onMove)
document.addEventListener('touchend', onEnd)
document.addEventListener('mousemove', onMove)
document.addEventListener('mouseup', onEnd)
}
const removeListener = () => {
document.removeEventListener('touchmove', onMove)
document.removeEventListener('touchend', onEnd)
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onEnd)
}
const onUpdateLum = (e: any) => {
//获取内组件偏移量
let clientY = e.clientY !== undefined ? e.clientY : e.touches[0].screenY
//我们要控制好边界,保证进度在 0 ~ 1 之间
let offsetY = clientY - y.current
const height = h.current
//控制边界,保证进度在 0 ~ 1 之间
if (offsetY < 0) offsetY = 0
if (offsetY > height) offsetY = height
let radio = offsetY / height
//获取进度条值间隔
let interval = max.current - min.current
//实际进度值
let num = radio * interval
//通过四射五入纠正到整数
let q = Math.floor(num)
let r = num - q
if (r >= 0.5 || r <= -0.5) {
q++
}
//为了渲染过渡,值变化才进行渲染
if (value.current !== q) {
//结果需要加上最小值,才是实际进度
value.current = q + min.current
//设置进度百分比
setSliderHeight(`${q / interval * 100}%`)
//对外反馈进度
props.changed && props.changed(q)
}
}
const onStart = (e: any) => {
addListener()
onUpdateLum(e)
}
const onMove = (e: any) => {
onUpdateLum(e)
}
const onEnd = async (e: any) => {
removeListener()
props.completed && props.completed(value.current)
}
const styles = props.style ?? {}
const width = props.width !== undefined ? props.width : 10
return (
<div
id="my-slider"
onMouseDown={onStart}
onTouchStart={onStart}
style={{width: width, height: 100, display: 'flex',
flexDirection: 'column', backgroundColor: '#fff',
pointerEvents: 'auto', border: '2px #333333 solid', ...styles}}>
<div style={{width: width, height: sliderHeight, backgroundColor: '#1e2129'}}/>
</div>
)
}
<LightSlider
min={0}
max={10}
style={{marginLeft: 100, marginTop: 100}}
changed={(e) => {
console.log('changed', e)
}}
completed={(e) => {
console.log('completed', e)
}}
/>