• 自造简易版音频进度条


    最近在做音乐播放器页面, 积累了很多有趣的经验, 今天先分享播放进度条的开发过程.

    效果

    话不多说,先看效果

    支持点击修改进度,拖拽修改进度,当然大家肯定都知道ui库里面有现成的,为何要自己造一个

    首先著名的ui库中确实都要这样的滑动输入条,比如antd-mobile中的slider

    官网:https://mobile.ant.design/zh/components/slider/

    效果很多

    比我自己造的肯定功能丰富的多,但是亲自试过之后,发现效果不太友好,下面可以看看使用antd-mobile中的slider效果如下:

    对比

    这是antd-mobile的效果

    代码如下:

    其实把value属性去掉,这个组件就会丝滑很多,但是音乐播放器,需要随着音频播放,更改进度条,这是必须要的功能,不能去掉。

    <div className={styles.process}>
      <div className={styles.processTime}>
        {
          currentTime ? formatTime(currentTime) : '00:00'
        }
      </div>
      <Slider
        className={styles.songSlider}
        defaultValue={0}
        onAfterChange={changeProgressValue}
        value={currentTime && duration ? currentTime / duration * 100 : 0}
        icon={<div className={styles.sliderDot} />}
        />
      {/*  */}
      <div className={styles.processTime}>
        {
          duration ? formatTime(duration) : '00:00'
        }
      </div>
    </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    changeProgressValue事件就是修改音频的currentTime

    除此之外,我也试用了其他的依赖库,比如react-slider

    https://github.com/zillow/react-slider

    但是效果依旧不好,就是因为有这种两三点的滑动,所以导致逻辑复杂,滑动效果就不太丝滑

    所以,我就觉得自己造一个,slider组件

    点击修改进度

    点击事件比较简单,就需要给这个区域绑定点击事件

    灰色区域是父元素,红色元素是子元素,红色元素的宽度就是歌曲当前播放进度比分比 * 父元素宽度

    首先,需要理清楚几个坐标,如何确定点击这里是音频进度所占比分比

    点击当前点的坐标,点击的时候,能够拿到;父元素的宽度,通过getBoundingClientRect().width也能拿到

    父元素左边距离最左边的距离,也能拿到getBoundingClientRect().left,也就是下面这段距离。

    所以最终的点击函数如下:

    // 点击事件
      const barClick = (e: React.MouseEvent) => {
        // @ts-ignore
        const rect = mmProgress.current.getBoundingClientRect()
        const activeWidthVal = Math.min(rect.width, Math.max(0, e.clientX - rect.left))
        // @ts-ignore
        const progress = Math.floor(activeWidthVal / mmProgress.current.clientWidth * 100)
        setActiveWidth(progress)
        if (onAfterChange) {
          onAfterChange(progress)
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    拖拽修改进度

    在电脑上,需要监听的是鼠标的mouseup和mouseMove事件

    在移动端,需要监听的是touchend和touchmove事件

    鼠标移动/触屏移动:需要更新进度条的百分比

    鼠标弹起/触屏结束:需要更新歌曲的进度

    开始事件能够直接绑定在进度小圆点上

    开始时,需要获取到开始的坐标,并且存起来,方便移动事件计算

    mouseDown

    // 触摸开始事件
      const barDown = (e: React.TouchEvent) => {
        startX.current = e.touches[0].pageX
        // @ts-ignore
        leftVal.current = mmProgressInner.current.clientWidth
        isDrag.current = true
      }
    // 鼠标开始移动
      const barDown1 = (e: React.MouseEvent) => {
        startX.current = e.clientX
        // @ts-ignore
        leftVal.current = mmProgressInner.current.clientWidth
        isDrag.current = true
      }
    
    // tsx
    <div className={styles.sliderDot}
      onMouseDown={barDown1}
      onTouchStart={barDown}
      ></div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    由于我直接绑定在了tsx元素上,为了防止ts报错,我就写了两个函数,因为两者的类型不同

    类型错误如下:

    mouseMove

    鼠标移动,需要及时更新进度条的样式,也就是红色条的宽度

    所以需要计算当前点击的坐标,和上面函数保持的开始移动坐标

    然后就是计算百分比,更新样式

    // 鼠标/触摸移动事件
      const barMove = (e: React.TouchEvent & React.MouseEvent) => {
        if (isDrag.current) {
          const endX = e.clientX || e.touches[0].pageX
          const dist = endX - startX.current
          // @ts-ignore
          const activeWidthVal = Math.min(mmProgress.current.clientWidth, Math.max(0, leftVal.current + dist))
    
          // @ts-ignore
          const progress = Math.floor(activeWidthVal / mmProgress.current.clientWidth * 100)
          setActiveWidth(progress)
          dynamicState.current = progress
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    mouseUp

    鼠标抬起,这个函数需要说一下,首先需要判断一下是否已经在鼠标抬起时完成了鼠标放下事件mouseDown

    为什么呢?防止这两种情况

    这两种情况,也会触发mouseMove和mouseUp事件,但是这两种情况都不可以修改进度

    所以需要一个变量来判断是否是在小圆点处发生了mouseDown事件

     // 鼠标/触摸释放事件
      const barUp = () => {
        // 避免打开Playing组件时触发
        if (isDrag.current && onAfterChange) {
          // @ts-ignore
          onAfterChange(dynamicState.current)
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    销毁事件

    到这里已经接近尾声了,但是注意挂载了事件,需要销毁

    useMount(() => {
      bindEvents()
    })
    
    useUnmount(()=> {
        unbindEvents()
      })
      // 添加绑定事件
      const bindEvents = () => {
        // @ts-ignore
        mmProgress.current.addEventListener('mousemove', barMove)
        // @ts-ignore
        mmProgress.current.addEventListener('mouseup', barUp)
        // @ts-ignore
        mmProgress.current.addEventListener('touchmove', barMove)
        // @ts-ignore
        mmProgress.current.addEventListener('touchend', barUp)
      }
    
      // 移除绑定事件
      const unbindEvents = () => {
        if (mmProgress.current) {
          // @ts-ignore
          mmProgress.current.removeEventListener('mousemove', barMove)
          // @ts-ignore
          mmProgress.current.removeEventListener('mouseup', barUp)
          // @ts-ignore
          mmProgress.current.removeEventListener('touchmove', barMove)
          // @ts-ignore
          mmProgress.current.removeEventListener('touchend', barUp)
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    最后全部代码如下:

    
    import classNames from 'classnames'
    import { useEffect, useRef, useState } from 'react'
    import styles from './index.module.scss'
    import { useMount, useUnmount  } from 'ahooks';
    
    export default function MusicSlider(props: any) {
      const { className, defaultValue, onAfterChange, value } = props
      const [activeWidth, setActiveWidth] = useState(defaultValue)
      const dynamicState = useRef(0)
      const startX = useRef(0) // 记录最开始点击的x坐标
      const leftVal = useRef(0) // 记录当前已经移动的距离
      const isDrag = useRef(false) // 是否可以拖拽
    
      const mmProgress = useRef(null)
      const mmProgressInner = useRef(null)
      useMount(() => {
        bindEvents()
      })
    
      useEffect(() => {
        const progress = Math.floor(value)
        // @ts-ignore
        setActiveWidth(progress)
      }, [value])
    
      useUnmount(()=> {
        unbindEvents()
      })
      // 添加绑定事件
      const bindEvents = () => {
        // @ts-ignore
        mmProgress.current.addEventListener('mousemove', barMove)
        // @ts-ignore
        mmProgress.current.addEventListener('mouseup', barUp)
        // @ts-ignore
        mmProgress.current.addEventListener('touchmove', barMove)
        // @ts-ignore
        mmProgress.current.addEventListener('touchend', barUp)
      }
    
      // 移除绑定事件
      const unbindEvents = () => {
        if (mmProgress.current) {
          // @ts-ignore
          mmProgress.current.removeEventListener('mousemove', barMove)
          // @ts-ignore
          mmProgress.current.removeEventListener('mouseup', barUp)
          // @ts-ignore
          mmProgress.current.removeEventListener('touchmove', barMove)
          // @ts-ignore
          mmProgress.current.removeEventListener('touchend', barUp)
        }
      }
    
      // 点击事件
      const barClick = (e: React.MouseEvent) => {
        // @ts-ignore
        const rect = mmProgress.current.getBoundingClientRect()
        const activeWidthVal = Math.min(rect.width, Math.max(0, e.clientX - rect.left))
        // @ts-ignore
        const progress = Math.floor(activeWidthVal / mmProgress.current.clientWidth * 100)
        setActiveWidth(progress)
        if (onAfterChange) {
          onAfterChange(progress)
        }
      }
    
      // 触摸开始事件
      const barDown = (e: React.TouchEvent) => {
        startX.current = e.touches[0].pageX
        // @ts-ignore
        leftVal.current = mmProgressInner.current.clientWidth
        isDrag.current = true
      }
    // 鼠标开始移动
      const barDown1 = (e: React.MouseEvent) => {
        startX.current = e.clientX
        // @ts-ignore
        leftVal.current = mmProgressInner.current.clientWidth
        isDrag.current = true
      }
      // 鼠标/触摸移动事件
      const barMove = (e: React.TouchEvent & React.MouseEvent) => {
        if (isDrag.current) {
          const endX = e.clientX || e.touches[0].pageX
          const dist = endX - startX.current
          // @ts-ignore
          const activeWidthVal = Math.min(mmProgress.current.clientWidth, Math.max(0, leftVal.current + dist))
    
          // @ts-ignore
          const progress = Math.floor(activeWidthVal / mmProgress.current.clientWidth * 100)
          setActiveWidth(progress)
          dynamicState.current = progress
        }
      }
    
      // 鼠标/触摸释放事件
      const barUp = () => {
        // 避免打开Playing组件时触发
        if (isDrag.current && onAfterChange) {
          // @ts-ignore
          onAfterChange(dynamicState.current)
        }
      }
    
      return (
        <div className={classNames(className, styles.progress)} ref={mmProgress} onClick={barClick}>
          <div className={styles.bar}></div>
          <div className={styles.outer}></div>
          <div className={styles.inner} ref={mmProgressInner} style={{
            width: `${activeWidth}%`
          }}>
            <div className={styles.sliderDot}
            onMouseDown={barDown1}
            onTouchStart={barDown}
            ></div>
          </div>
        </div>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
  • 相关阅读:
    [附源码]Python计算机毕业设计Django蛋糕购物商城
    文献 | 柳叶刀发文:虚拟现实的新用途之治疗场所恐惧症
    【医学影像】LIDC-IDRI数据集的无痛制作
    微服务moleculer03
    【软考学习1】数据表示——进制转换,R进制转10进制 和 10进制转R进制
    银河麒麟 ARM 架构 离线安装Docker
    学习笔记-算法-9-二叉树-1
    Buuctf [MRCTF2020]Ez_bypass 1 WP解析
    SolidWork的使用技巧总结
    精分合并抑郁康复经历分享:如何从死亡边缘回到生的海洋?
  • 原文地址:https://blog.csdn.net/qq_44859233/article/details/132767952