前言
在很多场景下我们需要展示一串数字,这串数字可以是写死固定在页面上的,也可以是动态刷新实时请求的,还有一些是根据用户的交互产生变化的数字。之前我们网站在数字发生变化时是用anime.js
做的类似于这样的一种动画:

用anime.js
做这种动画的其中一个缺点就是:数字中间不能像上面那张图一样有逗号。就可以简单的理解为必须是number
类型的值,字符串'6,000.00'
这种就不行。当然我们可以为每一串或者每一个数字单独应用效果,只不过效果没有上图那么好罢了,大概效果类似于这样:

估计支付宝也是遇见了跟我们同样的问题,显示在页面上的内容看起来像是数字
,但实际上确是字符串
,只能用类似于split
一样的方法找到逗号和小数点的位置进行分割,然后再把得到的数字字符用parseInt
解析成数字类型然后再应用这种效果。
产品那边对这种效果一直不是很满意,终于在一次开会时:
产品组:我们本期的主要任务是要优化交互体验,大家用过饿死了么
?他们的软件在数字这方面有这样的一种效果:

开发组:你这啥欣赏水平啊?想要改成它们这个*
样?
产品组:当然不是,只是给大家看这种效果,刚才那个效果可能不太好,那我们再给大家换一个页面看看吧:

开发组:。。。
你要是实在闲的没事干就去打扫打扫卫生吧!别想起一出是一出了,网站用户迟早让你给搞到流失没了…
产品:哎呀不是!我想要的效果没有这么难看!只是这个效果让我想起了老虎机🎰前两天还在饿死了么
里看到过呢!等我找找… 喏!就是这个:

研发:行 知道了 就按照这个效果做是吧?
产品:诶~先别走啊!我还没说完呢!首先我觉得这个方向不对,咱们要做成像老虎机那种从上往下滚的:

然后不能像饿死了么
那效果一样就那么慢慢的停住…
研发:不慢慢的停住还想咋停住?快快的停住?
产品:不是这个意思,是因为慢慢停住那种效果太普通了,咱们能不能做出来更加动感一点的效果?
研发:你想怎么个动感法?
产品:就是类似于要停住但是速度太快没能刹住,过去了一点然后再弹回来,你们有什么专业名词能形容这种效果么?
研发:就是回弹效果呗?
产品:对!就是这个!
研发:可以!没问题!比你刚给我们看的那个动画强。
动画展示
为了防止大家想象不出来具体是什么样的效果,我们先展示一下已经写好并且已实际应用在我们页面上的一些组件:

上面这串数字好像有点不太吉利啊…… 赶紧换一个⬇


这样是不是就很有感觉了呢?
这让我想起以前在中学时看过的一部电影:《夺命手机》
男主角靠着一部开了挂的手机进入拉斯维加斯的大赌场,短短几分钟之内就疯狂赚取十万欧元💶 他来到一部老虎机的面前投币后按下按钮,那部老虎机就自带这种回弹效果:

个人觉得这个回弹效果不够动感不够带劲,所以用CSS加强了一下回弹效果,不知大家是喜欢饿死了么
那种无回弹效果、还是喜欢拉斯维加斯
这部老虎机的轻微回弹效果、还是喜欢本篇文章
将要开发出来的动感回弹效果呢?
祝点赞和关注的朋友去赌场玩老虎机时也能像上面那张图一样赢大奖💰💰💰
不过小赌怡情 大赌伤身 ☠️ 珍爱生命 远离赌博 🎲
原理分析
其实这玩意的原理和轮播图非常相似:

一个合格的前端至少也要能够达到会写轮播图的水平吧!那么相信大家对轮播图的原理应该都不陌生,就是把你要轮播的图片横着排列,然后绝对定位,再定义一个代表index
的变量,点击箭头改变变量的值,再把变量映射到DOM
的style
属性上,最后再用overflow: hidden;
隐藏掉露在外面的那些图:

当然这只是个简易的轮播图,一个完整的轮播图底下应该还有一堆小圆点,一方面用来表示一共有多少张图,另一方面用来表示当前是第几张图。不过这对于我们要开发的老虎机式滚动数字来说根本用不到,所以暂时就先不写了。轮播图不是横着的吗?那我们把给它竖过来试试:

