歌词滚动和拖动定位
一、简介
一些零基础小白学习vue3
的开发记录,目标实现web端音乐播放器。github开发代码,欢迎一起交流学习。
上一篇记录进度条实现的原理
二、相关准备
需求分析
这也是在网易云手机端的功能复制:
- 歌曲播放时,显示歌词并随着时间滚动和显示高亮
- 拖动歌词界面,显示定位条,可在选择歌词处跳转播放
- 切换歌词和专辑封面时,保持歌词的播放进度
- 第一点需求,解析的每条歌词对应有时间戳和内容,渲染在DOM上后,通过
watch
函数监听歌曲的currentTime
。找到对应的Element
后用了一款滚动插件进行滚动,原生实现可以设置translate
和transform
属性。 - 第二点需求,单独拿出来时,拖动歌词页面和显示定位条容易实现,歌词的跳转播放即通过点击直接改变当前音频的
currentTime
。难点是拖动和播放时自动滚动功能的分离。 - 第三点需求,单独的拿出来分析可以用
vue
的<keep-alive>
组件进行状态的缓存。
难点分析
歌词拖动和播放自动滚动,通过watch
函数返回的取消监听currentTime
的歌词跳动可以实现,这个和之前进度条实现的原理差不多。
但是额外的要求是:查看歌词时可能会不断的进行滚动,想要的效果是不断滚动后,停止的5s后再进行自动滚动。假如每次@touchstart
时unwatch
,@touchend
时又进行watch
,在实现的过程中5s的间隔会然监听乱套。
最后是利用防抖函数,取消@touchend
不断的滚动watch
的执行,在停止的5s后执行一次。
三、实现过程
歌词原始数据的解析
这里就不放代码了,具体就是用正则匹配各行歌词,然后提取出{time,lyric,trans}
即时间、原歌词和翻译三类。用到的正则如下:
const regexp = /\[\d{2}:\d{2}.\d{2,3}\]/g;
歌词高亮显示和自动滚动
其中比较重要的是lyricWatcher
和jumpWatcher
,这里把用于跳转的watcher
拿出来,可以单独进行控制。其他的功能函数就不展开了。
// ./composables/lyricScroll.js
import { ref, watchEffect } from "vue";
export default function lyricScroll(currentTime, lyricsArr) {
// 记录播放歌词位置
let currentLyricRef = null;
let oldLyricRef = null;
// 用于歌词滚动
const scroll = ref(null);
const jumper = (currentEl = undefined, time = 1000, offsetY = 255) => {
···
};
// 改变歌词样式
const styler = (prevEl, currentEl, color = "#fff") => {
···
};
// 该时间点所处的时间段的index
const findLyricRef = time => {
···
};
const lyricWatcher = log => {
return watchEffect(() => {
findLyricRef(currentTime.value);
styler(oldLyricRef, currentLyricRef);
});
};
const jumpWatcher = log => {
return watchEffect(() => {
const watch = currentTime.value;
jumper(currentLyricRef, undefined, undefined);
});
};
return {
scroll,
currentLyricRef,
lyricWatcher,
jumpWatcher,
jumper,
styler
};
}
拖动歌词和跳转对应歌词播放
这里关键是:
touchstart
回调中取消监听以及取消之前已有的防抖- 同时把用到的
touchend
回调中的操作进行防抖处理
其中防抖函数的取消主要参考了lodash源码中的实现。
// ./composables/lyricTouch.js
import { computed, ref } from "vue";
import { debounce } from "common/utils";
export default function lyricTouch(
emit,
jumpWatcher,
styler,
lyricsArr,
verticalOffset
) {
let isTouch = false;
let unwatchJump = ref(null);
// 一些用于触摸滚动的变量
···
const debouncedTouchEnd = debounce(() => {
unwatchJump.value = jumpWatcher();
emit("touching", false);
isTouch = false;
}, 5000);
const handleTouchStart = e => {
isTouch = true;
emit("touching", true);
unwatchJump.value();
// 长按情况,取消歌词跳动
if (debouncedTouchEnd) {
debouncedTouchEnd.cancel();
}
};
const handleTouchEnd = e => {
debouncedTouchEnd();
};
const handleTouchMove = pos => {
// 跳过自动滚动情况
if (!isTouch) {
return;
}
// 以下为改变歌词样式和找到对应时间戳传给定位条
// 具体不展开
···
};
return {
unwatchJump,
handleTouchStart,
handleTouchEnd,
handleTouchMove
};
}
保持歌词进度
这里就比较简单了,在onActivated
回调中每次直接跳转到当前歌词。<Scroll/>
是包装插件的组件。
我这里浪费点性能,直接放在onMounted
执行一次看着简单点,其实应该在onActivated
中开始watch
,然后在onDeactivated
时unwatch
,这样是比较好的。
// player/comps/ContentLyric.vue
<template>
<Scroll
ref="scroll"
class="scroll"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
@scroll="handleTouchMove"
>
<div class="content-lyric">
··· //歌词相关HTML代码
</div>
</Scroll>
</template>
<script>
import Scroll from "components/common/scroll/Scroll";
import lyricParse from "./composables/lyricParse";
import lyricScroll from "./composables/lyricScroll";
import lyricTouch from "./composables/lyricTouch";
import {
defineComponent,
ref,
computed,
onActivated,
onDeactivated,
onMounted
} from "vue";
import { useStore } from "vuex";
export default defineComponent({
name: "ContentLyric",
components: {
Scroll
},
emits: {
touching: null,
scrollTime: null
},
setup(props, { emit }) {
const $store = useStore();
const currentTime = computed(() => $store.state.currentTime);
const currentSong = computed(() => $store.state.currentSong);
/* 歌词解析 */
let lyricsArr = ref({
lyric: [],
trans: [],
el: []
});
// 获取各行歌词的DOM元素
const setLyricRefs = el => {
lyricsArr.value.el.push(el);
};
··· //lyricParse
/* 滚动监听相关 */
const verticalOffset = 255;
··· //lyricScroll
/* 歌词滑动相关 */
··· //lyricTouch
/* 执行与回调相关 */
onMounted(() => {
parseLyrics(currentSong.value.lyrics);
const unwatchLyric = lyricWatcher();
unwatchJump.value = jumpWatcher();
});
onActivated(() => {
jumper(currentLyricRef, 0, undefined);
});
return {
scroll,
lyricsArr,
setLyricRefs,
verticalOffset,
handleTouchStart,
handleTouchEnd,
handleTouchMove
};
}
});
四、总结
完整的详细代码放在github
用vue3的组合式api还是很舒服,个人感觉没有this
和可以自由组合拆分的特点,可以更好的维护,从vue2到vue3删了很多没有用和重复的代码,感觉很丝滑。