1. 前言
书接上回,前几篇文章我们大都在谈规划,当然后面我们还会谈规划,不过没有前面几篇文章谈的那么多,废话不多说,接下来我们进入正文。
在这篇文章中我们会进行视频控件的编写,不过实现细节很多,后面会再撰写几篇文章细讲。
实现一个功能我们要尽可能先将简单的功能实现,较难的功能放到较后来实现。所以这篇文章要讲的功能实现是视频控件面板的显示和隐藏
。
另外,各位看客的点赞与收藏就是我源源不断的动力,有兴趣的可以关注该专栏,如果有合适的功能需求,专栏也会进行更新。为了大家,爆发吧小宇宙!!!╰(‵□′)╯
2. 分析
相信大多数人都能快速的写出控件的显示和隐藏的代码,所以这里也就不啰嗦了。不过开始前我们得先规划一下。
控件什么时候显示?
- 当视频正在播放,并且鼠标在视频播放器中发生了移动,即触发了
mousemove
事件时。 - 当鼠标移入控件面板并且未移出时。
- 当视频暂停时。(这个可加可不加,因为控件显示时会遮挡内容,而用户暂停可能就是为了看一下视频内容)
控件什么时候隐藏?
- 当视频播放时,并且鼠标未发生移动,也不在控件面板中时,会过一段时间自动隐藏。
下面展示了鼠标移动时显示控件面板。
2.1. 实现
下面我们编码顺序是先写页面,再写逻辑。当然这个顺序并不是必须的,只要你觉得哪种顺序你能更快地、更清晰地实现功能,那就是适合你的顺序。
2.1.1. 编写页面
下面是相关的 HTML 代码。其中 class 值为 video-controls
的 div 就是控件面板元素。
html
<div class="video-container">
<div class="video-instance">
<video src="./spy.mp4" controls></video>
</div>
<div class="video-controls"></div>
</div>
下面是 video-controls 的 css 样式。其他元素的样式在上篇文章有提过,这里就不再赘述了。
css
.video-controls {
position: absolute;
z-index: 2;
bottom: 0px;
width: 100%;
height: 65px;
background: -webkit-linear-gradient(top, #ffffff00, #000000db);
}
具体的效果如下图所示:
好了,页面代码是非常简单的,下面我们就开始书写逻辑代码了。
2.2.2. 逻辑实现
下面我们将会涉及到比较深入的问题了,先来分析一下。
- 控件面板显示和隐藏需不需要动画过渡?如果需要使用什么动画?如何实现?
- 控件面板显示和隐藏时是否也需要执行相关的回调函数?
- 鼠标如果持续移动是否需要进行限制?如果限制,使用防抖还是限流?
通过以上几个简短的问题,我们能大致理清控件面板显示和隐藏的实现方向。下面是对于上面问题的回复:
-
我们的控件面板显示和隐藏将会使用
渐现、渐隐
的动画过渡,主要通过元素的opacity
属性来实现,然后通过 display 来控制元素的显示和隐藏。 -
我们无法确定控件显示和隐藏是否会
执行相关的回调函数
,对于这种不确定项,可加可不加,当然为了提高代码的可维护性,可以实现相关的逻辑。当然之后你可以看到我们是会加上的,因为确实会用到。 -
由于在视频播放时我们无法确定用户的鼠标何时会移动,并且在用户的鼠标移动时我们要保证控件一定得显示出来。如果不加限制,很明显可以是可以,但是频率太高,就算电脑吃得消,也是在浪费性能,所以我们需要加以限制。那么使用限流还是防抖呢?如果使用防抖,可是防抖是在用户持续触发结束后的最后一次才执行,由于我们只是限制执行次数,限流是在一段时间内执行一次,很明显
限流
符合我们的要求。
分析完毕,接下来就是编写逻辑代码了。还是先从简单的开始吧!我给上面的实现难度进行了排序:
难度顺序:执行相关的回调函数 < 操作限流 < 渐现、渐隐过渡动画。
大家可能对我给出难度排序有疑问,动画过渡有什么难的,没事,很正常,因为刚开始我也是这么觉得,哈哈~
2.2.2.1. 执行相关的回调函数
代码实现比较简单,所以我就不解释了哈 = ̄ω ̄=
其中 window.execCallback
是一个通用的函数,后面还会用到,大概知道它是做什么的就行了。
js
"use strict";
/**
* 显示控件后执行的回调
* @type {[function]}
*/
const showControlsCallbackArr = [];
/**
* 隐藏控件后执行的回调
* @type {[function]}
*/
const hideControlsCallbackArr = [];
/**
* 执行回调函数
* @param callbackArr {function[]} 回调函数数组
* @param args {...*} 传入函数的形参
*/
function execCallback(callbackArr, ...args) {
callbackArr.forEach((fn) => {
fn(...args);
});
}
// 在控件面板显示时我们会运行这段代码:
window.execCallback(showControlsCallbackArr);
// 在控件面板隐藏时我们会运行这段代码:
window.execCallback(hideControlsCallbackArr);
2.2.2.2. 操作限流
相信限流的代码大家已经烂熟于心了吧,其实我还是记不太清,记性不好,所以一般好久没用还会去网上搜索 js 限流
。骗你们的,其实我是为了给别人的文章增加热度,哈哈~
对于限流,我认为有两种方式,区别大同小异:window.setTimeout()
和 window.performance.now()
这两个 API 都可以实现限流。我使用的是第二种,因为它更准确!
- window.setTimeout 实现限流,亲测有效。
js
function getThrottle(fn, delay) {
let timer = null;
return function() {
if (!timer) {
timer = window.setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
}
}
let throttle = window.getThrottle(() => {
console.log("running");
}, 1000);
for(let i = 0; i < 1000; i++) {
throttle();
}
- window.performance.now() 实现限流,同样亲测有效。
js
/**
* 获取节流函数
* @param fn {function} 要节流的函数
* @param delay {number} 延迟执行的时间, 单位是毫秒
* @return {function(...*): *}
*/
function getThrottle(fn, delay) {
let now, last;
return function (...args) {
now = performance.now();
if (!(last && now < last + delay)) {
last = now;
return fn && fn(...args);
}
}
}
不知有没有看客不知道 为什么要返回一个函数
的,这里我多嘴一句,因为我们在其他地方可能还会使用限流,这种方式使用闭包封装了一个私有变量,而 getThrottle 方法每次返回的函数都是不同的,它们之间互不影响。这样我们就能多次使用限流逻辑而不必再编写重复代码了。
同时这里我并没有使用 fn.apply(this, args);
而是使用 fn(...args);
。下面是我的看法:
通常 getThrottle 方法都书写在全局环境中,即 window.getThrottle() 可以调用,所以我认为如果 getThrottle 返回的函数(fn)如果不添加在一个对象(obj)上,在严格模式下 this 的值将始终为 undefined
,而通常我们如果能执行这个返回的函数(fn),就也能获取到它所处的那个对象(obj),也就是 this 有点冗余的意思了。
另外需要注意了,如果你在严格模式的全局环境下直接调用函数,this 是 undefined,非严格模式下是 window。具体的原因是严格模式下 this 不会进行默认绑定。举个例子让大家更易懂一点。
js
// 处于非严格模式
// "use strict";
// 比如我有个 fn 函数
function fn() {
console.log(this);
}
fn(); // 输出 window 对吧?对的!
window.fn(); // 输出 window 对吧?也是对的!
再来个严格模式的例子:
js
// 处于严格模式
"use strict";
// 再来个 fn 函数,fn:好的主人!
function fn() {
console.log(this);
}
window.fn(); // 输出 window 对吧?还是对的!
fn(); // 输出 window 对吧?这里就不对咯!输出 undefined
通过上面的例子我们应该就知道为什么执行 fn(); 时为什么 this 还会是 window 了,是因为非严格模式 this 会进行默认绑定到 window。简而言之就是别人替我们做了,别人替我们负重前行对吧?
如果有跟不上的小伙伴听好了,现在我们还处于打地基的阶段,需要循序渐进,把基本的功能写好,最后慢慢整合,功能自然就实现了!所以跟不上的小伙伴先别慌,放轻松,你都已经这么努力看到这里了,天道酬勤。后面的内容会上点难度,倒不是难在理解,而是难在细心,因为我们会开发一个通用型的渐现、渐隐动画。
好了,扯得有点远了,收!
2.2.2.3. 渐现、渐隐动画过渡
相信大家都遇到过这样的渐变过渡动画制作,大家是如何解决的呢?使用 JQuery 的 $(el).fadeIn(); ?还是 vue 提供的 transition 组件?
如果让你去实现下面这样的效果,你会怎么实现?
看起来很简单对吧?可以自己尝试一下,其中的细节还是比较多的。
我也使用了 JQuery 和 Vue 去实现这个效果,发现纯靠添加或删除类名是实现不了这个效果的,所以 Vue 的 transition 的这个方法是不可行的,如果有小伙伴使用 transition 实现了,请在评论区留言分享一下。亲测 JQuery 是可以实现的,而且代码挺简单的,所以这个效果以后可以使用 JQuery 来快速开发。
我们最终要实现的效果大概就是上面图片的那个样子,可以看到这个透明度的渐变还是需要一定的通用性,因为页面上许多地方都要用到。下面我们会尝试用各种方法去尽可能的实现它,讨论对应的方法是否可行,以及它的优劣。
这里列出了几种方法,我们会尝试下面这几种方式去实现:
- 通过鼠标 hover 去实现。
- 通过动画帧 animation 去实现。
- 通过 js 控制动画。
2.2.2.3.1. 通过鼠标 hover 实现
刚开始时我也是觉得这个实现起来应该挺简单的,因为我第一想到的就是使用 hover 伪元素去实现,开始时我还兴致勃勃的去写了好多代码,觉得马上就写好了,但是写到最后发现了一些问题,接下来我们细说。
注意:下面简单的代码就不给出了,因为本文章篇幅有点长了。所以各位看客在看的时候最好是能进行相应的编码尝试,不然你的理解可能会出现歧义。下面的代码都是实践出来的,所以自行编码会理解的更快。
这里我画了一张简图,通常我们都是希望鼠标移入触发元素然后展示元素渐现,移出则渐隐。按道理 hover 应该能完全胜任才对。我也曾这样想过,还亲自做过😂。(所以我为大家排雷来了,哈哈~)
当你写完了,你会发现你又想把之前写的代码给删了,为什么?我们不仅想让设置元素的 opacity
属性,还想设置它的 display
属性为 none 让它滚蛋,但是直接在 hover 里面加的话,过渡并不能生效,因为 none 不是数值,无法过渡。
当然你可能也会立马说 hover 与 js 事件结合起来怎么样?当然我之前并没有尝试过,这是我写这篇文章的时候想到的,所以说帮助大家学习也是帮助我自己学习啊!
具体的思路是:hover 的那部分代码留着,同时我们给展示元素添加一个 transitionend
事件,当过渡结束时设置它的 display 就行了,我说不能实现不是不能实现,得让我们的水平来发话哈。
好,这段代码我就贴出来了,下面展示了一部分哈,其他的自己脑补~
html
<style>
.container {
position: relative;
width: 200px;
height: 200px;
background-color: #dfdf33;
}
.container:hover .box {
opacity: 1;
}
.box {
position: absolute;
top: 0px;
right: -120px;
opacity: 0;
width: 100px;
height: 100px;
background-color: #3344ff;
transition: all 1500ms;
}
</style>
<div class="container">
<div class="box"></div>
</div>
<script>
const container = document.querySelector(".container");
const box = document.querySelector(".box");
let show = false;
box.addEventListener("transitionend", () => {
let computedStyle = window.getComputedStyle(box);
if(computedStyle.style.display !== "none") {
// 这里已经无从下手了
}
});
</script>
看完了是不是感觉写的好好的,然后就看到 "无从下手" 了?原因是我们无法确定当前的 transitionend
到底是隐藏元素还是显示元素,可以看到我还试图通过定义一个 show 变量来实现,但是还是于事无补。
其实这里还可以在这个 show 变量上下文章,下面是我思索片刻写出的代码,用来彻底断了大家使用 hover 的念头,哈哈~ 坏人我做定了~~
html
<style>
.container {
position: relative;
width: 200px;
height: 200px;
background-color: #dfdf33;
}
.container:hover .box {
opacity: 1;
}
.box {
position: absolute;
top: 0px;
right: -120px;
opacity: 0;
width: 100px;
height: 100px;
background-color: #3344ff;
transition: all 1500ms;
}
</style>
<div class="container">
<div class="box"></div>
</div>
<script>
const container = document.querySelector(".container");
const box = document.querySelector(".box");
let enter = false;
container.addEventListener("mouseenter", () => {
enter = true;
let computedStyle = window.getComputedStyle(box);
// box 的类样式(不是行样式哈)中 display 可能为 flex 或者是其他值
box.style.display = "";
document.documentElement.clientHeight;
computedStyle = window.getComputedStyle(box);
if(computedStyle.display === "none") {
// 如果还不显示就上大招了
computedStyle.display = "block";
}
});
container.addEventListener("mouseleave", () => {
enter = false;
});
box.addEventListener("transitionend", () => {
if(!enter) {
box.style.display = "none";
}
});
</script>
不知道大家伙们能看得懂增加的这些代码不,如果没看懂的小伙伴可以在评论区求助~~ 救救我~~~ ,也可以私信我,因为后面还会讲到类似的代码。这里的代码已经有最终代码的雏形了嘿,坚持就是胜利!这篇文章确实写得有点长了诶,我也写累了😅
说一下上面的代码为什么不行,由于口说无凭,各位可以复制上面的代码去试试,能发现它在显示元素时,透明度突然变化为 1,没有过渡,与在 hover 的类样式中直接设置 display: block 无异
。大概原因也许是 hover 比 mouseenter 事件先执行吧,顺序反一下就能用了。
到这里我们就能淘汰掉这个 hover 方法了,主要就是因为 display 无法过渡的限制。
下面和各位说一下该方法的优劣点:
优点如下:
- 它能在不添加 js 事件的基础上实现元素的透明度变化。
- 它能在鼠标移出并快速移入时,保证元素透明度不突然变化。
缺点如下:
- 它会由于 display: none 导致过渡失效。
2.2.2.3.2. 通过动画帧 animation 实现
通过动画帧实现,这个方法其实刚想到就能淘汰掉了,这里我就说一下不采用它的原因吧。因为它只能从一个状态到另一个状态,中间不能打断,而我们需要能打断。
不能打断的话我们就不能修改 animation 的值,无法让透明度缓慢变化。
2.2.2.2.3. 通过 js 实现
我们最终采用的方法就是这个方法哈,所以接下来就是重头戏了。敲黑板了!!转下篇文章。
从零制作视频播放器------别人能搞得出来视频控件,我们能搞得更好!(第四章)
3. 中场休息
篇幅太长了,差几百个字快一万了,虽然其中有代码和扯蛋的内容,不过这么冷的天,家里又没暖气,我得不时的对手哈气让手暖和一些。这里就进行掐断了哈,当然两篇文章是同时发的,不用担心没下文。
各位家财万贯的老板!你的点赞和收藏就是我源源不断的动力,感兴趣的可以关注一下该专栏,有对人家感兴趣的也可以关注我嘛~ 😋 文章没热度更新频率就低了哈~ 我这个人不开玩笑的哈~~ 😁🤞😘