uni-app动画效果实战:从基础到高级的动画实现指南 0 次阅读

优秀的动画效果可以大大提升应用的用户体验,本文将详细介绍如何在uni-app中实现各种动画效果,助你打造出色的交互体验。

1. CSS动画基础

1.1 过渡动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<view class="container">
<view
class="box"
:class="{ active: isActive }"
@click="toggleActive"
></view>
</view>
</template>

<script>
export default {
data() {
return {
isActive: false
}
},
methods: {
toggleActive() {
this.isActive = !this.isActive
}
}
}
</script>

<style lang="scss">
.box {
width: 100rpx;
height: 100rpx;
background: #409eff;
transition: all 0.3s ease;

&.active {
transform: scale(1.5) rotate(45deg);
background: #67c23a;
}
}
</style>

1.2 关键帧动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
.loading {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

.pulse {
width: 100rpx;
height: 100rpx;
background: #409eff;
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}

@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.5);
opacity: 0.5;
}
100% {
transform: scale(1);
opacity: 1;
}
}

2. JS动画实现

2.1 使用animation对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// utils/animation.js
export function createAnimation(options = {}) {
return uni.createAnimation({
duration: options.duration || 300,
timingFunction: options.timingFunction || 'ease',
delay: options.delay || 0,
transformOrigin: options.transformOrigin || '50% 50% 0'
})
}

// 使用示例
<template>
<view>
<view :animation="animationData" class="box"></view>
<button @click="startAnimation">开始动画</button>
</view>
</template>

<script>
import { createAnimation } from '@/utils/animation'

export default {
data() {
return {
animation: null,
animationData: {}
}
},
methods: {
startAnimation() {
// 创建动画实例
const animation = createAnimation()

// 定义动画
animation
.scale(1.5)
.rotate(45)
.step()
.scale(1)
.rotate(0)
.step()

// 导出动画数据
this.animationData = animation.export()
}
}
}
</script>

2.2 requestAnimationFrame实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// utils/raf.js
let lastTime = 0
const raf = callback => {
const currTime = new Date().getTime()
const timeToCall = Math.max(0, 16 - (currTime - lastTime))
const id = setTimeout(() => {
callback(currTime + timeToCall)
}, timeToCall)
lastTime = currTime + timeToCall
return id
}

const caf = id => {
clearTimeout(id)
}

export const requestAnimationFrame = typeof window !== 'undefined'
? window.requestAnimationFrame || raf
: raf

export const cancelAnimationFrame = typeof window !== 'undefined'
? window.cancelAnimationFrame || caf
: caf

// 使用示例
export class Animator {
constructor(options = {}) {
this.duration = options.duration || 300
this.easing = options.easing || (t => t)
this.running = false
}

animate(from, to, callback) {
const startTime = Date.now()
const change = to - from

const update = () => {
const currentTime = Date.now()
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / this.duration, 1)
const value = from + change * this.easing(progress)

callback(value)

if (progress < 1 && this.running) {
requestAnimationFrame(update)
}
}

this.running = true
requestAnimationFrame(update)
}

stop() {
this.running = false
}
}

// 使用示例
const animator = new Animator({
duration: 1000,
easing: t => t * t // 缓动函数
})

animator.animate(0, 100, value => {
// 更新元素位置
element.style.transform = `translateX(${value}px)`
})

3. 手势动画

3.1 拖拽动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<template>
<view class="drag-container">
<view
class="drag-item"
:style="itemStyle"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
拖拽我
</view>
</view>
</template>

<script>
export default {
data() {
return {
startX: 0,
startY: 0,
moveX: 0,
moveY: 0,
isDragging: false
}
},
computed: {
itemStyle() {
if (!this.isDragging) {
return {
transform: `translate3d(${this.moveX}px, ${this.moveY}px, 0)`,
transition: 'transform 0.3s'
}
}
return {
transform: `translate3d(${this.moveX}px, ${this.moveY}px, 0)`
}
}
},
methods: {
handleTouchStart(e) {
const touch = e.touches[0]
this.startX = touch.clientX - this.moveX
this.startY = touch.clientY - this.moveY
this.isDragging = true
},
handleTouchMove(e) {
const touch = e.touches[0]
this.moveX = touch.clientX - this.startX
this.moveY = touch.clientY - this.startY

// 防止页面滚动
e.preventDefault()
},
handleTouchEnd() {
this.isDragging = false

// 判断是否超出边界,如果是则回弹
const maxX = this.containerWidth - this.itemWidth
const maxY = this.containerHeight - this.itemHeight

if (this.moveX < 0) this.moveX = 0
if (this.moveY < 0) this.moveY = 0
if (this.moveX > maxX) this.moveX = maxX
if (this.moveY > maxY) this.moveY = maxY
}
}
}
</script>

<style lang="scss">
.drag-container {
position: relative;
width: 100%;
height: 500rpx;
background: #f5f5f5;
}

