MENU
前言
实现一个简单的跑马灯功能,原本觉得没什么,就是一个div从右往左移动就可以了。但是没想到真正动手起来,却发现很多细节问题,大概知识生疏了,一时也没想到如何解决,后来才想到解决方案。
这篇文章的跑马灯基本使用css实现,但也有少量的JavaScript。
父元素高度塌陷解析。
跑马灯的基本代码
先贴上基本代码(不含动画),下面的示例均以这些基本代码为基础进行修改。
* { margin: 0; padding: 0; } .marquee-container { width: 100%; height: 50px; line-height: 50px; background-color: cadetblue; overflow: hidden; } .marquee-box { display: inline-block; color: #fff; white-space: nowrap; animation: marquee 3s linear infinite; }
<div class="marquee-container"> <div class="marquee-box"> <p>123456789</p> </div> </div>
跑马灯的错误实现
看到很多博客都说使用css实现跑马灯的方法有两种,但我发现这两种方法都存在一定的问题。
错误方法一
.marquee-container { position: relative; } .marquee-box { position: absolute; } @keyframes marquee { 0% { left: 100%; } 100% { left: -100%; } }
虽然动画可以动起来,但是只要仔细观察,就不难发现当内容体移出容器的最左边边界时,内容体并没有立刻重新出现在右边边界,也即是在某个时间段,内容体并不会展示出来。
这是因为100%
动画帧的left: -100%;
导致的,left: -100%;
并不是指内容体相对于自身宽度的-100%
的水平位移,而是指内容体相对于容器的宽度的-100%
的水平位移,这意味着内容体并非移出容器外后立刻出现在起点。
即使能够接受内容体不会立刻重新出现右边边界
的情况,这种写法依然是错误的,只要跑马灯内容宽度足够长时,跑马灯动画结束的地方可能落在容器内的某个位置,而不是边界或边界以外的地方。
错误方法二
@keyframes marquee { 0% { transform: translateX(100%); } 100% { transform: translateX(-100%); } }
这段动画和错误方法一的动画的错误比较类似。首先当内容过短时,
0%
动画帧的translateX(100%)
根本就不能以容器的右边界做为起点。这是因为translateX
里的100%
是相对内容体自己的宽度的100%
的水平位移。
其次当内容宽度过长时,起点会离容器的右边界太远。
错误方法总结
由此可见,上面的两种错误方法主要有两个缺陷。
1、受到内容体的宽度影响,内容体过长或过短时,可能会引发bug。
2、无法精准定位内容体移动的起点或终点。
跑马灯的正确实现
改进一(不再受内容宽度影响,并且在特定场景精准定位内容体移动的起点和终点)
事实上,错误方法二的使用
translateX
的思路是正确的,只是0%
动画帧的translateX(100%)
需要改一下。只需要改为以下的代码就基本正确了。@keyframes marquee { 0% { transform: translateX(100vw); } 100% { transform: translateX(-100%); } }
100vw
即视口的宽度,在这个场景里,容器的width
为100%
,等同于视口宽度,也即是说内容体translateX(100vw)
后肯定刚好落在容器的右边边界。
这个方法解决了两个问题,一个是不受内容体的长短影响,第二个是能够精准定位内容体移动的起点和终点。
但是,这个方法也存在一定缺陷,只适用于跑马灯容器的宽度等于视口宽度的情况。
原因是当容器宽度不等于视口宽度时,内容体translateX(100vw)
后,它的移动起点永远落在视口的右边界外,不会再精准落在容器的右边界。
改进二(自适应不同的跑马灯容器宽度)
这个方法的实现比较巧妙,使用
left
和translate
组合进行实现动画,无论你的跑马灯容器宽度如何,都可以进行自适应。.marquee-container { position: relative; } .marquee-box { position: absolute; } @keyframes marquee { 0% { left: 100%; } 100% { left: 0%; transform: translateX(-100%); } }
其实很简单,就是使用
absolute
+left
进行实现最基本的移动,当元素从left: 100%;
移动到left: 0%;
时,内容体刚好位于容器的最左侧,但没有移出容器的左边界外,这里再加一个transform: translateX(-100%);
,向左移动内容体100%
的宽度,即可将内容体刚好移出跑马灯容器位置。
这里还存在一个问题,跑马灯的内容都是从接口请求数据,内容是动态变化,这说明内容宽度不一致。不同宽度的内容在相同的播放动画时间里,它们的移动速度是不一致的,过长的内容会移动得相对较快,过短的内容会移动得相对较慢。
所以,不论内容宽度如何,我们要求动画都要有一个恒定的移动速度,而不是恒定的动画时间。
改进三(动态改变动画时间,得到恒定的速度)
只有一个字的内容和有几百个字的内容在相同的动画时间里,肯定是前者速度极快,后者速度极慢,因为前者的位移量比后者的位移量要短很多,因此需要根据内容去动态改变动画时间。
首先,复习下初中物理的简单的位移公式:s = vt
,s为位移,v为平均速度,t为时间。
现在要根据不同的内容宽度求一个动态时间,可得时间公式:t = s / v
。
先看下速度v,速度v其实就是我们要定义一个的恒定速度(以px单位,因为下面位移公式也是以px单位),可以假设定义为:150px / s
。
然后看下位移s怎么求。
在此之前,先说一下为什么在该篇文章一直称跑马灯的文字内容长度
为内容宽度
,而不是内容长度
。
因为如果用了内容长度
这个词语,某些读者的思维可能会被引导到,求位移就是获取跑马灯的内容字符串,然后使用字符串.length
进行去获取字符串长度,再根据字符串长度进行计算位移。但是这个所谓的字符串长度其实是字数,并非文字所在的dom
实际宽度。
这里要用dom
的实际宽度去求解位移的两个原因。
1、在等同的font-size
中,一个数字与一个中文在实际的dom
上所占的实际宽度是不一致的,但它们都可归为一个字,因此用字数去代表位移,肯定有误差。
2、没有人说过跑马灯内容只能显示纯文字,如果要在某个关键字里添加一个a
链接,那是不是计算字数的时候要排除这个a
标签。而且如果这里还可以添加emoji
表情和图片呢?那是不是又要额外处理?
好了,回到正题。
位移s = 跑马灯内容宽度 * 2 + 跑马灯容器宽度
可以很简单的得到以下的动态计算动画时间的JavaScript代码。<script> // 跑马灯容器宽度 let containerWidth = document.querySelector('.marquee-container').offsetWidth; // 跑马灯内容宽度 let boxWidth = document.querySelector('.marquee-container .marquee-box').offsetWidth; // 动画时间,这里我没有四舍五入,你可以进行四舍五入 let duration = (boxWidth * 2 + containerWidth) / 150 + 's'; document.querySelector('.marquee-container .marquee-box').style.cssText = 'animation-duration:' + duration; </script>
不过,这段动画看起来似乎有一丢丢的抖动掉帧的感觉,我朋友也是这么说的,目前还没有解决的头绪。
完整代码
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.box {
position: relative;
width: 68px;
margin: 0px auto;
background-color: cadetblue;
overflow: hidden;
}
.item {
position: absolute;
display: inline-block;
padding: 2px 0px;
color: #fff;
white-space: nowrap;
}
@keyframes anim {
0% {
left: 100%;
}
100% {
left: 0%;
transform: translateX(-100%);
}
}
</style>
<div id="idBox" class="box">
<div id="idItem" class="item"></div>
</div>
<script>
const str = '程序、编程、写代码、互联网、物联网、技术栈、程序员、工程师、软件开发、硬件开发、计算机科技、智慧电子模块';
function init(val = '更多关于我的消息,请关注“代码农”微信公众号。', speed = 150) {
let len = val.length;
idItem.textContent = val;
setTimeout(() => {
let height = idItem.offsetHeight;
let idBoxW = idBox.getBoundingClientRect().width;
let idItemW = idItem.getBoundingClientRect().width;
let duration = (idItemW * 2 + idBoxW) / speed;
duration = Math.round(duration);
idBox.style.height = height + 'px';
idItem.style.animation = `anim ${duration}s linear infinite`;
// running paused
idItem.style.animationPlayState = 'running';
}, 0);
}
init(str);
</script>