小程序中动画的几种实现方式

做小程序很久了,但是动画做的很少,一提到动画,总是就感觉无从下手,今天正好有时间就把小程序中的动画,好好整理下。以后再遇到这种需求,就不会那么手足无措了。

以下内容,我们以一个经典的动画,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需要改变的样式属性和时间,因为我们是一个上下的淡入效果,因为需要改变的是opacitytransform两个样式属性

因为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-toclass。从而实现动态的改变元素的样式。

代码里还有个需要略微说明下的地方。

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,默认值,表示动画在执行前后,不会对目标应用任何样式。这样的话动画执行后,动画的那些样式就会被移除。对应这里的就是transformopacity就会被移除。
  • 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.createAnimationthis.animate一样,适合给那种始终都在页面上的元素设置动画。不太合适给那种需要动态在页面上添加和删除的元素设置动画。

3.尾声

以上就是小程序动画常见的4种形式,各有优劣,相比较使用css的animation是最简单。如果上面有什么说的不对的,欢迎指正。

也欢迎各位能摘评论区,闲聊,一起说说话,解解闷

相关推荐
CRMEB定制开发4 小时前
CRMEB会员电商系统集群部署 + 腾讯云日志托管优化方案
微信小程序·公众号商城·商城源码·crmeb·开源商城
百万蹄蹄向前冲4 小时前
秋天的第一口代码,Trae SOLO开发体验
前端·程序员·trae
努力奋斗14 小时前
VUE-第二季-02
前端·javascript·vue.js
路由侠内网穿透4 小时前
本地部署 SQLite 数据库管理工具 SQLite Browser ( Web ) 并实现外部访问
运维·服务器·开发语言·前端·数据库·sqlite
一只韩非子5 小时前
程序员太难了!Claude 用不了?两招解决!
前端·claude·cursor
JefferyXZF5 小时前
Next.js项目结构解析:理解 App Router 架构(二)
前端·全栈·next.js
Sane5 小时前
react函数组件怎么模拟类组件生命周期?一个 useEffect 搞定
前端·javascript·react.js
gnip5 小时前
可重试接口请求
前端·javascript
若梦plus5 小时前
模块化与package.json
前端
烛阴5 小时前
Aspect Ratio -- 宽高比
前端·webgl