自造简易版音频进度条

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

效果

话不多说,先看效果

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

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

官网:mobile.ant.design/zh/componen...

效果很多

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

对比

这是antd-mobile的效果

代码如下:

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

ts 复制代码
<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} />}
    />
  {/* <MusicSlider
    className={styles.songSlider}
    defaultValue={currentTime && duration ? currentTime / duration * 100 : 0}
    onAfterChange={change}
    value={currentTime && duration ? currentTime / duration * 100 : 0}
  /> */}
  <div className={styles.processTime}>
    {
      duration ? formatTime(duration) : '00:00'
    }
  </div>
</div>

changeProgressValue事件就是修改音频的currentTime

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

github.com/zillow/reac...

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

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

点击修改进度

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

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

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

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

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

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

ts 复制代码
// 点击事件
  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)
    }
  }

拖拽修改进度

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

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

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

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

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

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

mouseDown

ts 复制代码
// 触摸开始事件
  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>

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

类型错误如下:

mouseMove

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

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

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

ts 复制代码
// 鼠标/触摸移动事件
  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
    }
  }

mouseUp

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

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

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

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

ts 复制代码
 // 鼠标/触摸释放事件
  const barUp = () => {
    // 避免打开Playing组件时触发
    if (isDrag.current && onAfterChange) {
      // @ts-ignore
      onAfterChange(dynamicState.current)
    }
  }

销毁事件

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

ts 复制代码
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)
    }
  }

最后全部代码如下:

ts 复制代码
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>
  )
}
相关推荐
2401_857439697 分钟前
智慧社区电商系统:提升用户体验的界面设计
前端·javascript·php·ux
我是高手高手高高手17 分钟前
ThinkPHP8多应用配置及不同域名访问不同应用的配置
linux·服务器·前端·php
csdnLN1 小时前
$.ajax() 对应事件done() 、fail()、always() 的用法
前端·javascript·ajax
甜味橘阳1 小时前
echarts地图可视化展示
前端·javascript·echarts
bloxed1 小时前
前端文件下载多方式集合
前端·filedownload
余生H2 小时前
前端Python应用指南(三)Django vs Flask:哪种框架适合构建你的下一个Web应用?
前端·python·django
LUwantAC2 小时前
CSS(四)display和float
前端·css
cwtlw2 小时前
CSS学习记录20
前端·css·笔记·学习
界面开发小八哥2 小时前
「Java EE开发指南」如何用MyEclipse构建一个Web项目?(一)
java·前端·ide·java-ee·myeclipse
米奇妙妙wuu2 小时前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js