.drag-item {
position: absolute;
width: 200rpx;
height: 200rpx;
background: #409eff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
</style>

3.2 滑动删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<template>
<view class="swipe-container">
<view
class="swipe-item"
:style="itemStyle"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<view class="swipe-content">
列表项内容
</view>
<view class="swipe-actions">
<view class="swipe-btn delete" @click="handleDelete">删除</view>
</view>
</view>
</view>
</template>

<script>
const THRESHOLD = 80 // 滑动阈值

export default {
data() {
return {
startX: 0,
moveX: 0,
isMoving: false
}
},
computed: {
itemStyle() {
return {
transform: `translate3d(${this.moveX}px, 0, 0)`,
transition: this.isMoving ? '' : 'transform 0.3s'
}
}
},
methods: {
handleTouchStart(e) {
this.startX = e.touches[0].clientX - this.moveX
this.isMoving = true
},
handleTouchMove(e) {
const moveX = e.touches[0].clientX - this.startX
// 限制只能向左滑动
this.moveX = Math.min(0, moveX)
},
handleTouchEnd() {
this.isMoving = false
// 判断是否超过阈值
if (Math.abs(this.moveX) > THRESHOLD) {
this.moveX = -100 // 展开删除按钮
} else {
this.moveX = 0 // 回弹
}
},
handleDelete() {
// 删除逻辑
this.$emit('delete')
}
}
}
</script>

<style lang="scss">
.swipe-item {
position: relative;
width: 100%;
height: 100rpx;
background: #fff;

.swipe-content {
height: 100%;
padding: 0 30rpx;
display: flex;
align-items: center;
}

.swipe-actions {
position: absolute;
top: 0;
right: 0;
height: 100%;
display: flex;
transform: translateX(100%);
}

.swipe-btn {
width: 100rpx;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;

&.delete {
background: #f56c6c;
}
}
}
</style>

4. 高级动画技巧

4.1 路由转场动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<template>
<view class="page-container">
<view
class="page-wrapper"
:class="transitionClass"
:style="{ 'z-index': zIndex }"
>
<slot></slot>
</view>
</view>
</template>

<script>
export default {
props: {
name: {
type: String,
default: 'fade'
}
},
data() {
return {
transitionClass: '',
zIndex: 1
}
},
methods: {
enter() {
this.zIndex = 2
this.transitionClass = `${this.name}-enter`
requestAnimationFrame(() => {
this.transitionClass = `${this.name}-enter ${this.name}-enter-active`
})
},
leave() {
this.zIndex = 1
this.transitionClass = `${this.name}-leave`
requestAnimationFrame(() => {
this.transitionClass = `${this.name}-leave ${this.name}-leave-active`
})
}
}
}
</script>

<style lang="scss">
.page-container {
position: relative;
width: 100%;
height: 100%;
}

.page-wrapper {
position: absolute;
width: 100%;
height: 100%;
transition: all 0.3s;
}

// 淡入淡出
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
}
.fade-leave {
opacity: 1;
}
.fade-leave-active {
opacity: 0;
}

// 滑动
.slide-enter {
transform: translateX(100%);
}
.slide-enter-active {
transform: translateX(0);
}
.slide-leave {
transform: translateX(0);
}
.slide-leave-active {
transform: translateX(-100%);
}
</style>

4.2 列表动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<template>
<view class="list-container">
<view
v-for="(item, index) in list"
:key="item.id"
class="list-item"
:style="getItemStyle(index)"
>
{{ item.text }}
</view>
</view>
</template>

<script>
export default {
data() {
return {
list: []
}
},
methods: {
getItemStyle(index) {
return {
animation: `slideIn 0.3s ease-out ${index * 0.1}s both`
}
},
addItem() {
this.list.push({
id: Date.now(),
text: '新项目'
})
},
removeItem(index) {
this.list.splice(index, 1)
}
}
}
</script>

<style lang="scss">
.list-item {
height: 100rpx;
margin: 20rpx;
background: #fff;
border-radius: 8rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}

@keyframes slideIn {
from {
opacity: 0;
transform: translateY(60rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

5. 性能优化

  1. 使用transform代替位置属性
  2. 使用will-change提示浏览器
  3. 避免同时动画过多元素
  4. 适当使用硬件加速
  5. 合理设置动画帧率

6. 最佳实践建议

  1. 动画要有目的性
  2. 遵循物理运动规律
  3. 注意动画时长控制
  4. 提供动画开关选项
  5. 做好性能优化

7. 总结

  1. 掌握动画基础知识
  2. 灵活运用多种实现方式
  3. 注意性能优化
  4. 提升用户体验
  5. 保持代码可维护性

如果觉得文章对你有帮助,欢迎点赞、评论、分享,你的支持是我继续创作的动力!

上一篇 uni-app性能优化指南:从加载到渲染的全方位提升
下一篇 uni-app组件开发实战:从基础到进阶的最佳实践
感谢您的支持!
微信赞赏码 微信赞赏
支付宝赞赏码 支付宝赞赏