Flutter+HarmonyOS跨端实战—第08篇:启动页与引导页动画实现
·
打造流畅的应用启动体验
前言
启动页和引导页是用户接触应用的第一印象。一个精心设计的启动动画能提升应用的品质感,而引导页则能帮助新用户快速了解应用功能。
CleanMark AI 的启动页采用渐变背景 + Logo 缩放动画,引导页使用 PageView 实现滑动切换。这篇文章我会带你实现完整的启动流程。
一、启动流程设计
1.1 启动流程图
应用启动
↓
启动页 (2秒动画)
↓
判断是否首次启动
↓
是 → 引导页 (3页滑动)
↓
否 → 判断登录状态
↓
已登录 → 首页
未登录 → 登录页
1.2 状态管理
使用 SharedPreferences 记录启动状态:
// lib/core/utils/app_prefs.dart
class AppPrefs {
static const String _keyOnboardingDone = 'onboarding_done';
static const String _keyAuthToken = 'auth_token';
/// 引导页是否已完成
static Future<bool> isOnboardingDone() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keyOnboardingDone) ?? false;
}
/// 标记引导页已完成
static Future<void> setOnboardingDone() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyOnboardingDone, true);
}
/// 是否已登录
static Future<bool> isLoggedIn() async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString(_keyAuthToken);
return token != null && token.isNotEmpty;
}
}
二、启动页实现
2.1 启动页 UI
// lib/features/splash/splash_page.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../app/app_colors.dart';
import '../../core/utils/app_prefs.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
void initState() {
super.initState();
_initAnimation();
_navigateToNext();
}
void _initAnimation() {
_controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
// Logo 缩放动画 (0.5 → 1.0)
_scaleAnimation = Tween<double>(
begin: 0.5,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOutBack,
));
// 渐显动画 (0.0 → 1.0)
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
));
_controller.forward();
}
Future<void> _navigateToNext() async {
// 等待动画完成
await Future.delayed(const Duration(milliseconds: 2000));
if (!mounted) return;
// 判断启动流程
final onboardingDone = await AppPrefs.isOnboardingDone();
if (!onboardingDone) {
// 首次启动,进入引导页
context.go('/onboarding');
} else {
// 非首次启动,判断登录状态
final isLoggedIn = await AppPrefs.isLoggedIn();
if (isLoggedIn) {
context.go('/home');
} else {
context.go('/login');
}
}
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF1e0b4a),
AppColors.black,
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo 动画
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _fadeAnimation.value,
child: child,
),
);
},
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColors.purple,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: AppColors.purple.withOpacity(0.5),
blurRadius: 30,
spreadRadius: 5,
),
],
),
child: const Icon(
Icons.auto_fix_high,
size: 60,
color: Colors.white,
),
),
),
const SizedBox(height: 24),
// 应用名称
FadeTransition(
opacity: _fadeAnimation,
child: const Text(
'CleanMark AI',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 8),
// 副标题
FadeTransition(
opacity: _fadeAnimation,
child: Text(
'AI 智能去水印',
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 16,
),
),
),
],
),
),
),
);
}
}
设计要点:
- 渐变背景: 深紫色到黑色的渐变,营造科技感
- Logo 动画: 缩放 + 渐显,使用
easeOutBack曲线产生弹性效果 - 动画时长: 2秒,既不会太快也不会让用户等待太久
- 自动跳转: 动画结束后根据状态自动跳转
本篇小结(第一部分)
这部分我们完成了:
- ✅ 启动流程设计
- ✅ 状态管理(SharedPreferences)
- ✅ 启动页 UI 和动画实现
下一部分我会继续讲解引导页的实现。
三、引导页实现
3.1 引导页数据模型
// lib/features/onboarding/models/onboarding_item.dart
class OnboardingItem {
final String title;
final String description;
final IconData icon;
final Color color;
const OnboardingItem({
required this.title,
required this.description,
required this.icon,
required this.color,
});
}
// 引导页数据
final List<OnboardingItem> onboardingItems = [
OnboardingItem(
title: 'AI 智能识别',
description: '先进的 AI 算法自动识别图片和视频中的水印位置',
icon: Icons.auto_awesome,
color: AppColors.purple,
),
OnboardingItem(
title: '一键去除',
description: '只需上传文件,AI 自动处理,快速去除水印',
icon: Icons.touch_app,
color: AppColors.orange,
),
OnboardingItem(
title: '高质量输出',
description: '保持原始画质,无损去除水印,效果自然',
icon: Icons.high_quality,
color: AppColors.green,
),
];
3.2 引导页 UI
// lib/features/onboarding/onboarding_page.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../app/app_colors.dart';
import '../../core/utils/app_prefs.dart';
import '../../shared/widgets/gradient_button.dart';
import 'models/onboarding_item.dart';
class OnboardingPage extends StatefulWidget {
const OnboardingPage({super.key});
State<OnboardingPage> createState() => _OnboardingPageState();
}
class _OnboardingPageState extends State<OnboardingPage> {
final PageController _pageController = PageController();
int _currentPage = 0;
void dispose() {
_pageController.dispose();
super.dispose();
}
void _onPageChanged(int page) {
setState(() {
_currentPage = page;
});
}
Future<void> _onGetStarted() async {
// 标记引导页已完成
await AppPrefs.setOnboardingDone();
if (!mounted) return;
// 跳转到登录页
context.go('/login');
}
void _onSkip() {
// 跳转到最后一页
_pageController.animateToPage(
onboardingItems.length - 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.bgDark,
body: SafeArea(
child: Column(
children: [
// 顶部跳过按钮
if (_currentPage < onboardingItems.length - 1)
Align(
alignment: Alignment.topRight,
child: TextButton(
onPressed: _onSkip,
child: const Text(
'跳过',
style: TextStyle(
color: AppColors.gray400,
fontSize: 16,
),
),
),
)
else
const SizedBox(height: 48),
// PageView
Expanded(
child: PageView.builder(
controller: _pageController,
onPageChanged: _onPageChanged,
itemCount: onboardingItems.length,
itemBuilder: (context, index) {
return _buildPage(onboardingItems[index]);
},
),
),
// 指示器
_buildIndicator(),
const SizedBox(height: 32),
// 底部按钮
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: _currentPage == onboardingItems.length - 1
? GradientButton(
text: '立即开始',
onPressed: _onGetStarted,
)
: GradientButton(
text: '下一步',
onPressed: () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
),
const SizedBox(height: 32),
],
),
),
);
}
Widget _buildPage(OnboardingItem item) {
return Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 图标
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: item.color.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
item.icon,
size: 60,
color: item.color,
),
),
const SizedBox(height: 48),
// 标题
Text(
item.title,
style: const TextStyle(
color: AppColors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// 描述
Text(
item.description,
style: const TextStyle(
color: AppColors.gray400,
fontSize: 16,
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
onboardingItems.length,
(index) => AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: _currentPage == index ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: _currentPage == index
? AppColors.purple
: AppColors.gray700,
borderRadius: BorderRadius.circular(4),
),
),
),
);
}
}
I've added the onboarding page implementation with:
- Data model for onboarding items
- PageView-based UI with three pages
- Page indicator with animation
- Skip button and navigation logic
Now I should continue with the next section covering animations and optimizations. Let me add that in the next segment.
设计要点:
- PageView: 使用 PageView.builder 实现滑动切换
- 指示器动画: 当前页指示器宽度变化,使用 AnimatedContainer
- 跳过功能: 点击跳过直接跳转到最后一页
- 状态保存: 完成后标记引导页已完成,下次启动不再显示
四、动画增强
4.1 页面切换动画
为引导页添加视差滚动效果:
// lib/features/onboarding/widgets/parallax_page.dart
class ParallaxPage extends StatelessWidget {
final OnboardingItem item;
final double pageOffset;
const ParallaxPage({
required this.item,
required this.pageOffset,
super.key,
});
Widget build(BuildContext context) {
// 计算视差偏移量
final iconOffset = pageOffset * 50;
final textOffset = pageOffset * 30;
return Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 图标 - 视差效果
Transform.translate(
offset: Offset(iconOffset, 0),
child: Opacity(
opacity: 1 - pageOffset.abs().clamp(0.0, 1.0),
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: item.color.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
item.icon,
size: 60,
color: item.color,
),
),
),
),
const SizedBox(height: 48),
// 文字 - 视差效果
Transform.translate(
offset: Offset(textOffset, 0),
child: Opacity(
opacity: 1 - pageOffset.abs().clamp(0.0, 1.0),
child: Column(
children: [
Text(
item.title,
style: const TextStyle(
color: AppColors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
item.description,
style: const TextStyle(
color: AppColors.gray400,
fontSize: 16,
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
),
),
],
),
);
}
}
在 OnboardingPage 中使用:
PageView.builder(
controller: _pageController,
onPageChanged: _onPageChanged,
itemCount: onboardingItems.length,
itemBuilder: (context, index) {
// 计算页面偏移量
double pageOffset = 0.0;
if (_pageController.hasClients && _pageController.position.haveDimensions) {
pageOffset = (_pageController.page ?? 0) - index;
}
return ParallaxPage(
item: onboardingItems[index],
pageOffset: pageOffset,
);
},
)
4.2 按钮动画
为"立即开始"按钮添加脉冲动画:
// lib/shared/widgets/pulse_button.dart
class PulseButton extends StatefulWidget {
final String text;
final VoidCallback onPressed;
const PulseButton({
required this.text,
required this.onPressed,
super.key,
});
State<PulseButton> createState() => _PulseButtonState();
}
class _PulseButtonState extends State<PulseButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat(reverse: true);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 1.05,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: child,
);
},
child: GradientButton(
text: widget.text,
onPressed: widget.onPressed,
),
);
}
}
I've added animation enhancements including parallax scrolling effects and pulse button animation. Now I should add sections on:
- HarmonyOS-specific considerations
- Performance optimization
- Route integration
- Summary and key points
Let me continue with the HarmonyOS considerations and route integration sections.
五、路由集成
5.1 GoRouter 配置
// lib/app/router.dart
final router = GoRouter(
initialLocation: '/splash',
routes: [
// 启动页
GoRoute(
path: '/splash',
builder: (context, state) => const SplashPage(),
),
// 引导页
GoRoute(
path: '/onboarding',
builder: (context, state) => const OnboardingPage(),
),
// 登录页
GoRoute(
path: '/login',
builder: (context, state) => const LoginPage(),
),
// 首页
GoRoute(
path: '/home',
builder: (context, state) => const HomePage(),
),
],
);
5.2 启动逻辑优化
在 main.dart 中预加载状态:
// lib/main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化本地存储
await AppPrefs.init();
// 预判断启动路由
String initialRoute = '/splash';
// 如果已完成引导且已登录,直接进入首页
final onboardingDone = await AppPrefs.isOnboardingDone();
final isLoggedIn = await AppPrefs.isLoggedIn();
if (onboardingDone && isLoggedIn) {
initialRoute = '/home';
}
runApp(MyApp(initialRoute: initialRoute));
}
class MyApp extends StatelessWidget {
final String initialRoute;
const MyApp({required this.initialRoute, super.key});
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'CleanMark AI',
theme: AppTheme.dark,
routerConfig: GoRouter(
initialLocation: initialRoute,
routes: [...],
),
);
}
}
六、HarmonyOS 适配要点
6.1 启动页配置
HarmonyOS 需要在 module.json5 中配置启动页:
// ohos/entry/src/main/module.json5
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:splash_icon",
"startWindowBackground": "$color:splash_background"
}
]
}
}
6.2 原生启动页
HarmonyOS 可以使用原生启动窗口,避免白屏:
// ohos/entry/src/main/ets/entryability/EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
// 设置启动窗口背景
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
return;
}
// 获取主窗口
windowStage.getMainWindow((err, windowClass) => {
if (err.code) {
return;
}
// 设置全屏
windowClass.setWindowLayoutFullScreen(true);
});
});
}
}
6.3 状态栏适配
// lib/features/splash/splash_page.dart
import 'package:flutter/services.dart';
void initState() {
super.initState();
// 设置状态栏样式
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
),
);
_initAnimation();
_navigateToNext();
}
七、性能优化
7.1 预加载优化
// 在启动页预加载必要资源
class SplashPage extends StatefulWidget {
void initState() {
super.initState();
_preloadResources();
}
Future<void> _preloadResources() async {
// 预加载图片
await precacheImage(
const AssetImage('assets/images/logo.png'),
context,
);
// 预初始化服务
await Future.wait([
AppPrefs.init(),
// 其他初始化操作
]);
}
}
7.2 动画性能优化
// 使用 RepaintBoundary 隔离重绘区域
RepaintBoundary(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: child,
);
},
child: const Logo(),
),
)
7.3 内存优化
// 及时释放动画控制器
void dispose() {
_controller.dispose();
_pageController.dispose();
super.dispose();
}
八、实战技巧
8.1 调试启动流程
// 添加调试开关,方便测试
class DebugConfig {
static const bool skipSplash = false; // 跳过启动页
static const bool skipOnboarding = false; // 跳过引导页
static const bool forceOnboarding = false; // 强制显示引导页
}
// 在启动逻辑中使用
Future<void> _navigateToNext() async {
if (DebugConfig.skipSplash) {
context.go('/home');
return;
}
await Future.delayed(const Duration(milliseconds: 2000));
if (DebugConfig.forceOnboarding) {
context.go('/onboarding');
return;
}
// 正常流程...
}
8.2 重置引导页
// 提供重置功能,方便测试
class SettingsPage extends StatelessWidget {
Future<void> _resetOnboarding() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('onboarding_done');
// 提示用户
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('引导页已重置,重启应用生效')),
);
}
}
本篇完整小结
这篇文章我们完成了:
- ✅ 启动流程设计(启动页 → 引导页 → 首页/登录页)
- ✅ 启动页实现(渐变背景 + Logo 动画)
- ✅ 引导页实现(PageView + 指示器)
- ✅ 动画增强(视差滚动 + 脉冲按钮)
- ✅ 路由集成(GoRouter 配置)
- ✅ HarmonyOS 适配(原生启动窗口)
- ✅ 性能优化(预加载 + RepaintBoundary)
- ✅ 实战技巧(调试开关 + 重置功能)
关键要点:
- 使用 SharedPreferences 记录启动状态
- 启动页动画时长控制在 2 秒左右
- 引导页使用 PageView 实现滑动切换
- 添加视差滚动效果提升体验
- HarmonyOS 配置原生启动窗口避免白屏
- 使用 RepaintBoundary 优化动画性能
- 提供调试开关方便开发测试
动画设计原则:
- 时长适中:不要太快(用户看不清)也不要太慢(用户等待)
- 曲线选择:使用 easeOutBack 产生弹性效果
- 视差效果:不同元素不同速度,增加层次感
- 性能优先:使用 RepaintBoundary 隔离重绘区域
思考题
- 为什么要在 main.dart 中预判断启动路由,而不是在启动页中判断?
- 如何实现引导页只在首次安装后显示,更新版本不显示?
- 视差滚动效果的原理是什么?如何计算偏移量?
下一篇预告:第09篇 - 图片去水印功能实现
更多推荐


所有评论(0)