css揭秘 - 过渡与动画(一)

369 阅读6分钟

这是我参与更文挑战的第15天,活动详情查看: 更文挑战

缓动效果

难题

给过渡和动画加上缓动效果(比如具有回弹效果的过渡过程)是一种流行的表现手法,可以让界面显得更加生动和真实。

弹跳动画

@keyframes bounce {
  60%, 80%, to { transform: translateY(150px); }
  70% { transform: translateY(50px); }
  90% { transform: translateY(100px); }
}

.ball {
  /* 尺寸样式、颜色样式等 */
  margin-left: 200px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: red;
  animation: bounce 3s;
}

不论是在 animation/transition 简写属性中,还是在 animation-timing-function /transition-timing-function 展开式属性中,都可以将默认的调速函数显示指定为 ease 关键字。除此之外,还有四种内置的缓动曲线,可以用来改变动画的推进方式。

image.png

如图所示,ease-inease-out 是反向版本,这一对组合正好是实现回弹效果所需要的:每当小球的运动方向相反时,调速函数也是相反的。可以在 animation 属性中指定一个通用的调速函数,然后在关键帧中按需覆盖它。

@keyframes bounce1 {
  60%, 80%, to {
    transform: translateY(200px);
    animation-timing-function: ease-out;
  }
  70% { transform: translateY(150px); }
  90% { transform: translateY(180px); }
}
.ball2 {
/* 其余样式写在这里 */
  margin-left: 400px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: green;
  animation: bounce1 3s ease-in;
}

从逻辑上来讲,只要把控制锚点的水平坐标和垂直坐标互换,就可以得到任何调速函数的反向版本,这一点对关键字也是适用的;上述五个关键字都有其对应的 cubic-bezier() 形式的值。比如:ease 等同于 cubic-bezier(.25, .1, .25, 1),因此它的反向版本就是 cubic-bezier(.1, .25, 1, .25)。因此可以将回弹动画修改为:

@keyframes bounce2 {
  60%, 80%, to {
    transform: translateY(200px);
    animation-timing-function: ease;
  }
  70% { transform: translateY(150px); }
  90% { transform: translateY(180px); }
}
.ball2 {
  /* 外观样式 */
  margin-left: 100px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: blue;
  animation: bounce 3s cubic-bezier(.1,.25,1,.25);
}

上述三种代码对应的动画 221.gif

弹性过渡

有一个文本框,当它被聚焦时,都需要展示一个提示框。这个提示框用来向用户提供帮助信息,比如字段的正确格式等。结构代码为:

<style>
input {
  display: block;
  padding: 0 .4em;
  font: inherit;
}

.callout {	
  position: absolute;
  max-width: 14em;
  padding: .6em .8em;
  border-radius: .3em;
  margin: .3em 0 0 -.2em;
  background: #fed;
  border: 1px solid rgba(0,0,0,.3);
  box-shadow: .05em .2em .6em rgba(0,0,0,.2);
  font-size: 75%;
}

.callout:before {
  content: "";
  position: absolute;
  top: -.4em;
  left: 1em;
  padding: .35em;
  background: inherit;
  border: inherit;
  border-right: 0;
  border-bottom: 0;
  transform: rotate(45deg);
}

input:not(:focus) + .callout {
  transform: scale(0);
}
.callout {
  transition: .5s transform;
  transform-origin: 1.4em -.4em;
}
</style>

<input type="text" value="niuniu">
<span class="callout">只能输入字母、数字、下划线以及中划线</span>

原始的过渡效果 222.gif

可以看到,目前当用户聚焦到这个文本框时有一个1.5s的过渡,但是如果在结尾时能更夸张一点就可以更生动和自然(比如:先扩大到110%的尺寸,然后再缩回到100%),可以使用上述学会的动画。

@keyframes elastic-grow {
  from { transform: scale(0); }
  70% {
    transform: scale(1.1);
    animation-timing-function:
    cubic-bezier(.1,.25,1,.25); /* 反向的ease */
  }
}
input:not(:focus) + .callout { transform: scale(0); }
input:focus + .callout { animation: elastic-grow .5s; }
.callout { transform-origin: 1.4em -.4em; }

