做小程序很久了,但是动画做的很少,一提到动画,总是就感觉无从下手,今天正好有时间就把小程序中的动画,好好整理下。以后再遇到这种需求,就不会那么手足无措了。
以下内容,我们以一个经典的动画,fade-up为例进行说明。
基本代码如下
index.wxml代码
html
<view class="page-main">
<view class="title">小程序中的动画测试</view>
<view class="btn-group mt16">
<van-button type="default" bindtap="toggle">显示/隐藏</van-button>
</view>
<view class="box">
向上淡入动画测试
</view>
</view>
index.scss 代码
css
.page-main{
position: relative;
width: 100%;
padding:32rpx;
box-sizing: border-box;
.box{
position: fixed;
left:0rpx;
bottom:0rpx;
width: 100%;
height: 320rpx;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
box-shadow: 0px -2px 10px rgba(0,0,0,0.1);
padding:32rpx;
box-sizing: border-box;
text-align: center;
border:1rpx solid red
}
}
页面效果如下

我们要做的,就是让底部的box,实现淡入的效果。
1.css动画
1.1 transiton 动画
transiton是依靠在不同的时期给元素添加不同的样式,来实现动画效果的。因为我们需要对代码进行改造。
首先我们要定义动画所涉及的样式,结果如下
css
.fade-up{
transition-property: opacity, transform;
transition-duration: 500ms;
}
.fade-up-enter{
transform: translateY(100%);
opacity: 0;
}
.fade-up-enter-to{
transform: translateY(0);
opacity: 1;
}
.fade-up-leave{
transform: translateY(0);
opacity: 1;
}
.fade-up-leave-to{
transform: translateY(100%);
opacity: 0;
}
其中fade-up
,定义了transition需要改变的样式属性和时间,因为我们是一个上下的淡入效果,因为需要改变的是opacity
和transform
两个样式属性
因为transition是依靠动态改变元素的样式来实现动画效果的,因此我们需要定义下动画开始的样式和动画结束时的样式。
以向上淡入为例
fade-up-enter
就是元素开始时的样式,在动画开始前,我们首先将先元素向下移动100%(translateY(100%)),然后把opcity改为0。
fade-up-enter-to
就是元素结束动画时的样式,此时元素恢复到原先的位置(translateY(0%)),并且透明度变为1.
当元素样式由fade-up-enter
改为fade-up-enter-to
时。浏览器就会调用transition,将translateY
由100% 改为 0,将opacity由0 改为1。从而实现一个向上淡入的效果。
我们要做的就是在动画刚开始的时候给元素添加fade-up-enter
样式,初始化完样式后,再移除它,然后再给元素添加fade-up-enter-to
样式。这样就可以实现向上淡入的动画效果了。
向下淡出的效果,与此类似,只是样式相反而已。
完整代码如下
index.wxml
html
<view class="page-main">
<view class="title">小程序中的动画测试</view>
<view class="btn-group mt16">
<van-button type="default" bindtap="toggle">显示/隐藏</van-button>
</view>
<view class="box fade-up {{classes}}" wx:if="{{inited}}">
向上淡入动画测试
</view>
</view>
其中classes
是动态的,在不同的时期,我们通过js改变它的值,从而给元素添加不同的样式,实现不同的动画。
index.scss
css
.fade-up{
transition-property: opacity, transform;
transition-duration: 500ms;
}
.fade-up-enter{
transform: translateY(100%);
opacity: 0;
}
.fade-up-enter-to{
transform: translateY(0);
opacity: 1;
}
.fade-up-leave{
transform: translateY(0);
opacity: 1;
}
.fade-up-leave-to{
transform: translateY(100%);
opacity: 0;
}
.page-main{
position: relative;
width: 100%;
padding:32rpx;
box-sizing: border-box;
.box{
position: fixed;
left:0rpx;
bottom:0rpx;
width: 100%;
height: 320rpx;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
box-shadow: 0px -2px 10px rgba(0,0,0,0.1);
padding:32rpx;
box-sizing: border-box;
text-align: center;
border:1rpx solid red
}
}
index.ts
js
//Page Object
const getClassNames = (name: string) => ({
enter: `${name}-enter`,
'enter-to': `${name}-enter-to`,
leave: `${name}-leave`,
'leave-to': `${name}-leave-to`,
});
Page({
data: {
inited:false,
show:false,
name: 'fade',
duration: 500,
classes: ''
},
//options(Object)
onLoad: function(options){
},
toggle(){
let oldValue = this.data.show
let newValue = !oldValue
this.setData({
show:newValue
},()=>{
this.observeShow(newValue,oldValue)
})
},
observeShow(value: boolean, old: boolean) {
if (value === old) {
return;
}
value ? this.enureEnter() : this.enureLeave();
},
enureEnter(){
let classNames = getClassNames('fade-up');
this.setData({
inited: true,
classes: classNames.enter,
})
// beforeEnter
setTimeout(() => {
// enter
this.setData({
classes: classNames['enter-to'],
})
},50)
},
enureLeave(){
let classNames = getClassNames('fade-up');
this.setData({
classes: classNames.leave,
})
setTimeout(() => {
this.setData({
classes: classNames['leave-to'],
})
setTimeout(() => {
this.setData({
inited: false,
})
}, this.data.duration)
},50)
},
onTransitionEnd(){
console.log('动画结束')
}
});
点击页面上的按钮会调用toggle
函数,改变show
的值,当show
为true的时候,我们就调用enureEnter
函数,这个函数的功能是动态的改变classes的值,先把classes属性赋值为fade-up-enter
,然后赋值为fade-up-enter-to
class。从而实现动态的改变元素的样式。
代码里还有个需要略微说明下的地方。
view的显示隐藏我们使用了一个新的属性inited
而不是直接使用show
。原因是如果使用show的元,当show为false的时候,元素直接就没了,就看不到动画效果了。所以这里使用了一个新变量inited
。当动画结束的时候,才隐藏元素。
另外还需要说明的一点的是,这里没有考虑用户频繁点击的情况。如果用户频繁点击,可能动画就会出错,生产环境还是要注意下。
1.2. animation 动画
animation比transition动画要简单很多。首先我们定义两个动画序列和调用该序列的动画样式
css
@keyframes fade-up-keyframes {
0% {
transform: translateY(100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes fade-down-keyframes {
0% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(100%);
opacity: 0;
}
}
.animate-fade-up{
animation: fade-up-keyframes 1s;
animation-fill-mode: forwards;
}
.animate-fade-down{
animation: fade-down-keyframes 1s;
animation-fill-mode: forwards;
}
其中animate-fade-up
就是实现想上的淡入动画,animate-fade-down
就是实现向下的淡出动画。请注意其中的animation-fill-mode
,我们定义的值是forwards
而不是默认值none
。这里赋值为both
也可以 如果没有这个的话,动画就会出错。
这里解释下
animation-fill-mode是CSS中的一个属性,它决定了动画在执行前后如何对目标元素应用样式。这个属性主要用于控制动画完成后元素的状态,是否保留动画中的某些样式
- none,默认值,表示动画在执行前后,不会对目标应用任何样式。这样的话动画执行后,动画的那些样式就会被移除。对应这里的就是
transform
和opacity
就会被移除。 - forwards,动画完成后,目标元素将保留由动画中最后一个关键帧计算出的样式值。也就是动画序列100%时的样式。
- backwards,动画将在应用于目标元素时立即应用第一个关键帧中定义的值,并在动画延迟期间保留此值。也就是动画还没开始的时候,先给元素应用动画序列中0%的样式。
- both,同时遵循forwards和backwards的规则
index.wxml代码如下
html
<view class="page-main">
<view class="title">小程序中的动画测试</view>
<view class="btn-group mt16">
<van-button type="default" bindtap="toggle">显示/隐藏</van-button>
</view>
<view class="box {{classes}}" wx:if="{{inited}}" bind:animationend="onAnimationEnd">
向上淡入动画测试
</view>
</view>
这里我们定义了animationend
事件,用于在动画结束时,将inited
改为false,从而隐藏目标元素。
index.ts代码如下
js
//Page Object
const getClassNames = (name: string) => ({
enter: `${name}-enter`,
'enter-to': `${name}-enter-to`,
leave: `${name}-leave`,
'leave-to': `${name}-leave-to`,
});
Page({
data: {
inited:false,
show:false,
name: 'fade',
duration: 500,
classes: '',
status:''
},
//options(Object)
onLoad: function(options){
},
toggle(){
let oldValue = this.data.show
let newValue = !oldValue
this.setData({
show:newValue
},()=>{
this.observeShow(newValue,oldValue)
})
},
observeShow(value: boolean, old: boolean) {
if (value === old) {
return;
}
value ? this.fadeUp() : this.fadeDown();
},
fadeUp(){
this.setData({
inited:true,
classes: 'animate-fade-up',
status:'fadeUp'
})
},
fadeDown(){
this.setData({
classes: 'animate-fade-down',
status:'fadeDown'
})
},
onAnimationEnd(){
console.log('onAnimationEnd 动画结束')
if(this.data.status === 'fadeDown'){
this.setData({
inited:false
})
}
},
enureEnter(){
let classNames = getClassNames('fade-up');
this.setData({
inited: true,
classes: classNames.enter,
})
// beforeEnter
setTimeout(() => {
// enter
this.setData({
classes: classNames['enter-to'],
})
},50)
},
enureLeave(){
let classNames = getClassNames('fade-up');
this.setData({
classes: classNames.leave,
})
setTimeout(() => {
this.setData({
classes: classNames['leave-to'],
})
setTimeout(() => {
this.setData({
inited: false,
})
}, this.data.duration)
},50)
},
onTransitionEnd(){
console.log('动画结束')
}
});
我们新增了fadeUp
,fadeDown
,onAnimationEnd
三个函数。代码很简单,这里就不过多解释了。
2.js动画
2.1 关键帧动画 this.animate
教程地址:小程序框架 / 视图层 / 动画
语法格式如下
js
this.animate(selector, keyframes, duration, callback)
index.wxml代码
html
<view class="page-main">
<view class="title">小程序中的动画测试</view>
<view class="btn-group mt16">
<van-button type="default" bindtap="toggle">显示/隐藏</van-button>
</view>
<view class="box {{classes}}" wx:if="{{inited}}" id="box">
向上淡入动画测试
</view>
</view>
跟之前代码不同的地方,就是我们给box添加了一个id。方便程序获取id后,执行动画
index.js代码
js
//Page Object
Page({
data: {
inited:false,
show:false,
duration: 500,
status:''
},
//options(Object)
onLoad: function(options){
},
toggle(){
let oldValue = this.data.show
let newValue = !oldValue
this.setData({
show:newValue
},()=>{
this.observeShow(newValue,oldValue)
})
},
observeShow(value: boolean, old: boolean) {
if (value === old) {
return;
}
value ? this.fadeUp() : this.fadeDown();
},
fadeUp(){
this.setData({
inited:true
})
this.animate("#box",[
{opacity: 0,translateY: 100},
{opacity: 1,translateY: 0}
],1000,function(){
console.log('动画运动完成')
})
},
fadeDown(){
console.log('fadeDown')
this.animate("#box",[
{opacity: 1,translateY: 0},
{opacity: 0,translateY: 100}
],1000,()=>{
console.log('动画运动完成')
this.setData({
inited:false
})
})
},
});
核心代码就是我们通过this.animate
执行一些关键帧,从而实现动画效果。
index.css 代码
css
.page-main{
position: relative;
width: 100%;
padding:32rpx;
box-sizing: border-box;
.box{
position: fixed;
left:0rpx;
bottom:0rpx;
width: 100%;
height: 320rpx;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
box-shadow: 0px -2px 10px rgba(0,0,0,0.1);
padding:32rpx;
box-sizing: border-box;
text-align: center;
border:1rpx solid red;
opacity: 0;
transform: translateY(100%);
}
}
css跟之前的代码有很大的不同,多了以下两个样式。
css
opacity: 0;
transform: translateY(100%);
原因是,使用this.animate
前,必须先把inited
设置为true,确保页面上有这个元素。但是这样的话,页面上就会先出现这个元素,然后再执行动画。元素就会在页面上闪一下。
使用transition和animation就没这个问题,因为它们会在inited为true的时候,立即给元素初始样式(transform:translateY(100);opacity:0),这样元素就不会闪。
而this.animate
是js动画,并不会立即给元素初始样式,所以会有闪一下的情况。因此this.animate
更适用于给页面上已有的元素设置样式,不太适合给新增的元素设置样式。
调用this.animate
,动画执行完毕后,会给元素添加最后一个关键帧的样式。也就是动画结束时元素是什么样式,它就会一直保持那个样式。如果要删除这些样式,让元素恢复到初始状态。可以调用下面这个函数。
js
this.clearAnimation(selector, options, callback)
2.2 wx.createAnimation
index.wxml 代码
html
<view class="page-main">
<view class="title">小程序中的动画测试</view>
<view class="btn-group mt16">
<van-button type="default" bindtap="toggle">显示/隐藏</van-button>
</view>
<view
class="box {{classes}}"
wx:if="{{inited}}"
animation="{{animationData}}"
>
向上淡入动画测试
</view>
</view>
跟之前代码的区别就是,给需要动画的view元素添加了一个animation属性.
index.scss。这个跟之前一样,没有什么变化,也需要给元素一个初始样式 transform:translateY(100%);opacity:0
css
.page-main{
position: relative;
width: 100%;
padding:32rpx;
box-sizing: border-box;
.box{
position: fixed;
left:0rpx;
bottom:0rpx;
width: 100%;
height: 320rpx;
border-top-left-radius: 32rpx;
border-top-right-radius: 32rpx;
box-shadow: 0px -2px 10px rgba(0,0,0,0.1);
padding:32rpx;
box-sizing: border-box;
text-align: center;
border:1rpx solid red;
opacity: 0;
transform: translateY(100%);
}
}
index.js 代码
typescript
//Page Object
Page({
data: {
inited:false,
show:false,
duration: 500,
status:'',
animationData: {}
},
//options(Object)
onLoad: function(){
},
toggle(){
let oldValue = this.data.show
let newValue = !oldValue
this.setData({
show:newValue
},()=>{
this.observeShow(newValue,oldValue)
})
},
observeShow(value: boolean, old: boolean) {
if (value === old) {
return;
}
value ? this.fadeUp() : this.fadeDown();
},
fadeUp(){
this.setData({
inited:true
})
const animation = wx.createAnimation({
duration: 1000,
timingFunction: 'ease',
})
animation.opacity(1).translateY(0).step()
this.setData({
animationData: animation.export()
})
},
fadeDown(){
console.log('fadeDown')
const animation = wx.createAnimation({
duration: 1000,
timingFunction: 'ease',
})
animation.opacity(0).translateY(100).step()
this.setData({
animationData: animation.export()
})
setTimeout(() => {
this.setData({
inited:false
})
},1000)
}
});
跟this.animate
比起来,animation有一点不足,就是动画运行完成后没有回调函数。还得使用setTimeout
修改动画结束时的状态。
我本来以为可以给元素绑定animationend
事件,在动画运动结束后,可以响应这个事件。结果这个事件没有响应。也就是使用wx.createAnimation
并没有回调函数。我们无法准确的知道动画运行的状态。
wx.createAnimation
跟this.animate
一样,适合给那种始终都在页面上的元素设置动画。不太合适给那种需要动态在页面上添加和删除的元素设置动画。
3.尾声
以上就是小程序动画常见的4种形式,各有优劣,相比较使用css的animation是最简单。如果上面有什么说的不对的,欢迎指正。
也欢迎各位能摘评论区,闲聊,一起说说话,解解闷