开源鸿蒙 Flutter 实战|应用启动页(Splash Screen)全流程实现
本文详细介绍了基于Flutter框架实现开源鸿蒙应用启动页(Splash Screen)的全流程开发。针对新手常见问题,提供了五大核心功能模块的实现方案:渐变背景、品牌Logo弹性缩放、应用名称滑入动画、进度条加载和平滑过渡到主页。重点解决了启动页白屏、动画卡顿、跳转时机不当、Logo布局错位等典型问题,并给出了完整的代码实现和优化建议。文章包含技术选型说明、开发踩坑复盘、修复方案以及可直接复用的
🚀 开源鸿蒙 Flutter 实战|应用启动页(Splash Screen)全流程实现
欢迎加入开源鸿蒙跨平台社区→https://openharmonycrosplatform.csdn.net
【摘要】本文面向开源鸿蒙跨平台开发新手,基于 Flutter 框架完成 ** 任务 1:添加应用启动页(Splash Screen)** 的全流程开发,实现了渐变背景、品牌 Logo 弹性缩放、应用名称滑入、进度条加载、平滑过渡到主页五大核心模块,重点修复了启动页白屏、动画卡顿、跳转时机不对、Logo 布局错乱等新手高频踩坑问题,完整讲解了代码实现、踩坑复盘、鸿蒙适配要点与虚拟机实机运行验证,代码可直接复制复用,完美适配开源鸿蒙设备。
哈喽宝子们!我是刚学鸿蒙跨平台开发的大一新生😆
这次我完成了 ** 任务 1:添加应用启动页(Splash Screen)** 的开发,最开始踩了好几个新手坑:启动页刚打开有白屏、动画同时播放太乱、进度条加载完还没跳转、Logo 在不同设备上布局错乱!经过两轮优化,我不仅解决了这些问题,还实现了渐变背景、弹性 Logo、滑入文字、进度条加载、平滑过渡全功能,已经在 Windows 和开源鸿蒙虚拟机上完整验证通过!
先给大家汇报一下这次的最终完成成果✨:
✅ 渐变紫色背景,带装饰圆,视觉效果高级
✅ 品牌 Logo 弹性缩放 + 淡入动画,有质感
✅ 应用名称 + Slogan 滑入 + 淡入动画,层次分明
✅ 线性进度条加载,0%→100%,带百分比文字
✅ 版本号淡入显示,信息完整
✅ 加载完成后平滑淡出过渡到主页,体验流畅
✅ 深色 / 浅色模式自动适配,背景色自动调整
✅ 开源鸿蒙虚拟机实机验证,启动流畅,无白屏、无卡顿
✅ 代码结构清晰,新手可直接修改 Logo、文字、颜色
一、技术选型说明
全程使用 Flutter 原生动画 API,无需引入额外的大型动画库,完全规避兼容风险,新手可以放心使用:
二、开发踩坑复盘与修复方案
作为大一新生,这次开发踩了好几个新手高频踩坑点,整理出来给大家避避坑👇
🔴 坑 1:启动页刚打开有白屏,体验很差
错误现象:点击应用图标,先出现 1-2 秒的白屏,然后才显示启动页,体验非常差。
根本原因:
Flutter 应用的默认启动页是空白的,没有设置原生的启动页
应用初始化需要时间,这段时间显示的是空白页面
修复方案:
Android 端:在android/app/src/main/res/drawable/launch_background.xml中设置渐变背景和 Logo,作为原生启动页
iOS 端:在ios/Runner/Assets.xcassets/LaunchImage.imageset中设置启动图片
鸿蒙端:在entry/src/main/resources/base/media/launch_image.png中设置启动图片,作为原生启动页
Flutter 端的启动页作为第二阶段,在应用初始化完成后显示,避免白屏
🔴 坑 2:所有动画同时播放,太乱没有层次
错误现象:Logo、应用名称、Slogan、进度条同时开始动画,页面很乱,没有层次感。
根本原因:
所有动画的延迟时间都设置为 0,同时开始
没有按照视觉层次设置动画的先后顺序
修复方案:
按照视觉层次设置动画延迟:
Logo:延迟 300ms 开始
应用名称 + Slogan:延迟 900ms 开始
进度条 + 版本号:延迟 900ms 开始
每个动画的时长也做区分:
Logo:600ms,弹性效果
文字:400ms,缓动效果
进度条:1500ms,线性效果
动画曲线也做区分:
Logo:Curves.elasticOut,弹性效果
文字:Curves.easeOutCubic,缓动效果
进度条:Curves.linear,线性效果
🔴 坑 3:进度条加载完还没跳转,用户等待时间太长
错误现象:进度条已经到 100% 了,但是页面还没跳转,用户还要等 1-2 秒,体验很差。
根本原因:
进度条动画和跳转逻辑没有同步,进度条动画完成后,还在等固定的时间
没有监听动画控制器的状态,动画完成后没有立即触发跳转
修复方案:
监听动画控制器的AnimationStatus.completed状态,进度条动画完成后立即触发跳转
跳转前加一个 300ms 的淡出动画,让过渡更平滑,不会太突兀
总加载时间控制在 2.5 秒左右,既展示了品牌,又不让用户等太久
🔴 坑 4:Logo 在不同设备上布局错乱,大小不一致
错误现象:Logo 在小屏幕手机上太大,在大屏幕平板上太小,布局错乱。
根本原因:
Logo 的尺寸用了固定的像素值,没有根据屏幕尺寸自适应
没有使用MediaQuery获取屏幕尺寸,动态调整 Logo 大小
修复方案:
使用MediaQuery.of(context).size获取屏幕尺寸
Logo 的宽度设置为屏幕宽度的 25%,高度和宽度一致,确保正方形
使用LayoutBuilder监听布局变化,动态调整尺寸
所有间距也使用相对值,比如屏幕宽度的 2%,确保在不同设备上布局一致
三、核心代码完整实现(可直接复制)
我把所有代码都做了规范整理,带完整注释,新手直接复制就能用。
3.1 第一步:创建启动页
在lib/pages目录下新建splash_page.dart,完整代码如下:
import 'package:flutter/material.dart';
import 'main.dart';
/// 启动页
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> with TickerProviderStateMixin {
/// Logo动画控制器
late AnimationController _logoController;
/// Logo缩放动画
late Animation<double> _logoScaleAnimation;
/// Logo淡入动画
late Animation<double> _logoFadeAnimation;
/// 文字动画控制器
late AnimationController _textController;
/// 应用名称滑入动画
late Animation<Offset> _titleSlideAnimation;
/// 应用名称淡入动画
late Animation<double> _titleFadeAnimation;
/// Slogan滑入动画
late Animation<Offset> _sloganSlideAnimation;
/// Slogan淡入动画
late Animation<double> _sloganFadeAnimation;
/// 进度条动画控制器
late AnimationController _progressController;
/// 进度条动画
late Animation<double> _progressAnimation;
/// 版本号淡入动画
late Animation<double> _versionFadeAnimation;
/// 淡出动画控制器
late AnimationController _fadeOutController;
/// 淡出动画
late Animation<double> _fadeOutAnimation;
void initState() {
super.initState();
_initAnimations();
_startAnimations();
}
void dispose() {
_logoController.dispose();
_textController.dispose();
_progressController.dispose();
_fadeOutController.dispose();
super.dispose();
}
/// 初始化所有动画
void _initAnimations() {
// Logo动画
_logoController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_logoScaleAnimation = Tween<double>(begin: 0.5, end: 1.0).animate(
CurvedAnimation(parent: _logoController, curve: Curves.elasticOut),
);
_logoFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _logoController, curve: Curves.easeIn),
);
// 文字动画
_textController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_titleSlideAnimation = Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
CurvedAnimation(parent: _textController, curve: Curves.easeOutCubic),
);
_titleFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _textController, curve: Curves.easeIn),
);
_sloganSlideAnimation = Tween<Offset>(begin: const Offset(0, 0.2), end: Offset.zero).animate(
CurvedAnimation(parent: _textController, curve: Curves.easeOutCubic),
);
_sloganFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _textController, curve: Curves.easeIn),
);
// 进度条动画
_progressController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_progressAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _progressController, curve: Curves.linear),
);
_versionFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _progressController, curve: Curves.easeIn),
);
// 淡出动画
_fadeOutController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeOutAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _fadeOutController, curve: Curves.easeOut),
);
// 监听进度条动画完成,触发跳转
_progressController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_navigateToHome();
}
});
}
/// 开始所有动画
void _startAnimations() {
// 延迟300ms开始Logo动画
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) {
_logoController.forward();
}
});
// 延迟900ms开始文字和进度条动画
Future.delayed(const Duration(milliseconds: 900), () {
if (mounted) {
_textController.forward();
_progressController.forward();
}
});
}
/// 跳转到主页
void _navigateToHome() {
_fadeOutController.forward().then((_) {
if (mounted) {
Navigator.pushReplacement(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const MainPage(),
transitionDuration: Duration.zero,
),
);
}
});
}
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final screenSize = MediaQuery.of(context).size;
return FadeTransition(
opacity: _fadeOutAnimation,
child: Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDarkMode
? [const Color(0xFF1a1a2e), const Color(0xFF16213e)]
: [const Color(0xFF667eea), const Color(0xFF764ba2)],
),
),
child: Stack(
children: [
// 背景装饰圆
Positioned(
top: -screenSize.width * 0.2,
left: -screenSize.width * 0.2,
child: Container(
width: screenSize.width * 0.6,
height: screenSize.width * 0.6,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
shape: BoxShape.circle,
),
),
),
Positioned(
bottom: -screenSize.width * 0.15,
right: -screenSize.width * 0.15,
child: Container(
width: screenSize.width * 0.5,
height: screenSize.width * 0.5,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
shape: BoxShape.circle,
),
),
),
// 主内容
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
ScaleTransition(
scale: _logoScaleAnimation,
child: FadeTransition(
opacity: _logoFadeAnimation,
child: Container(
width: screenSize.width * 0.25,
height: screenSize.width * 0.25,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Icon(
Icons.code,
size: screenSize.width * 0.15,
color: const Color(0xFF667eea),
),
),
),
),
SizedBox(height: screenSize.height * 0.05),
// 应用名称
SlideTransition(
position: _titleSlideAnimation,
child: FadeTransition(
opacity: _titleFadeAnimation,
child: const Text(
'开发者社区',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.2,
),
),
),
),
SizedBox(height: screenSize.height * 0.015),
// Slogan
SlideTransition(
position: _sloganSlideAnimation,
child: FadeTransition(
opacity: _sloganFadeAnimation,
child: Text(
'连接开发者,分享创造',
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.8),
letterSpacing: 0.5,
),
),
),
),
SizedBox(height: screenSize.height * 0.08),
// 进度条
FadeTransition(
opacity: _versionFadeAnimation,
child: Column(
children: [
// 进度条
Container(
width: screenSize.width * 0.6,
height: 6,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(3),
),
child: AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: _progressAnimation.value,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(3),
),
),
);
},
),
),
SizedBox(height: screenSize.height * 0.015),
// 百分比文字
AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
final percentage = (_progressAnimation.value * 100).toInt();
return Text(
'$percentage%',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
fontWeight: FontWeight.w500,
),
);
},
),
],
),
),
],
),
),
// 版本号
Positioned(
bottom: screenSize.height * 0.05,
left: 0,
right: 0,
child: FadeTransition(
opacity: _versionFadeAnimation,
child: const Text(
'v1.0.0',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
),
],
),
),
),
);
}
}
3.2 第二步:集成到 main.dart
修改lib/main.dart,把启动页作为初始页面:
import 'package:flutter/material.dart';
import 'pages/splash_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '开发者社区',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF667eea)),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF667eea),
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system,
home: const SplashPage(),
);
}
}
/// 主页(示例)
class MainPage extends StatelessWidget {
const MainPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('开发者社区')),
body: const Center(
child: Text('欢迎来到开发者社区!'),
),
);
}
}
四、全项目接入说明
4.1 接入步骤
把splash_page.dart复制到lib/pages目录下
替换main.dart的内容为上面的代码
运行flutter pub get(如果需要)
运行应用,查看启动页效果
4.2 自定义说明
修改 Logo:替换Icon(Icons.code)为你的 Logo 图片
修改文字:替换 “开发者社区” 和 “连接开发者,分享创造” 为你的应用名称和 Slogan
修改颜色:替换渐变背景的颜色值为你的品牌色
修改时长:调整动画控制器的duration参数,改变动画速度
4.3 运行命令
# 安装依赖
flutter pub get
# Windows端运行
flutter run -d windows
# 鸿蒙端运行(需配置鸿蒙开发环境)
flutter run -d ohos
五、开源鸿蒙平台适配核心要点
5.1 原生启动页适配
在鸿蒙端的entry/src/main/resources/base/media/目录下添加launch_image.png,作为原生启动页
原生启动页的背景色和 Flutter 端的渐变背景色保持一致,避免视觉断层
原生启动页的 Logo 和 Flutter 端的 Logo 保持一致,提升品牌一致性
5.2 性能优化
所有静态组件都用const修饰,避免不必要的重建,提升鸿蒙设备上的性能
动画控制器在dispose中释放,避免内存泄漏
总加载时间控制在 2.5 秒左右,既展示了品牌,又不让用户等太久
针对鸿蒙设备,避免使用过于复杂的动画组合,优先使用单一动画
5.3 深色模式适配
渐变背景色根据isDarkMode动态适配,深色模式下用深紫色,浅色模式下用浅紫色
所有文字、装饰圆的透明度也做了调整,确保深色模式下的对比度和可读性
使用ThemeMode.system,自动跟随系统的深色 / 浅色模式
5.4 权限说明
启动页功能为纯 UI 实现和动画渲染,无需申请任何开源鸿蒙系统权限,直接接入即可使用,无需修改鸿蒙配置文件。
六、开源鸿蒙虚拟机运行验证
6.1 一键运行命令
# 进入鸿蒙工程目录
cd ohos
# 构建HAP安装包
hvigorw assembleHap -p product=default -p buildMode=debug
# 安装到鸿蒙虚拟机
hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap
# 启动应用
hdc shell aa start -a EntryAbility -b com.example.demo1
Flutter 开源鸿蒙启动页 - 完整启动流程
效果:完整启动流程:原生启动页→Flutter 启动页→主页,过渡平滑,体验连贯
Flutter 开源鸿蒙启动页
七、新手学习总结
作为刚学 Flutter 和鸿蒙开发的大一新生,这次应用启动页的开发真的让我收获满满!从最开始的白屏、动画混乱,到最终实现了流畅、有质感的启动页,整个过程让我对 Flutter 的动画控制器、动画曲线、渐变背景、布局适配有了更深入的理解,而且完全兼容开源鸿蒙平台,成就感直接拉满🥰
这次开发也让我明白了几个新手一定要注意的点:
1.启动页一定要做原生的,不然刚打开会有白屏,体验很差,原生启动页和 Flutter 启动页配合,体验才好
2.动画不要同时播放,要按照视觉层次设置延迟,先 Logo,再文字,再进度条,这样才有层次感,不乱
3.动画控制器一定要在dispose中释放,不然会内存泄漏,虽然新手可能感觉不到,但这是个好习惯
4.布局一定要用相对值,比如屏幕宽度的百分比,不要用固定像素,不然在不同设备上布局会错乱
5.动画曲线的选择很重要,Logo 用弹性曲线,文字用缓动曲线,进度条用线性曲线,选对了效果会很自然
开源鸿蒙对 Flutter 原生动画 API 的支持真的越来越好了,只要按照规范开发,基本不会出现大的兼容问题
后续我还会继续优化启动页,比如添加 Lottie 动画、添加品牌视频、添加跳过按钮、添加隐私政策弹窗,也会持续给大家分享
我的鸿蒙 Flutter 新手实战内容,和大家一起在开源鸿蒙的生态里慢慢进步✨
如果这篇文章有帮到你,或者你也有更好的启动页实现思路,欢迎在评论区和我交流呀!
更多推荐


所有评论(0)