关于前端:产品经理能不能让这串数字滚动起来

47次阅读

共计 7852 个字符,预计需要花费 20 分钟才能阅读完成。

前言

在很多场景下咱们须要展现一串数字,这串数字能够是写死固定在页面上的,也能够是动静刷新实时申请的,还有一些是依据用户的交互产生变动的数字。之前咱们网站在数字发生变化时是用 anime.js 做的相似于这样的一种动画:

anime.js 做这种动画的其中一个毛病就是:数字两头不能像下面那张图一样有逗号。就能够简略的了解为必须是 number 类型的值,字符串 '6,000.00' 这种就不行。当然咱们能够为每一串或者每一个数字独自利用成果,只不过成果没有上图那么好罢了,大略成果相似于这样:

预计支付宝也是遇见了跟咱们同样的问题,显示在页面上的内容 看起来像是数字 但实际上确是字符串 ,只能用相似于split 一样的办法找到逗号和小数点的地位进行宰割,而后再把失去的数字字符用 parseInt 解析成数字类型而后再利用这种成果。

产品那边对这种成果始终不是很称心,终于在一次散会时:

产品组:咱们本期的次要工作是要优化交互体验,大家用过 饿死了么?他们的软件在数字这方面有这样的一种成果:

开发组:你这啥观赏程度啊?想要改成它们这个 * 样?

产品组:当然不是,只是给大家看这种成果,方才那个成果可能不太好,那咱们再给大家换一个页面看看吧:

开发组:。。。

你要是切实闲的没事干就去清扫打扫卫生吧!别想起一出是一出了,网站用户迟早让你给搞到散失没了…

产品:哎呀不是!我想要的成果没有这么难看!只是这个成果让我想起了老虎机🎰前两天还在 饿死了么 里看到过呢!等我找找… 喏!就是这个:

研发:行 晓得了 就依照这个成果做是吧?

产品:诶~先别走啊!我还没说完呢!首先我感觉这个方向不对,咱们要做成像老虎机那种从上往下滚的:

而后不能像 饿死了么 那成果一样就那么缓缓的停住…

研发:不缓缓的停住还想咋停住?快快的停住?

产品:不是这个意思,是因为缓缓停住那种成果太普通了,咱们能不能做进去更加动感一点的成果?

研发:你想怎么个动感法?

产品:就是相似于要停住然而速度太快没能刹住,过来了一点而后再弹回来,你们有什么专业名词能形容这种成果么?

研发:就是回弹成果呗?

产品:对!就是这个!

研发:能够!没问题!比你刚给咱们看的那个动画强。

动画展现

为了避免大家设想不进去具体是什么样的成果,咱们先展现一下曾经写好并且已理论利用在咱们页面上的一些组件:

下面这串数字如同有点不太吉利啊…… 连忙换一个⬇

这样是不是就很有感觉了呢?

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

集体感觉这个回弹成果不够动感不够带劲,所以用 CSS 增强了一下回弹成果,不知大家是喜爱 饿死了么 那种无回弹成果、还是喜爱 拉斯维加斯 这部老虎机的轻微回弹成果、还是喜爱 本篇文章 将要开发进去的动感回弹成果呢?

祝点赞和关注的敌人去赌场玩老虎机时也能像下面那张图一样赢大奖💰💰💰

不过小赌怡情 大赌伤身 ☠️ 珍视生命 远离赌博 🎲

原理剖析

其实这玩意的原理和轮播图十分类似:

一个合格的前端至多也要可能达到会写轮播图的程度吧!那么置信大家对轮播图的原理应该都不生疏,就是把你要轮播的图片横着排列,而后相对定位,再定义一个代表 index 的变量,点击箭头扭转变量的值,再把变量映射到 DOMstyle属性上,最初再用 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,而后再用computed886变成 [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

结语

怎么样,是不是成果还不错呢?当初的你只须要把我的组件复制过来,就能变成本人我的项目中的一个炫酷小组件啦!

我开源、你开心、老板开法拉利!

  • 本文首发于公众号:《前端学不动》

正文完
 0