Flutter for OpenHarmony 实战:全屏弹窗实现
自定义组件:通过创建类实现了可复用的全屏弹窗组件参数化设计:使用命名参数和默认值,提高组件的灵活性和易用性文档注释:为组件和参数添加了详细的文档注释,提高代码可读性和可维护性。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
前言:跨生态开发的新机遇
在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。
Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。将现有的Flutter应用适配到鸿蒙,听起来像是一个“跨界”任务,但它本质上是一次有价值的技术拓展:让产品触达更多用户,也让技术栈覆盖更广。
不过,这条路走起来并不像听起来那么简单。Flutter和鸿蒙,从底层的架构到上层的工具链,都有着各自的设计逻辑。会遇到一些具体的问题:代码如何组织?原有的功能在鸿蒙上如何实现?那些平台特有的能力该怎么调用?更实际的是,从编译打包到上架部署,整个流程都需要重新摸索。
这篇文章想做的,就是把这些我们趟过的路、踩过的坑,清晰地摊开给你看。我们不会只停留在“怎么做”,还会聊到“为什么得这么做”,以及“如果出了问题该往哪想”。这更像是一份实战笔记,源自真实的项目经验,聚焦于那些真正卡住过我们的环节。
无论你是在为一个成熟产品寻找新的落地平台,还是从一开始就希望构建能面向多端的应用,这里的思路和解决方案都能提供直接的参考。理解了两套体系之间的异同,掌握了关键的衔接技术,不仅能完成这次迁移,更能积累起应对未来技术变化的能力。
混合工程结构深度解析
项目目录架构
当Flutter项目集成鸿蒙支持后,典型的项目结构会发生显著变化。以下是经过ohos_flutter插件初始化后的项目结构:
my_flutter_harmony_app/
├── lib/ # Flutter业务代码(基本不变)
│ ├── main.dart # 应用入口
│ ├── home_page.dart # 首页
│ └── utils/
│ └── platform_utils.dart # 平台工具类
├── pubspec.yaml # Flutter依赖配置
├── ohos/ # 鸿蒙原生层(核心适配区)
│ ├── entry/ # 主模块
│ │ └── src/main/
│ │ ├── ets/ # ArkTS代码
│ │ │ ├── MainAbility/
│ │ │ │ ├── MainAbility.ts # 主Ability
│ │ │ │ └── MainAbilityContext.ts
│ │ │ └── pages/
│ │ │ ├── Index.ets # 主页面
│ │ │ └── Splash.ets # 启动页
│ │ ├── resources/ # 鸿蒙资源文件
│ │ │ ├── base/
│ │ │ │ ├── element/ # 字符串等
│ │ │ │ ├── media/ # 图片资源
│ │ │ │ └── profile/ # 配置文件
│ │ │ └── en_US/ # 英文资源
│ │ └── config.json # 应用核心配置
│ ├── ohos_test/ # 测试模块
│ ├── build-profile.json5 # 构建配置
│ └── oh-package.json5 # 鸿蒙依赖管理
└── README.md
展示效果图片
flutter 实时预览 效果展示
运行到鸿蒙虚拟设备中效果展示
目录
功能代码实现
全屏弹窗组件
组件设计思路
全屏弹窗是一种常见的交互模式,通常用于需要用户集中注意力完成某个操作的场景。本次开发的 FullScreenDialog 组件采用了以下设计思路:
- 分层结构:使用
Stack布局实现背景遮罩和弹窗内容的分层显示 - 动画效果:通过
AnimatedOpacity和AnimatedPositioned实现平滑的显示/隐藏动画 - 交互体验:支持点击背景遮罩和右上角关闭按钮关闭弹窗
- 可定制性:提供了背景透明度、动画时长等可配置参数
- 内容灵活性:通过
content参数支持自定义弹窗内容
组件实现代码
import 'package:flutter/material.dart';
/// 全屏弹窗组件
class FullScreenDialog extends StatelessWidget {
/// 是否显示弹窗
final bool isVisible;
/// 弹窗内容
final Widget content;
/// 关闭弹窗的回调
final VoidCallback onClose;
/// 背景透明度
final double backgroundOpacity;
/// 动画时长
final Duration animationDuration;
const FullScreenDialog({
super.key,
required this.isVisible,
required this.content,
required this.onClose,
this.backgroundOpacity = 0.5,
this.animationDuration = const Duration(milliseconds: 300),
});
Widget build(BuildContext context) {
if (!isVisible) {
return const SizedBox.shrink();
}
return Stack(
children: [
// 背景遮罩
GestureDetector(
onTap: onClose,
child: AnimatedOpacity(
opacity: isVisible ? backgroundOpacity : 0.0,
duration: animationDuration,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black,
),
),
),
// 弹窗内容
AnimatedPositioned(
top: isVisible ? 0 : MediaQuery.of(context).size.height,
left: 0,
right: 0,
height: MediaQuery.of(context).size.height,
duration: animationDuration,
curve: Curves.easeInOut,
child: GestureDetector(
// 防止点击内容区域关闭弹窗
onTap: () {},
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// 顶部关闭按钮和指示器
Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
// 指示器
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
// 关闭按钮
Align(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: onClose,
icon: const Icon(Icons.close),
color: Colors.black,
),
),
],
),
),
// 内容区域
Expanded(
child: content,
),
],
),
),
),
),
],
);
}
}
组件参数说明
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
isVisible |
bool |
是 | - | 控制弹窗是否显示 |
content |
Widget |
是 | - | 弹窗的自定义内容 |
onClose |
VoidCallback |
是 | - | 关闭弹窗的回调函数 |
backgroundOpacity |
double |
否 | 0.5 |
背景遮罩的透明度 |
animationDuration |
Duration |
否 | Duration(milliseconds: 300) |
显示/隐藏动画的时长 |
组件使用方法
在首页中的应用
本次开发中,我们在首页直接集成了全屏弹窗组件,默认显示弹窗,用户可以通过点击关闭按钮或背景区域关闭弹窗,也可以通过首页的按钮重新显示弹窗。
使用代码示例
import 'package:flutter/material.dart';
import 'package:aa/components/full_screen_dialog.dart';
/// 功能特点列表项组件
class FeatureItem extends StatelessWidget {
final IconData icon;
final String text;
const FeatureItem({super.key, required this.icon, required this.text});
Widget build(BuildContext context) {
return Row(
children: [
Icon(
icon,
color: Colors.deepPurple,
size: 20,
),
const SizedBox(width: 12),
Text(
text,
style: const TextStyle(fontSize: 16),
),
],
);
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter for openHarmony',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const MyHomePage(title: 'Flutter for openHarmony'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 控制全屏弹窗显示
bool _isDialogVisible = true;
// 关闭弹窗
void _closeDialog() {
setState(() {
_isDialogVisible = false;
});
}
// 重新显示弹窗
void _showDialog() {
setState(() {
_isDialogVisible = true;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Stack(
children: [
// 首页内容
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'首页内容',
style: TextStyle(fontSize: 24),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _showDialog,
child: const Text('重新显示弹窗'),
),
],
),
),
// 全屏弹窗
FullScreenDialog(
isVisible: _isDialogVisible,
onClose: _closeDialog,
content: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// 标题部分
const Text(
'欢迎使用',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Flutter for OpenHarmony',
style: TextStyle(fontSize: 24, color: Colors.deepPurple),
),
const SizedBox(height: 40),
// 图片部分
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
color: Colors.deepPurple.withOpacity(0.1),
),
child: Center(
child: Icon(
Icons.flutter_dash,
size: 120,
color: Colors.deepPurple,
),
),
),
const SizedBox(height: 40),
// 功能介绍卡片
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 0,
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'功能特点',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
FeatureItem(
icon: Icons.check_circle,
text: '可复用的全屏弹窗组件',
),
SizedBox(height: 12),
FeatureItem(
icon: Icons.check_circle,
text: '流畅的动画过渡效果',
),
SizedBox(height: 12),
FeatureItem(
icon: Icons.check_circle,
text: '响应式布局设计',
),
SizedBox(height: 12),
FeatureItem(
icon: Icons.check_circle,
text: '易于集成和自定义',
),
],
),
),
const SizedBox(height: 40),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton(
onPressed: _closeDialog,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
side: const BorderSide(color: Colors.deepPurple),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: const Text('稍后再说', style: TextStyle(color: Colors.deepPurple)),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: () {
print('开始使用');
_closeDialog();
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
backgroundColor: Colors.deepPurple,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: const Text('开始使用'),
),
],
),
const SizedBox(height: 40),
// 底部提示
const Text(
'点击右上角关闭按钮或背景区域关闭弹窗',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
),
],
),
);
}
}
开发注意事项
-
布局层级:使用
Stack布局时,组件的顺序很重要,后添加的组件会显示在前面。因此,全屏弹窗组件应该放在Stack的最后面,以确保它能覆盖其他内容。 -
点击事件处理:为了防止点击弹窗内容区域时关闭弹窗,我们在弹窗内容的
GestureDetector中添加了空的onTap回调,这样点击事件就不会传递到背景遮罩。 -
状态管理:弹窗的显示/隐藏状态需要通过父组件的状态来管理,这要求父组件是一个
StatefulWidget。 -
动画性能:使用
AnimatedOpacity和AnimatedPositioned时,Flutter 会自动处理动画的性能优化,确保动画流畅运行。 -
响应式设计:弹窗高度使用
MediaQuery.of(context).size.height确保在不同屏幕尺寸上都能全屏显示。
本次开发中容易遇到的问题
1. 包名导入错误
问题描述
在导入组件时,可能会因为包名配置不正确而导致编译错误。例如,使用了 package:aa2/components/full_screen_dialog.dart 而不是正确的 package:aa/components/full_screen_dialog.dart。
解决方案
- 检查
pubspec.yaml文件中的name字段,确保使用的包名与配置一致 - 可以使用相对路径导入(如
import 'components/full_screen_dialog.dart')来避免包名问题
2. 布局层级问题
问题描述
如果全屏弹窗没有显示在最上层,可能是因为在 Stack 布局中的顺序不正确,或者其他组件设置了较高的 z-index。
解决方案
- 确保全屏弹窗组件在
Stack的子组件列表中位于最后面 - 检查其他组件是否设置了
Positioned或Align等可能影响层级的属性
3. 点击事件穿透
问题描述
点击弹窗内容区域时,可能会意外触发背景内容的点击事件,导致弹窗关闭或其他意外行为。
解决方案
- 在弹窗内容的外层添加
GestureDetector并设置空的onTap回调 - 确保背景遮罩的
GestureDetector只在点击背景区域时触发关闭回调
4. 动画效果不流畅
问题描述
在一些低端设备上,弹窗的显示/隐藏动画可能会出现卡顿现象。
解决方案
- 适当调整
animationDuration,避免动画时长过长 - 确保弹窗内容不会过于复杂,减少动画过程中的渲染压力
- 考虑使用
RepaintBoundary包裹复杂的内容区域,减少不必要的重绘
总结本次开发中用到的技术点
1. Flutter 组件化开发
- 自定义组件:通过创建
FullScreenDialog类实现了可复用的全屏弹窗组件 - 参数化设计:使用命名参数和默认值,提高组件的灵活性和易用性
- 文档注释:为组件和参数添加了详细的文档注释,提高代码可读性和可维护性
2. 布局与动画
- Stack 布局:实现了背景遮罩和弹窗内容的分层显示
- AnimatedOpacity:实现背景遮罩的淡入淡出动画
- AnimatedPositioned:实现弹窗内容的滑入滑出动画
- GestureDetector:处理点击事件,实现背景遮罩点击关闭功能
3. 状态管理
- StatefulWidget:使用状态管理弹窗的显示/隐藏状态
- setState:通过调用
setState方法更新状态,触发 UI 重建
4. 响应式设计
- MediaQuery:使用
MediaQuery.of(context).size获取屏幕尺寸,确保弹窗在不同设备上都能全屏显示 - 弹性布局:使用
Expanded组件确保弹窗内容区域能够自适应剩余空间
5. 代码组织
- 目录结构:创建
lib/components目录专门存放可复用组件 - 代码分离:将全屏弹窗组件从主页面代码中分离出来,提高代码的模块化程度
6. 开发最佳实践
- 组件复用:通过抽象和参数化设计,创建可在多个场景中复用的组件,如
FullScreenDialog和FeatureItem - 动画优化:合理使用 Flutter 提供的动画组件,确保动画流畅运行
- 用户体验:添加平滑的动画效果、清晰的交互反馈和丰富的视觉元素,提升用户体验
- 代码可读性:使用清晰的命名和详细的注释,提高代码的可读性和可维护性
- 响应式设计:使用
SingleChildScrollView确保内容在不同屏幕尺寸上正常显示 - 视觉层次:通过卡片、阴影、颜色对比等元素创建清晰的视觉层次结构
- 交互设计:提供明确的操作按钮和反馈机制,引导用户完成预期操作
本次开发的全屏弹窗组件不仅实现了基本的功能需求,还通过精心的设计和实现,确保了良好的用户体验和代码质量。这种组件化的开发方式也为后续的功能扩展和代码维护打下了坚实的基础。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)