如果有这么一把神奇的尺子,一辈子都不想从小学毕业

417 阅读5分钟

最近公司项目中有一个画刻度尺的需求,本以为很简单,产品小姐姐宣讲完需求感觉还是有些难度的,具体请看下图: 2021-05-09 001707.gif 如上,刻度尺在整个波形的上方,标识波形中每个凸起线(项目中叫沿)的坐标,这没什么,关键的需求是,刻度尺要随着显示比例和波形长度而变化,也就是说尺子的长度可变,最小刻度可变。

src=http___tu.jiuwa.net_pic_20171008_1507458374714726.jpg&refer=http___tu.jiuwa.jpeg

这是孙悟空的金箍棒吗?说大就大,说小就小吗。奈何,在产品小姐姐的淫威下还是心甘情愿的撸代码去了。 先上个思维导图,刻度尺ui比较简单,就是刻度,还有显示的数字(还有可能有其他要显示的信息,比如单位等),统称为刻度描述。

image.png

实际需求中没有显示内容可编辑和颜色的需求,如果可编辑的话相关信息肯定就会存起来,目前还没有这样的需求,算是一种需求扩展,日后改造成公共组件可以更完善

下面进入coding环节

Dom版本

好吧,我承认了,这部分代码目前是没有的,代码在公司,忘截图了,日后再补上吧。这个版本的思路是,无论刻度尺多长,因为刻度尺的长度要和波形总长一样,可以是几十几百也可以是几千几万,都把刻度尺分成十个区块(一个刻度块十个单位刻度,也就是我们平常尺子10毫米的块)

这种思路总刻度是基本不变的,那么根据公式
总刻度:10 * 10 = 100
长度 = 100(不变的总刻度)* 单位刻度长度
所以长度和单位刻度长度成正比例关系,如动图中所示当显示比例是100%的,也就是没有横向滚动条的时候是没问题的,比例尺显示很均匀,但是当波形长度很长,并且显示说比例很大时就会出现下图中的情况

WechatIMG3.jpeg

是的,单位刻度很大,一个单位刻度代表的长度太长,画波形的人很难通过刻度尺对沿进行比对,这样就失去了比例尺的意义。
而且这个dom版比例尺是用antd的Space和Divider分割线组件画的,下图是五个单位刻度渲染的dom结构,只能用恐怖来形容了,以目前总刻度一般为100的情况下,一个比例尺要渲染近1000个dom,而且这个刻度尺是实时变化的,这对浏览器是很大的开销,作为一个有追求的程序猿怎么能容忍这种事情发生!

WechatIMG1.jpeg

SVG版本

目前需要解决的有两个问题:

  1. 显示比例很大时,单位刻度很大,刻度显示不均匀
  2. dom过多,导致渲染卡顿问题 针对第一个问题,无论什么情况,每个单位刻度都有一个固定宽度
    // 默认一个刻度的宽度
    const BASE_WIDTH = 6
    const waveFormUtils = useWaveFormUtils(),
        {sumTime, isView} = useRecoilValue(waveFormSettingState),
        // 总长度
        plusSumTime = waveFormUtils.plusFormat(sumTime);
    let base_width = BASE_WIDTH;
    // 一个刻度默认宽度6px,一共有count个刻度
    let count = plusSumTime / BASE_WIDTH
    // 一个区域10个刻度,共有sectionCount个区域
    let sectionCount = count/10
    // 一个区域的刻度数,也就是刻度显示的一倍数字,如果单位刻度数不是被10整除的整数,需要调整为整数
    let base = sumTime/sectionCount
    // 计算刻度数接近哪边整数,将base调整为整数
    const getBase = () => {
        const remain = base % 10
        // 因为刻度尺要显示0,10,20,30。。。所以如果被10整除不用反推刻度长度
        if (remain === 0) { return }
        // 不是整除说明base是两个被10整除的数的中间值,比如14就是10和20中间,因为14更靠近10,所以让base取更靠近的值10
        base = base < 10 ? 10 : (remain > 5 ? Math.ceil(base/10) * 10 : Math.floor(base/10) * 10)
        // 反推一个刻度的长度
        sectionCount = sumTime / base
        count = sectionCount * 10
        base_width = plusSumTime / count
    }
    getBase()
    return <svg style={{height:25,width:plusSumTime}} onClick={mouseClick} onDoubleClick={doubleClick}>
            {new Array(Math.ceil(sectionCount)).fill('').map((space,i) => <Scale key={i+base} base_width={base_width} split={i} base={base} />)}
        </svg>

上面代码最主要就是得到base(一个刻度区块的长度),sectionCount(共有多少个刻度区块),base_width(单位刻度的长度),那刻度尺要怎么渲染呢?我们需要一个组件渲染一个刻度区块,在循环sectionCount次不就好了吗

    // 这里代码有点乱,包括样式写在标签里,待优化
    const Scale = function ({split,base,base_width}) {
    // 获取横坐标位置,split是第几个区块,split * 10 * base_width是每个区块的第一个刻度的位置,
    const getPosiNumber = (i) => {
        return split * 10 * base_width + i * base_width
    }
    const ds = new Array(11).fill('').map((item,index) => {
        // 不用关注,波形相关特殊处理
        const special = split==0 && index==0 ? 1 : 0
        // 刻度线的高度,每个区块的第一个和最后一个最高中间其次,其他最短
        const startY = (index===0 || index===10) ? '0' : (index===5 ? '10' : '16')
        return `M${getPosiNumber(index)+special},${startY} L${getPosiNumber(index)+special},25`

    })
    return <g style={{stroke: '#BFBFBF'}}>
        <text style={{fontSize:'11px',fill:'#BFBFBF',strokeWidth:0}} x={getPosiNumber(0)+2} y={10}>{Math.round(split * base)}</text>
        {ds.map((d,i) => <path key={i+split} d={d} />)}
    </g>
}

这个地方就已经解决了上边的第二个问题,dom过多渲染卡顿的问题,最开始想用拖动滚动条懒加载的办法,但是感觉很麻烦,而且体验不一定好,所以最终改成svg,下图就是svg改造后的dom结构,需要渲染成千上万个刻度的时候也不会卡顿,如丝般润滑。

不过这个地方还可以优化成一个path,其他代码写的也有待优化,就是记录一下这个过程吧,还有一些功能,比如按住shift点击鼠标左键,单击双击,时间原因就不写了,毕竟已经凌晨三点了😄,打完收工,古德耐特!

WechatIMG2.jpeg

2021-05-09 001655.gif