react-自制一个垂直Slider

438 阅读3分钟

开发 react 中,突然碰到一个效果,定制垂直滚动条,且需要在电脑端和移动端也能操作,此时发现 mobile-ant 不能用了,虽然没有适配过移动端,但尝试一下也不复杂

经过简单梳理,大致经过3个步骤:事件选择进度处理UI显示

demo地址

事件选择

由于要同时在电脑移动端显示,因此事件的选择至少要两种,当时就选择了下面 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)
    }}
  />