自造简易版音频进度条

news2024/12/23 0:13:13

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

效果

话不多说,先看效果

支持点击修改进度,拖拽修改进度,当然大家肯定都知道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} />}
    />
  {/* <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

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)
    }
  }

拖拽修改进度

在电脑上,需要监听的是鼠标的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>

由于我直接绑定在了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
    }
  }

mouseUp

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

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

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

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

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

销毁事件

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

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)
    }
  }

最后全部代码如下:


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>
  )
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/989952.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

flask bootstrap页面json格式化

html <!DOCTYPE html> <html lang"en"> <head><!-- 新 Bootstrap5 核心 CSS 文件 --> <link rel"stylesheet" href"static/bootstrap-5.0.0-beta1-dist/css/bootstrap.min.css"><!-- 最新的 Bootstrap5 核心 …

C++ goto 语句

goto 语句允许把控制无条件转移到同一函数内的被标记的语句。 注意&#xff1a;在任何编程语言中&#xff0c;都不建议使用 goto 语句。因为它使得程序的控制流难以跟踪&#xff0c;使程序难以理解和难以修改。任何使用 goto 语句的程序可以改写成不需要使用 goto 语句的写法。…

【vue3】一些关于hooks的使用经验

前言 最近接到了一个需求&#xff0c;隔壁嵌入式部门希望我们用前端解析渲染Kconfig表单。这篇文章用来记录一下本次使用hook pinia vue3的经验 hooks hooks的概念最早是在 React 中听到的&#xff0c;虽然早些时间也写过一点react&#xff0c;但也只是照葫芦画瓢&#xf…

C++多线程编程(第四章 promise和future)

promise 和future promise用于异步传输变量 std::promise提供存储异步通信的值&#xff0c;再通过其对象创建的std::future异步获得结果。 std::promise只能使用一次。void set_value(_Ty&& _Val)设置传递值&#xff0c;只能调用一次std::future提供访问异步操作结果…

最新期权开户的形式有哪些?

期权目前都有哪些开户方式? 摘对于上证50etf期权的投资者来说,最关心的就是开户的问题了,而50etf期权开户方式目前主要有券商和平台开户两种,各有优缺点&#xff0c;下文介绍最新期权开户的形式有哪些&#xff1f; 一、最新的期权开户方式有多种&#xff0c;包括在线开户、手机…

自己的碎碎念集合

自己的碎碎念集合 2023-09-07 c叠加三目运算符闰年计算法2023-08-13 一个小题目 AB problem一、问题及解答关碍 总结 2023-07-26 C的2至36进制转换函数一、itoa()函数的示例代码总结 2023-07-19 平面坐标下判断三角形以及输出周长和面积一. 基本知识总结 2023-06-25 达芬奇去除…

认识伦敦银的真相,并没有那么容易

我们进行伦敦银投资&#xff0c;其实就是想利用一定的时间在这个市场中获取盈利。对于普通人来说&#xff0c;我们获得金钱的方法就是从事一份工作努力的&#xff0c;在这个职位中&#xff0c;做好自己的本职工作&#xff0c;最后老板为我们的辛勤付出&#xff0c;支付相应的工…

快速构建基于Paddle Serving部署的Paddle Detection目标检测Docker镜像

快速构建基于Paddle Serving部署的Paddle Detection目标检测Docker镜像 项目介绍需要重点关注的几个文件构建cpu版本的docker构建gpu版本的docker&#xff08;cuda11.2cudnn8&#xff09; 阅读提示&#xff1a; &#xff08;1&#xff09;Paddle的Serving项目中&#xff0c;在t…

OpenCV 06(图像的基本变换)

一、图像的基本变换 1.1 图像的放大与缩小 - resize(src, dsize, dst, fx, fy, interpolation) - src: 要缩放的图片 - dsize: 缩放之后的图片大小, 元组和列表表示均可. - dst: 可选参数, 缩放之后的输出图片 - fx, fy: x轴和y轴的缩放比, 即宽度和高度的缩放比. - …

【React学习】React高级特性

1. 函数式组件和类组件区别 函数式组件 函数式组件是一种简单的组件定义方式&#xff0c;它是一个以JavaScript函数为基础的组件。 可以把函数式组件理解为纯函数&#xff0c;它的输入为props&#xff0c;输出为JSX。函数式组件没有状态&#xff0c;也没有生命周期。 functio…

java特殊文件 属性文件properties和XML文件

属性文件properties 后缀为.properties的文件&#xff0c;称之为属性文件&#xff0c;它可以很方便的存储一些类似于键值对的数据。经常当做软件的配置文件使用。 首先我们要掌握属性文件的格式&#xff1a; 1.属性文件后缀以.properties结尾 2.属性文件里面的每一行都是一个…

Python Opencv实践 - Shi-Tomasi角点检测

参考资料&#xff1a;Harris和Shi-tomasi角点检测笔记&#xff08;详细推导&#xff09;_harris焦点检测_亦枫Leonlew的博客-CSDN博客 cv.goodFeaturesToTrack&#xff1a;Shi-Tomasi角点检测-OpenCV-python_独憩的博客-CSDN博客 import cv2 as cv import numpy as np import …

精准定位,智慧港口:北斗技术在港口车辆智能监管中的应用

随着全球经济一体化的加速推进&#xff0c;港口作为全球物流网络中的关键节点、对外贸易货物的集散中心以及国际物流供应链的重要组成部分&#xff0c;其在区域经济发展中的作用变得越来越重要。然而&#xff0c;随着港口向大型化、专业化方向的发展&#xff0c;现有的基础设施…

基于Java+SpringBoot+UniApp的微信小程序朋友圈

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 随着社交媒体的兴起和…

在UE4虚幻引擎中加入导航网格体边界体积后丧尸不能移动和发现玩家

UE4系列文章目录 文章目录 UE4系列文章目录前言一、用到的知识点二、问题原因 前言 最近使用ue4做第一人称视角射击游戏发现问题&#xff0c;加入导航网格体边界体积后丧尸不能移动和发现玩家。下图是出现的问题图片 一、用到的知识点 1.行为树&#xff1a;控制并显示AI的决…

【后端面经-数据库】Redis数据结构和底层数据类型

【后端面经-数据库】Redis数据结构和底层数据类型 1. Redis数据类型1.1 基本数据类型1. string2. hash3. list4. set5. sortset/Zset 1.2 特殊数据类型1. bitmap2. hyperloglog3. GEO4. stream 2. Redis底层数据类型2.1 简介2.2 动态字符串SDS2.3 快表QuickList2.4 字典Dict2.5…

论文解读 | 基于中心的三维对象检测与跟踪

原创 | 文 BFT机器人 CenterPoint与传统基于框的3D物体检测器和跟踪器不同之处在于&#xff0c;它将3D物体表示、检测和跟踪为点&#xff0c;而不是使用边界框。这种方法具有几个优点&#xff0c;包括减少物体检测器的搜索空间&#xff0c;简化下游任务&#xff08;如跟踪&…

一键去除文件名中的空格,轻松解决文件命名烦恼!

你是否曾经为文件名中的空格而烦恼&#xff1f;这些空格可能会在传输、存储和搜索文件时带来各种问题。为了解决这个问题&#xff0c;本文将向你介绍几种实用的方法&#xff0c;让你轻松去除文件名中的空格&#xff0c;让文件命名变得更加简单&#xff01; 首先&#xff0c;我…

MAC终端美化

先看看效果&#xff1a; 1.安装on-my-zsh 打开终端&#xff0c;输出&#xff1a; sh -c "$(curl -fsSL https://gitee.com/mirrors/oh-my-zsh/raw/master/tools/install.sh)"安装过程中如果出现了链接超时的错误&#xff0c;不要慌&#xff0c;就再来一次&#x…

牛客网——BM62 斐波那契数列

class Solution { public:/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可** * param n int整型 * return int整型*/int Fibonacci(int n) {// write code hereif(n0) //考虑第0项return 0;else if(n1||n2)return 1;else…