接下来再把桥本环奈
(轮播图里特别可爱的那个小姐姐的名字)的动态图替换为数字:

做过无限轮播的朋友应该知道,从最后一张到第一张或从第一张到最后一张时为了看起来像是直接滚动过去,通常会在头部加上最后一张的复制版、在尾部加入第一张的复制版,我们这个也不例外,不过由于我们不像轮播图那样左右都可以滚动,我们只是从上到下这么滚,那么我们就在下面放上第一张的数字,也就是0
。然后去掉箭头,让它自己滚:

然后再用overflow: hidden;
隐藏掉露在外边的数字:

这样看起来是不是就有点像是这种感觉啦:

不过还有一个地方不太像,那就是上图这张老虎机在滚动时自带模糊效果,会给人一种滚动速度已经快到重影了的错觉。这一下就让我想起之前产品经理让我做的:《鸿蒙那个开场动画挺帅的 给咱们页面也整一个呗》

我知道一提到模糊大家第一时间想到的肯定是:filter: blur(几px);
,这个CSS属性的特点就是会将元素进行全方位模糊。但实际上在有些场景下需要的并不是全方位模糊,而是沿着x
轴模糊或者沿着y
轴模糊,给大家看看用filter: blur();
实现出来的效果:

而沿着y
轴模糊的效果是这样的:

可以看到效果有着明显的差异,而刚好我们想要打到老虎机那种效果需要的也是沿着y
轴模糊,那我们就从那篇文章里把滤镜部分的代码复制过来应用到我们的页面上试试:

效果好像还不错!那假如要是用filter: blur();
给数字去添加模糊效果会是怎么样的一种体验呢?我们来试一下:

emmmmmmm… 像是得了老花眼…
突发奇想,既然有了这个可以控制沿x
轴还是沿着y
轴模糊的SVG
滤镜,那我们同样也可以把沿着x
轴模糊的这一效果应用到轮播图上去对不对?来试一下:

跟之前的轮播图来个对比:

怎么样?是不是在加上了这个滤镜之后轮播图就显得更加动感了呢?
动画定位
假如我们想要让数字定位到6
这个数字:

不过这个6
还带有我们添加的上下模糊效果,我们在停住时把模糊滤镜去掉再来看看:

是不是看起来好像恰巧就是滚动到6
这个位置停下来的一样啊?但实际上并不是这样,而是这样:

仔细观察的话可以发现其实并不是那串数字恰巧滚动到6
这个位置然后停住的,而是不管滚动到哪,只要是到了时间就直接定位到6
。如果看不太清楚的话我们放慢速度、给6
加入一个红色背景后再来看一眼:

由于滚动速度快,所以即便没有滚动到了第6
位数字就突然在第6
位数字停住,人眼也看不出来,反而会觉得就是滚动到了6
这个数字的面前,其实也就是障眼法
。CSS
有很多特效都是靠着类似于障眼法一样的方式去实现的,比方说无限滚动的轮播图,看起来就像是真的有无数张图片连接在一起一样。这有点类似于魔术,都是在用一些小技巧去欺骗用户的眼睛,从而达到令人称赞的效果。这也是我为什么会喜欢炫酷CSS
特效的原因,感觉自己就像是在网页里的魔术师
,为大家表演了一段魔术一样。
不过肯定有人会问,这样做有什么好处吗?为什么不做成直接滚动到对应的数字再停住啊:

首先,无论是做成这样还是做成那样,他俩最终的效果差不多,都长成这样:

除非你家产品经理要求滚动的速度像蜗牛一样慢吞吞的,否则根本就看不出来有什么区别。而另一个原因则是这样可以方便我们能够精确控制在什么时间停止滚动。比如说我们设置了在几秒钟之后停止滚动,那么到了停止滚动的这个时间时它到底滚在了第几位是不确定的对吧?假如我们想在第9
位停住,但是到时间时动画恰巧处在第1
位,那么动画还要继续进行滚动,直到第9
位时才能够停下来。假设我们原本设置的是滚动两秒钟,而从第1
位滚动到第9
位需要耗时0.8
秒钟,那么最终整个动画其实是滚动了2.8
秒才停下,与我们所设置的两秒钟明显不符。
不过聪明的同学肯定会想到:你不默认从第0
位开始滚不就得了嘛!而是根据你传入的数字来动态计算应该从第几位开始滚。比如你计划滚动2
秒钟,然后在滚动到第6
位时停住,那么只需要计算从第几位开始滚,两秒钟之后它恰巧就能滚到第6
位不就完事了嘛!
这样做确实是可行的,但这无疑会增加我们代码的复杂度,而效果却又差不多,还会浪费掉我们好几根头发去进行计算,其实我们明明有更简单的实现方式,那就是:
把动画分为两段去运行!
分段式动画
第一段动画

