花潮经典版播放器开发环节之功能实现机制(一)
本帖最后由 马黑黑 于 2025-4-1 13:17 编辑 <br /><br /><style>.artbox p { margin: 10px 0; }
#showRes { padding: 2px 6px; border: 1px solid teal; border-radius: 8px; color: red; cursor: pointer; box-shadow: 1px 1px 3px #666; }
#showRes:hover { background: #eee; }
</style>
<div class="artbox">
<p>在上一个环节<a href="https://www.huachaowang.com/forum.php?mod=viewthread&tid=82546" target="_blank">《花潮经典版播放器开发环节之界面设计》</a>中,我们完成了界面设计与实现,接下来要做的是播放器的功能实现机制,包括人机交互操作(比如点击按钮、进度条,拖曳进度条上的滑块)、音频播放与暂停所引发的联动动作(比如音频暂停与播放状态按钮的样式、进度条的着色和滑块的运行位置)等等,都需要逐一使用JS来一一实现。在具体操作之前,我们先得获取播放器、按钮、进度条、滑块等元素的操作入口标识,这些标识指向对应的HTML标签,需要和上一环节的HTML代码进行关联,并且,得额外在HTML代码中加入audio标签。以下是五个元素操作标识,最后一个指向 audio 标签:</p>
<div class="divBox2" data-name="JS代码"><pre class="preBox2">
const mplayer = document.querySelector('.mplayer');
const btnPlay = document.querySelector('.btnPlay');
const progress = document.querySelector('.progress');
const thumb = document.querySelector('.thumb');
const audio = document.querySelector('audio');
<pre></div>
<p>这样,我们要处理按钮的点击操作,直接使用 btnPlay 作为操作依据,像这样:</p>
<div class="divBox2" data-name="JS代码"><pre class="preBox2">
btnPlay.onclick = () => audio.paused ? audio.play() : audio.pause();
</pre></div>
<p>btnPlay 按钮单击事件(onclick)执行一个操作,判断音频标签 audio 是否处于暂停中(audio.paused ?),若是,则令其播放(audio.play()),反之(:),若它处在播放状态中,则令其暂停(audio.pause())。按钮的工作就这么简单,只管控音频标签的播放、暂停。</p>
<p>那按钮的样式谁来管?没错,按钮的样式,在音频播放的时候它应该变成一个暂停样式,用以指示或暗示点击它可令音乐暂停,音频暂停的时候它应该是一个播放形状,用以引导用户点击它来让音乐暂停。这个工作由一个函数把关,然后利用音频标签的播放事件(playing)、暂停事件(pause)运行这个函数,从而达到智能动态管理按钮状态的目的:</p>
<div class="divBox2" data-name="JS代码"><pre class="preBox2">
// 联动函数 mState
const mState = () => {
btnPlay.className = `btnPlay ${['pause', 'play'][+audio.paused]}`;
// 其它需要联动管控的代码
};
// 音频标签开始播放和暂停事件
audio.onplaying = audio.onpause = () => mState();
</pre></div>
<p>audio 标签的开始播放、暂停这两个事件在多种情形下必会触发其一:每次点击 btnPlay 按钮之时都会可能触发,页面刚打开时、播放结束时也可能会触发。因此,监听它们并以此运行 mState() 函数非常牢靠。mState 函数要做的工作之一是管理按钮形状,将 CSS 的两个选择器 .play {...} 和 .pause {...} 所描述的 CSS 剪裁变量 --clip 数据以 className 的赋值方式交给 btnPlay,这样按钮的形状就会自动改变。之前,btnPlay 标签代码中有两个class名称,className="btnPlay play",其中的 play 是临时绑定的,我们现在就是动态改变它是 play 还是 pause,所有用一个数组来包含它们俩,['pause', 'play'],这个数组的下标(即序号)由 audio 标签的 paused 暂停属性来决定,audio.paused 返回 true(真,即音乐暂停中的状态)或 false(假,即音乐暂停状态为假),true 和 false 变量前加 + 号可以令其变为 1(真)或 0(假),如此使用 +audio.paused 作为下标读取记录暂停和播放选择器的数组 ['pause', 'play'] 的至就能对应当前的状态、给出正确的形状描述。</p>
<p>按钮形状的事情解决了,下来时进度条的着色、滑块的运行问题,两者共享一个 CSS 变量 --prg,我们只需计算出音频当前的播放进度与音频总时长的关系就行,具体说即为,--prg 变量是用于驱动进度指示色、滑块运行相对与进度条长度的百分比,这个百分比其实对等于音乐的播放进度,即它等于音频当前播放时间和音频总时长的比例。代码放在 audio.ontimeupdate 事件中,因为该事件是音乐播放时的音频播放时间更新事件:</p>
<div class="divBox2" data-name="JS代码"><pre class="preBox2">
audio.ontimeupdate = () => {
// ... 这里是其它相关
// 下面是利用 audio 的当前播放时间 currentTime 和 音频总时长
// duration 二者的关系来计算进度百分比并赋值给 progress 标签
progress.style.setProperty('--prg', `${audio.currentTime / audio.duration * 100}%`);
};
</pre></div>
<p>本节的最后一个要实现的功能是数字时间信息问题。我们在 CSS 代码中设置了一个 mplayer 标签的伪元素,它的内容属性 content 是 attr() 函数,函数值为一个特定变量 data-time,用来接收 HTML 相应标签代码中同名属性 data-time 传递的数据,JS 可以动态变更 HTML 相应标签所需的数据,用 dataset.* 指代 data.*。当然,音频标签提供的播放时间、音频时长是浮点数形式的秒数,我们需要把它们换算成便于阅读的 mm:ss 格式,这需要一个换算函数来处理,然后也是在 audio 的 timupdate 监听事件中获取时间信息时调用该换算函数:</p>
<div class="divBox2" data-name="JS代码"><pre class="preBox2">
// 秒数换算为 mm:ss 格式
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 音频 timeupdate 监听事件
audio.ontimeupdate = () => {
// ... 这里是其它相关
progress.style.setProperty('--prg', `${audio.currentTime / audio.duration * 100}%`);
mplayer.dataset.time = `${formatTime(audio.currentTime)} ${formatTime(audio.duration)}`;
};
</pre></div>
<p>两个时间信息都由 formatTime() 函数进行格式化输出,彼此间相隔一个空格,这样他们就会在 mplayer 伪元素中两端对齐。</p>
<p>至此,播放器已经具备了一些初始功能,我们结合上一环节的代码把播放器至今为止的 CSS、HTML、JS 代码整理一下:</p>
<div class="divBox2" data-name="JS代码"><pre class="preBox2">
<div id="hEdiv"><pre id="hEpre">
<style>
.mplayer { position: relative; width: 300px; height: fit-content; display: flex; flex-direction: column; align-items: center; gap: 10px; margin: auto; margin-top: 100px; }
.mplayer::before { position: absolute; content: attr(data-time); width: 100%; text-align-last: justify; pointer-events: none; }
.btnPlay { width: 20px; height: 20px; cursor: pointer; position: relative; }
.btnPlay::after { position: absolute; content: ''; width: 100%; height: 100%; background: red; clip-path: var(--clip); }
.progress { --prg: 0%; position: relative; width: 100%; height: 20px; display: grid; place-items: center start; background: linear-gradient(90deg, red var(--prg), gray var(--prg), gray 0) no-repeat center/100% 2px; padding: 0; margin: 0; }
.thumb { position: absolute; left: calc(var(--prg) - 10px); width: 20px; height: 20px; background: red; border: 8px solid green; border-radius: 50%; cursor: pointer; box-sizing: border-box; }
.play { --clip: polygon(10% 0,100% 50%,10% 100%); }
.pause { --clip: polygon(35% 0,15% 0,15% 100%, 35% 100%,35% 0,75% 0,75% 100%,55% 100%,55% 0); }
</style>
<div class="mplayer" data-time="00:00 00:00">
<div class="btnPlay play"></div>
<div class="progress">
<div class="thumb"></div>
</div>
</div>
<audio src="https://music.163.com/song/media/outer/url?id=30031117" autoplay loop></audio>
<script>
//获取需要操作的元素标识
const mplayer = document.querySelector('.mplayer');
const btnPlay = document.querySelector('.btnPlay');
const progress = document.querySelector('.progress');
const thumb = document.querySelector('.thumb');
const audio = document.querySelector('audio');
//时间格式化工具函数 :秒转分秒 mm:ss 格式
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 联动函数 mState :处理按钮形状
const mState = () => {
btnPlay.className = `btnPlay ${['pause', 'play'][+audio.paused]}`;
};
// 音频标签开始播放和暂停时执行联动函数
audio.onplaying = audio.onpause = () => mState();
// 音频时间更新事件 :驱动文本时间信息及进度条进度变更
audio.ontimeupdate = () => {
// 其它代码
mplayer.dataset.time = `${formatTime(audio.currentTime)} ${formatTime(audio.duration)}`;
progress.style.setProperty('--prg', `${audio.currentTime / audio.duration * 100}%`);
};
// 按钮单击 :播放、暂停状态切换
btnPlay.onclick = () => audio.paused ? audio.play() : audio.pause();
</script>
</pre></div>
</pre></div>
<p>上面的代码可以 <span id="showRes">点击这里</span> 运行,也可以将其复制到 <a href="http://mhh.52qingyin.cn/api/pencilcode/editor.html" target="_blank">简易编辑器</a> 或将其存为本地 .html 文档进行修改。</p>
<p>下一环节:<a href="https://www.huachaowang.com/forum.php?mod=viewthread&tid=82630&extra=page%3D1" target="_blank">花潮经典版播放器开发环节之功能实现机制(二)</a></p>
</div>
<script type="module">
import hlight from 'https://638183.freep.cn/638183/web/helight/helight1.js';
var div2s = document.querySelectorAll('.divBox2'),
pre2s = document.querySelectorAll('.preBox2');
div2s.forEach((div, key) => hlight.hl(div, pre2s));
showRes.onclick = () => {
var pw = window.open('', 'prev', 'width=800,height=280,top=200,left=200'),
pcode = pre2s.textContent;
pw.document.open();
pw.document.write(pcode);
setTimeout(function() {
pw.document.title = "预览";
}, 0);
pw.document.close();
};
</script>
到了最难的地方了,前面的是css和HTML,都还好理解,这个开始还能看明白,到了进度条的位置就看得比较吃力了,到了数字时间的转换,更是看得直接模糊了{:4_173:} 当然黑黑的讲解还是很清晰的,是我自己在JS的学习上一直有缺陷{:4_181:} 去运行了一下,好像进度不能拖动,如果拖动还要有更多代码吧。估计更要看迷糊了{:4_173:} 红影 发表于 2025-3-30 19:32
去运行了一下,好像进度不能拖动,如果拖动还要有更多代码吧。估计更要看迷糊了
拖曳是下一个环节的任务。这个环节,一开头就说要做什么 红影 发表于 2025-3-30 19:30
到了最难的地方了,前面的是css和HTML,都还好理解,这个开始还能看明白,到了进度条的位置就看得比较吃力 ...
第三个环节还有难 红影 发表于 2025-3-30 19:31
当然黑黑的讲解还是很清晰的,是我自己在JS的学习上一直有缺陷
其实一句一句吃透也不是天大的事 JS帅呆了,小白直接被迷晕了。。。溜了溜了,灌水去了{:4_170:} 认认真真看完一遍。
结论:只有最强大脑才能整出这么复杂程序。。脉络清晰,逻辑完美。。点好多赞。。
音乐好听{:4_173:} 花飞飞 发表于 2025-3-30 22:12
认认真真看完一遍。
结论:只有最强大脑才能整出这么复杂程序。。脉络清晰,逻辑完美。。点好多赞。。
音 ...
音乐是随便找找就找到好听的 花飞飞 发表于 2025-3-30 21:33
JS帅呆了,小白直接被迷晕了。。。溜了溜了,灌水去了
应该不是特好懂。我本想谢成“
如果。。。那么。。。要不然。。。酱紫的话。。。
这样就会好懂一点,但是代码行数会多太多 马黑黑 发表于 2025-3-30 22:14
音乐是随便找找就找到好听的
金手指啊,我可以找半天都找不到好听的{:4_173:} 花飞飞 发表于 2025-3-30 22:18
金手指啊,我可以找半天都找不到好听的
你主要是没戴国产耳机 马黑黑 发表于 2025-3-30 22:15
应该不是特好懂。我本想谢成“
如果。。。那么。。。要不然。。。酱紫的话。。。
思虑好周全。。太不容易了。。
JS这块大骨头对小白来说可真不好啃{:4_173:} 马黑黑 发表于 2025-3-30 20:31
拖曳是下一个环节的任务。这个环节,一开头就说要做什么
嗯嗯,还好,相对还容易点{:4_173:} 马黑黑 发表于 2025-3-30 22:19
你主要是没戴国产耳机
哼哼,国产耳机找华语的呀。。
说到这个,最近我找的这两首听着还行{:4_173:} 花飞飞 发表于 2025-3-30 22:20
思虑好周全。。太不容易了。。
JS这块大骨头对小白来说可真不好啃
我这个JS代码,估计大漠大佬也看不懂{:4_170:} 马黑黑 发表于 2025-3-30 20:31
第三个环节还有难
是啊,越到后面越难了{:4_173:} 马黑黑 发表于 2025-3-30 20:32
其实一句一句吃透也不是天大的事
脑力不够,吃不透了{:4_173:} 马黑黑 发表于 2025-3-30 22:22
我这个JS代码,估计大漠大佬也看不懂
啥意思。。纯自创呗。。你有自己一套逻辑{:4_173:}