UniApp页面切换动效实战:打造流畅精致的转场体验
引言
在移动应用开发中,页面切换动效不仅能提升用户体验,还能传达应用的品质感。随着HarmonyOS的普及,用户对应用的动效体验要求越来越高。本文将深入探讨如何在UniApp中实现流畅精致的页面切换动效,并重点关注HarmonyOS平台的适配优化。
技术方案设计
1. 实现思路
页面切换动效主要包含以下几个关键点:
- 路由切换监听
- 动画状态管理
- 过渡效果实现
- 性能优化处理
2. 技术选型
- 动画实现:CSS3 + Animation API
- 状态管理:Pinia
- 路由管理:uni-router
- 性能优化:requestAnimationFrame + CSS硬件加速
核心实现
1. 页面切换容器组件
vue
<!-- components/PageTransition.vue -->
<template>
<view class="page-transition">
<view
class="page-container"
:class="[
transitionName,
{ 'is-switching': isSwitching }
]"
:style="containerStyle"
>
<slot></slot>
</view>
<!-- 过渡遮罩层 -->
<view
v-if="showMask"
class="transition-mask"
:style="maskStyle"
></view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useTransitionStore } from '@/stores/transition'
import { createAnimation } from '@/utils/animation'
import type { TransitionMode } from '@/types'
// 组件属性定义
const props = withDefaults(defineProps<{
mode?: TransitionMode
duration?: number
timing?: string
direction?: 'forward' | 'backward'
}>(), {
mode: 'slide',
duration: 300,
timing: 'ease-in-out',
direction: 'forward'
})
// 状态管理
const router = useRouter()
const route = useRoute()
const transitionStore = useTransitionStore()
// 响应式数据
const isSwitching = ref(false)
const showMask = ref(false)
const animation = ref(null)
// 计算属性
const transitionName = computed(() => {
const { mode, direction } = props
return `transition-${mode}-${direction}`
})
const containerStyle = computed(() => ({
'--transition-duration': `${props.duration}ms`,
'--transition-timing': props.timing
}))
const maskStyle = computed(() => ({
'--mask-opacity': transitionStore.maskOpacity
}))
// 监听路由变化
watch(() => route.path, async (newPath, oldPath) => {
if (newPath === oldPath) return
await startTransition()
})
// 初始化动画实例
const initAnimation = () => {
animation.value = createAnimation({
duration: props.duration,
timingFunction: props.timing
})
}
// 开始过渡动画
const startTransition = async () => {
isSwitching.value = true
showMask.value = true
// 根据不同模式执行对应动画
switch (props.mode) {
case 'slide':
await executeSlideAnimation()
break
case 'fade':
await executeFadeAnimation()
break
case 'zoom':
await executeZoomAnimation()
break
case 'custom':
await executeCustomAnimation()
break
}
// 结束过渡
await finishTransition()
}
// 滑动动画实现
const executeSlideAnimation = async () => {
const isForward = props.direction === 'forward'
const startX = isForward ? '100%' : '-100%'
const endX = '0%'
animation.value
.translateX(startX)
.step()
.translateX(endX)
.step()
return new Promise(resolve => {
setTimeout(resolve, props.duration)
})
}
// 淡入淡出动画实现
const executeFadeAnimation = async () => {
animation.value
.opacity(0)
.step()
.opacity(1)
.step()
return new Promise(resolve => {
setTimeout(resolve, props.duration)
})
}
// 缩放动画实现
const executeZoomAnimation = async () => {
const isForward = props.direction === 'forward'
const startScale = isForward ? 0.8 : 1.2
animation.value
.scale(startScale)
.opacity(0)
.step()
.scale(1)
.opacity(1)
.step()
return new Promise(resolve => {
setTimeout(resolve, props.duration)
})
}
// 自定义动画实现
const executeCustomAnimation = async () => {
// 执行自定义动画逻辑
emit('before-transition')
await transitionStore.executeCustomTransition()
emit('after-transition')
}
// 完成过渡
const finishTransition = async () => {
isSwitching.value = false
showMask.value = false
// 重置动画状态
animation.value.reset()
// 触发完成事件
emit('transition-end')
}
// 事件声明
const emit = defineEmits<{
(e: 'before-transition'): void
(e: 'after-transition'): void
(e: 'transition-end'): void
}>()
// 组件初始化
onMounted(() => {
initAnimation()
})
</script>
<style lang="scss">
.page-transition {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
.page-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--page-bg, #fff);
transition: transform var(--transition-duration) var(--transition-timing),
opacity var(--transition-duration) var(--transition-timing);
&.is-switching {
pointer-events: none;
}
// 滑动过渡
&.transition-slide-forward {
transform: translateX(100%);
&.is-switching {
transform: translateX(0);
}
}
&.transition-slide-backward {
transform: translateX(-100%);
&.is-switching {
transform: translateX(0);
}
}
// 淡入淡出过渡
&.transition-fade-forward,
&.transition-fade-backward {
opacity: 0;
&.is-switching {
opacity: 1;
}
}
// 缩放过渡
&.transition-zoom-forward {
transform: scale(0.8);
opacity: 0;
&.is-switching {
transform: scale(1);
opacity: 1;
}
}
&.transition-zoom-backward {
transform: scale(1.2);
opacity: 0;
&.is-switching {
transform: scale(1);
opacity: 1;
}
}
}
.transition-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, var(--mask-opacity, 0.3));
z-index: 9999;
transition: opacity var(--transition-duration) var(--transition-timing);
}
}
// 深色模式适配
@media (prefers-color-scheme: dark) {
.page-transition {
.page-container {
--page-bg: #121212;
}
}
}
</style>
2. 路由配置与动画管理
typescript
// router/index.ts
import { createRouter } from '@/uni-router'
import type { TransitionMode } from '@/types'
interface RouteConfig {
path: string
name: string
component: any
meta?: {
transition?: {
mode?: TransitionMode
duration?: number
direction?: 'forward' | 'backward'
}
}
}
const routes: RouteConfig[] = [
{
path: '/pages/index/index',
name: 'Home',
component: () => import('@/pages/index/index.vue'),
meta: {
transition: {
mode: 'slide',
duration: 300
}
}
},
{
path: '/pages/detail/detail',
name: 'Detail',
component: () => import('@/pages/detail/detail.vue'),
meta: {
transition: {
mode: 'zoom',
duration: 400
}
}
}
]
const router = createRouter({
routes
})
// 路由守卫中处理转场动画
router.beforeEach((to, from, next) => {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
// 根据路由深度判断前进后退
const direction = toDepth >= fromDepth ? 'forward' : 'backward'
// 设置转场动画配置
to.meta.transition = {
...to.meta.transition,
direction
}
next()
})
export default router
3. 状态管理
typescript
// stores/transition.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useTransitionStore = defineStore('transition', () => {
// 状态定义
const isTransitioning = ref(false)
const maskOpacity = ref(0.3)
const currentTransition = ref<TransitionMode>('slide')
// 动画控制方法
const startTransition = (mode: TransitionMode) => {
isTransitioning.value = true
currentTransition.value = mode
}
const endTransition = () => {
isTransitioning.value = false
}
const setMaskOpacity = (opacity: number) => {
maskOpacity.value = opacity
}
// 自定义转场动画
const executeCustomTransition = async () => {
// 实现自定义转场逻辑
return new Promise(resolve => {
setTimeout(resolve, 300)
})
}
return {
isTransitioning,
maskOpacity,
currentTransition,
startTransition,
endTransition,
setMaskOpacity,
executeCustomTransition
}
})
HarmonyOS平台优化
1. 性能优化
-
动画性能
typescript// utils/performance.ts export const optimizeAnimation = (element: HTMLElement) => { // 开启硬件加速 element.style.transform = 'translateZ(0)' element.style.backfaceVisibility = 'hidden' // 使用will-change提示 element.style.willChange = 'transform, opacity' } export const cleanupAnimation = (element: HTMLElement) => { element.style.transform = '' element.style.backfaceVisibility = '' element.style.willChange = '' }
-
内存管理
typescript// utils/memory.ts export class MemoryManager { private static instance: MemoryManager private cache: Map<string, any> private constructor() { this.cache = new Map() } static getInstance() { if (!MemoryManager.instance) { MemoryManager.instance = new MemoryManager() } return MemoryManager.instance } cacheAnimation(key: string, animation: any) { this.cache.set(key, animation) } getAnimation(key: string) { return this.cache.get(key) } clearCache() { this.cache.clear() } }
2. 动效适配
-
动画曲线
typescript// constants/animation.ts export const HarmonyOSCurves = { EASE_OUT: 'cubic-bezier(0.33, 0, 0.67, 1)', EASE_IN: 'cubic-bezier(0.33, 0, 1, 1)', STANDARD: 'cubic-bezier(0.2, 0.4, 0.8, 0.9)' }
-
手势适配
typescript// mixins/gesture.ts export const useGesture = () => { const handleGesture = (event: TouchEvent) => { // 处理手势逻辑 const touch = event.touches[0] const startX = touch.clientX const startY = touch.clientY // 判断手势方向 const direction = getGestureDirection(startX, startY) return { direction, distance: Math.sqrt(startX * startX + startY * startY) } } return { handleGesture } }
最佳实践建议
-
动效设计
- 遵循自然运动规律
- 保持动画时长适中
- 避免过度动画
-
性能优化
- 使用CSS3硬件加速
- 避免重绘和回流
- 合理使用动画缓存
-
用户体验
- 提供动画开关选项
- 支持手势返回
- 保持动效一致性
总结
通过本文的实践,我们实现了一个功能完备、性能优异的页面切换动效系统。该方案具有以下特点:
- 丰富的动效类型
- 流畅的过渡体验
- 优秀的性能表现
- 完善的平台适配
- 良好的可扩展性
希望本文的内容能够帮助开发者在UniApp项目中实现更加精致的页面切换效果,同时为HarmonyOS平台的应用开发提供参考。
参考资源
- UniApp官方文档
- HarmonyOS动效设计规范
- 前端动画性能优化指南
- 移动端手势交互设计