也就是无限滚动动画,我们会封装成组件,具体滚动多久由传入的参数决定。
第二段动画

可以看到最终我们会选择一个数字来做这样的动感回弹效果,无限滚动完就立马切换到这个动画上面去,具体是哪个数字也是由传进来的参数决定的。
连起来

组件代码
由于这个项目是用Vue2.x
来进行制作的,所以贴出来的代码也是Vue2
的风格,不过没关系,JS
部分很简单,主要代码都集中在CSS
部分了。所以大家可以很轻松的将这个组件改成符合自己项目的Vue3.x
组件或者React
组件等:
<template>
<component
:is="as"
class="scroll-num"
:class="{ 'border-animate': animate }"
:style="{ '--i': i, '--delay': delay }"
@animationend="animate = false"
>
<ul
ref="ul"
:class="{ animate }"
>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>0</li>
</ul>
<svg width="0" height="0">
<filter id="blur">
<feGaussianBlur
in="SourceGraphic"
:stdDeviation="`0 ${blur}`"
/>
</filter>
</svg>
</component>
</template>
<script>
export default {
name: 'ScrollNum',
props: {
as: {
type: String,
default: 'div'
},
i: {
type: Number,
default: 0,
validator: v => v < 10 && v >= 0 && Number.isInteger(v)
},
delay: {
type: Number,
default: 1
},
blur: {
type: Number,
default: 2
}
},
data: () => ({
timer: null,
showAnimate: true
}),
watch: { i () { this.showAnimate = true } },
mounted () {
const ua = navigator.userAgent.toLowerCase()
const testUA = regexp => regexp.test(ua)
const isSafari = testUA(/safari/g) && !testUA(/chrome/g)
// Safari浏览器的兼容代码
isSafari && (this.timer = setTimeout(() => {
this.$refs.ul.setAttribute('style', `
animation: none;
transform: translateY(calc(var(--i) * -9.09%))
`)
}, this.delay * 1000))
},
beforeDestroy () { clearTimeout(this.timer) }
}
</script>
<style scoped>
.scroll-num {
width: var(--width, 20px);
height: var(--height, calc(var(--width, 20px) * 1.8));
color: var(--color, #333);
font-size: var(--height, calc(var(--width, 20px) * 1.1));
line-height: var(--height, calc(var(--width, 20px) * 1.8));
text-align: center;
overflow: hidden;
}
.animate {
animation: move .3s linear infinite,
bounce-in-down 1s calc(var(--delay) * 1s) forwards
}
.border-animate {
animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards
}
ul {
/* padding: 0;
margin: 0;
list-style: none; */
transform: translateY(calc(var(--i) * -9.09%));
}
@keyframes move {
from {
transform: translateY(-90%);
filter: url(#blur)
}
to {
transform: translateY(1%);
filter: url(#blur)
}
}
@keyframes bounce-in-down {
from {
transform: translateY(calc(var(--i) * -9.09% - 7%));
filter: none
}
25% { transform: translateY(calc(var(--i) * -9.09% + 3%)) }
50% { transform: translateY(calc(var(--i) * -9.09% - 1%)) }
70% { transform: translateY(calc(var(--i) * -9.09% + .6%)) }
85% { transform: translateY(calc(var(--i) * -9.09% - .3%)) }
to { transform: translateY(calc(var(--i) * -9.09%)) }
}
@keyframes enhance-bounce-in-down {
25% { transform: translateY(8%) }
50% { transform: translateY(-4%) }
70% { transform: translateY(2%) }
85% { transform: translateY(-1%) }
to { transform: translateY(0) }
}
</style>
⚠️ 如果把这个组件复制到项目中去 发现样式显示不正确的话,只需要解开CSS部分ul里注释掉的样式即可。出现这种现象的原因是你没有引入
reset.css
,导致ul
标签有默认的边距、li
标签有默认的小圆点。如果有reset.css
的话,就删掉这段没用的注释。
这个组件封装的思路主要是用到了CSS变量+calc函数
来控制滚动时长,在不传--width
和--height
宽高的情况下默认会是20 * 36
,还可以只传宽不传高,利用calc函数
能保证在只传宽度的情况下,高度依然能够保持住原有的比例。我知道这时肯定会有人说:想要保持比例用aspect-ratio
就行了,何必那么麻烦呢?
首先就是这个属性比较新
,兼容性还不是特别好,虽然Edge
、火狐
和谷歌
的最新几个版本都已经支持这一属性了,但Safari
浏览器只有15-技术预览版
才支持,而在IOS
下则是完全不支持:

要知道用iPhone
的用户大多数都会选择Safari
,因为他们也不懂什么各种浏览器啥的,只知道点这个指南针🧭一样的图标是用来上网的。另一点则是我们其实并不是非要保持住这个比例,这是只是我封装组件的一个习惯。有时候懒,希望用组件时只传一个宽或者高就得了,没传的那个参数能够自动计算,所以才会封装成这个样子。你可以按照自己的喜好来,把那段代码改成你喜欢的样子。
如果不太清楚什么是CSS变量的话,可以点击这篇文章来学习一下。现在都已经2021
年了,是时候学习一下这种技术了,但如果你非要说这玩意IE
浏览器不支持:

以IE
不支持为理由拒绝学习任何新技术的话,那么很快很快,你就会比IE
淘汰的还要快。因为就连微软
和Vue3
都已经双双决定放弃掉IE
了:《尤雨溪:Vue3将不会支持IE11 精力会投入到Vue2.7》
用法
这只是一个组件,通常来说我们不会只让这么一个数字滚动,而是一串数字滚动,我们先定义一个数字886
,然后再用computed
把886
变成[8, 8, 6]
,最后再v-for
一个:
<template>
<ul class="flex">
<ScrollNum
v-for="(num, idx) of numArr"
:key="idx"
as="li"
:i="num"
:delay="idx + 1"
/>
</ul>
</template>
<script>
import ScrollNum from './components/ScrollNum.vue'
export default {
name: 'App',
components: { ScrollNum },
data: () => ({ num: 886 }),
computed: {
numArr () {
const str = String(this.num)
let arr = []
for (let i = 0; i < str.length; i++) {
arr.push(parseInt(str[i]))
}
return arr
}
},
mounted () {
setInterval(() => this.num++, 10000)
}
}
</script>
<style scoped>
.flex {
display: flex;
}
ul {
padding: 0;
margin: 0;
list-style: none;
}
</style>
一个完美的老虎机效果就这样完成啦:

如果想要调整大小的话,只需要给它一个--width
,高度和字体大小就会自动进行调整。我们还可以再加上一个边框:
<template>
<ul class="flex">
<ScrollNum
v-for="(number, idx) of numArr"
:key="idx"
:i="number"
:delay="idx + 2.5"
as="li"
class="num"
/>
</ul>
</template>
<script>
import ScrollNum from './components/ScrollNum.vue'
export default {
name: 'App',
components: { ScrollNum },
data: () => ({ num: 886 }),
computed: {
numArr () {
const str = String(this.num)
let arr = []
for (let i = 0; i < str.length; i++) {
arr.push(parseInt(str[i]))
}
return arr
}
},
mounted () {
setInterval(() => this.num++, 10000)
}
}
</script>
<style>
.flex {
display: flex;
}
ul {
padding: 0;
margin: 0;
list-style: none;
}
.num {
--width: 26px;
margin-right: 6px;
border: 1px solid black;
border-radius: 8px
}
</style>

⚠️ 如果你复制我的代码到自己项目中发现滚动无法停住的话,可能是
vue-loader
版本过低导致的编译scoped
多重动画时导致的bug
建议升级vue-cli
或者去掉<style scoped>
上的scoped
,然后给DOM
起一个不容易重名的类名或ID
结语
怎么样,是不是效果还不错呢?现在的你只需要把我的组件复制过去,就能变成自己项目中的一个炫酷小组件啦!
我开源、你开心、老板开法拉利!
- 本文首发于公众号:《前端学不动》
网友评论