从零制作视频播放器——别人能搞得出来视频控件,难道我们不能搞?(第三章)

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. 逻辑实现

下面我们将会涉及到比较深入的问题了,先来分析一下。

  1. 控件面板显示和隐藏需不需要动画过渡?如果需要使用什么动画?如何实现?
  2. 控件面板显示和隐藏时是否也需要执行相关的回调函数?
  3. 鼠标如果持续移动是否需要进行限制?如果限制,使用防抖还是限流?

通过以上几个简短的问题,我们能大致理清控件面板显示和隐藏的实现方向。下面是对于上面问题的回复:

  1. 我们的控件面板显示和隐藏将会使用 渐现、渐隐 的动画过渡,主要通过元素的 opacity 属性来实现,然后通过 display 来控制元素的显示和隐藏。

  2. 我们无法确定控件显示和隐藏是否会 执行相关的回调函数,对于这种不确定项,可加可不加,当然为了提高代码的可维护性,可以实现相关的逻辑。当然之后你可以看到我们是会加上的,因为确实会用到。

  3. 由于在视频播放时我们无法确定用户的鼠标何时会移动,并且在用户的鼠标移动时我们要保证控件一定得显示出来。如果不加限制,很明显可以是可以,但是频率太高,就算电脑吃得消,也是在浪费性能,所以我们需要加以限制。那么使用限流还是防抖呢?如果使用防抖,可是防抖是在用户持续触发结束后的最后一次才执行,由于我们只是限制执行次数,限流是在一段时间内执行一次,很明显限流符合我们的要求。

分析完毕,接下来就是编写逻辑代码了。还是先从简单的开始吧!我给上面的实现难度进行了排序:

难度顺序:执行相关的回调函数 < 操作限流 < 渐现、渐隐过渡动画。

大家可能对我给出难度排序有疑问,动画过渡有什么难的,没事,很正常,因为刚开始我也是这么觉得,哈哈~

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 都可以实现限流。我使用的是第二种,因为它更准确!

  1. 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();
}
  1. 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. 中场休息

篇幅太长了,差几百个字快一万了,虽然其中有代码和扯蛋的内容,不过这么冷的天,家里又没暖气,我得不时的对手哈气让手暖和一些。这里就进行掐断了哈,当然两篇文章是同时发的,不用担心没下文。

各位家财万贯的老板!你的点赞和收藏就是我源源不断的动力,感兴趣的可以关注一下该专栏,有对人家感兴趣的也可以关注我嘛~ 😋 文章没热度更新频率就低了哈~ 我这个人不开玩笑的哈~~ 😁🤞😘

相关推荐
Devil枫26 分钟前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
尚梦1 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子2 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
前端青山2 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
毕业设计制作和分享2 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
从兄3 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
清灵xmf4 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
大佩梨4 小时前
VUE+Vite之环境文件配置及使用环境变量
前端
GDAL4 小时前
npm入门教程1:npm简介
前端·npm·node.js
小白白一枚1115 小时前
css实现div被图片撑开
前端·css