使用动画的效果 223.gif

跟之前的相比,确实是实现了想要的功能,但是在这个场景中,需要的仅是给过渡加入一个弹性的效果,使用动画显得有点大材小用了。

input:not(:focus) + .callout { transform: scale(0); }
.callout {
  transform-origin: 1.4em -.4em;
  transition: .5s cubic-bezier(.25,.1,.3,1.5);
}

使用过渡代码实现的效果

224.gif

但是,发现在当文本框失去焦点,提示框缩回到消失的时候并不是我们想要的效果。只想给提示框的关闭过程指定普通的 ease 调速函数,那么可以在定义关闭状态的 CSS 规则中把当前的调速函数覆盖掉:

input:not(:focus) + .callout {
  transform: scale(0);
  transition-timing-function: ease;
}
.callout {
  transform-origin: 1.4em -.4em;
  transition: .5s cubic-bezier(.25,.1,.3,1.5);
}

缩回正常的过渡

225.gif 发现关闭的时候要迟钝一些。要修复这个问题既可以单独覆盖 transition-duration 这一个属性,也可以用 transition 这个简写属性来覆盖所有的值。如果选择后者的话,就不需要显式指定 ease 了,因为它本来就是初始值:

input:not(:focus) + .callout {
  transform: scale(0);
  transition: .25s;
}
.callout {
  transform-origin: 1.4em -.4em;
  transition: .5s cubic-bezier(.25,.1,.3,1.5);
}

关闭的时候不卡顿的过渡

226.gif

为避免不小心对颜色设置了弹性过渡,可以尝试把过渡的作用范围限制在某几种特定的属性上,而不是什么都不指定。最终完美的代码为:

input:not(:focus) + .callout {
  transform: scale(0);
  transition: .25s transform;
}
.callout {
  transform-origin: 1.4em -.4em;
  transition: .5s cubic-bezier(.25,.1,.3,1.5) transform;
}

逐帧动画

难题

在很多时候,需要一个很难(或不可能)只通过某些 CSS 属性的过渡来实现的动画。比如一段卡通影片,或是一个复杂的进度指示框。在这种场景下,基于图片的逐帧动画才是完美的选择;你可能会产生这样一种疑问:“难道不能用 GIF 动画吗?”对大多数情况来说,答案是“能”,GIF 动画可以完美胜任。但是,GIF 动画也有一些短板,在某些场景下可能会让整体效果大打折扣。

  1. GIF 图片的所能使用的颜色数量被限制在 256 色。
  2. GIF 不具备 Alpha 透明的特性。
  3. 无法在 CSS 层面修改动画的某些参数,比如动画的持续时间、循环次数、是否暂停等。

解决方案

将动画中的所有帧放在一张 png 上,然后用一个元素来容纳这个加载提示,并把它的宽高设置为单帧的尺寸:

<style>
.loader {
  width: 90px;
  height: 90px;
  background: url(loading.png) 0 0;
  text-indent: 200%;
  white-space: nowrap;
  overflow: hidden;
}
</style>
<div class="loader">加载中…</div>

与贝塞尔曲线调速函数迥然不同的是, steps() 会根据你指定的步进数量,把整个动画切分为多帧,而且整个动画会在帧与帧之间硬切,不会做任何插值处理。

@keyframes loader {
  to { background-position: -1248px 0; }  /*1248px 为图片的宽度*/
}

.loader{
  animation: loader 1s infinite steps(16); /* 16 为动画的帧数 */
}

steps() 还接受可选的第二个参数,其值可以是 start 或 end(默认值)。这个参数用于指定动画在每个循环周期的什么位置发生帧的切换动作但实际上这个参数用得并不多。如果我们只需要一个单步切换效果,还可以使用 step-start 和 step-end 这样的简写属性,它们分别等同于 steps(1, start) 和 steps(1, end) 。

最后说一句

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力,多谢支持。