惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
人人都是产品经理
人人都是产品经理
M
MIT News - Artificial intelligence
C
Cybersecurity and Infrastructure Security Agency CISA
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
爱范儿
爱范儿
Spread Privacy
Spread Privacy
P
Palo Alto Networks Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
T
The Blog of Author Tim Ferriss
The Cloudflare Blog
WordPress大学
WordPress大学
小众软件
小众软件
H
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Blog — PlanetScale
Blog — PlanetScale
V2EX - 技术
V2EX - 技术
H
Hacker News: Front Page
Last Week in AI
Last Week in AI
D
DataBreaches.Net
V
V2EX
S
Securelist
C
CXSECURITY Database RSS Feed - CXSecurity.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tenable Blog
Cloudbric
Cloudbric
月光博客
月光博客
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
N
News and Events Feed by Topic
K
Kaspersky official blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
S
Secure Thoughts
S
Security Affairs
W
WeLiveSecurity
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
AI
AI
有赞技术团队
有赞技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
罗磊的独立博客
Google DeepMind News
Google DeepMind News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
N
Netflix TechBlog - Medium
PCI Perspectives
PCI Perspectives
SecWiki News
SecWiki News
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog

吖远zzyの博客

记录一下豆包视频去水印的研究过程,以及fallback_api加密数据qAAB开头数据的解密。 | 吖远zzyの博客 解决华为开发者工具 DevEco Studio 登录跳转 localhost:10101 端口被阻止问题 | 吖远zzyの博客 微信小程序个人资质审核血泪史:踩坑,从反复被拒到神奇过审 | 吖远zzyの博客 如何实现公众号自动回复或做一个短视频无水印解析机器人? | 吖远zzyの博客 以后在手机上用 Termux 和 GitHub Actions 更新 Hexo 博客 | 吖远zzyの博客 必备!这款万能小程序能给抖音/豆包视频图集去除水印、还有有趣的测反应、玩成语模拟工具。 自制的一些免费API接口第二弹(获取QQ昵称、头像、抖音/豆包视频去水印等...) 告别原生导航栏:微信小程序自定义导航栏完美适配方案 在安卓手机上运行OpenClaw?当然可以,用Termux折腾OpenClaw安装QQ机器人的踩坑记录... | 吖远zzyの博客 个人开发工具之抖音直播录制工具:一款功能强大的Android直播录制应用 UniApp中Canvas绘图的易错点与踩坑指南 QQ频道机器人与UniApp开发:常见踩坑点与解决方案 UniApp中Canvas绘图不显示的常见问题与解决方案 Hexo博客实现随机文章功能的完整教程 QQ频道机器人Android客户端使用指南 | 吖远zzyの博客 安卓版QQ频道机器人APP客户端插件开发指南 | 吖远zzyの博客 2024年AI编程助手深度评测:哪款最适合你? uni-app地图定位踩坑记:地图功能和定位的那些坑 uni-app文件上传踩坑记:图片处理和上传全攻略 uni-app表单验证踩坑记:这些坑我替你踩过了 uni-app开发踩坑记录:新手必看的常见问题与解决方案 uni-app安全防护指南:构建可靠的跨端应用 uni-app性能优化指南:从加载到渲染的全方位提升 uni-app组件开发实战:从基础到进阶的最佳实践 uni-app网络请求与缓存策略:构建高效的数据层 uni-app状态管理进阶:Vuex最佳实践与性能优化 uni-app路由与页面跳转:最佳实践与踩坑指南
uni-app动画效果实战:从基础到高级的动画实现指南
吖远zzy · 2024-04-05 · via 吖远zzyの博客
  1. 1. 1. CSS动画基础
    1. 1.1. 1.1 过渡动画
    2. 1.2. 1.2 关键帧动画
  2. 2. 2. JS动画实现
    1. 2.1. 2.1 使用animation对象
    2. 2.2. 2.2 requestAnimationFrame实现
  3. 3. 3. 手势动画
    1. 3.1. 3.1 拖拽动画
    2. 3.2. 3.2 滑动删除
  4. 4. 4. 高级动画技巧
    1. 4.1. 4.1 路由转场动画
    2. 4.2. 4.2 列表动画
  5. 5. 5. 性能优化
  6. 6. 6. 最佳实践建议
  7. 7. 7. 总结

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. 保持代码可维护性

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