优秀的动画效果可以大大提升应用的用户体验,本文将详细介绍如何在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
| 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
| 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. 性能优化
- 使用transform代替位置属性
- 使用will-change提示浏览器
- 避免同时动画过多元素
- 适当使用硬件加速
- 合理设置动画帧率
6. 最佳实践建议
- 动画要有目的性
- 遵循物理运动规律
- 注意动画时长控制
- 提供动画开关选项
- 做好性能优化
7. 总结
- 掌握动画基础知识
- 灵活运用多种实现方式
- 注意性能优化
- 提升用户体验
- 保持代码可维护性
如果觉得文章对你有帮助,欢迎点赞、评论、分享,你的支持是我继续创作的动力!