Flutter for OpenHarmony 实战:天气动画(如下雨、下雪)
在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。
欢迎加入开源鸿蒙跨平台社区: 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 实时预览 效果展示
运行到鸿蒙虚拟设备中效果展示
目录
功能代码实现
天气动画核心组件实现
WeatherAnimation 组件
WeatherAnimation 是本次开发的核心组件,它使用 CustomPaint 实现天气动画效果,支持晴天、雨天、雪天三种天气类型。该组件具有良好的可配置性,支持自定义动画大小和点击回调。
核心实现代码:
import 'package:flutter/material.dart';
import 'dart:math';
enum WeatherType {
sunny,
rainy,
snowy,
}
class WeatherAnimation extends StatefulWidget {
final WeatherType weatherType;
final double size;
final Function()? onTap;
const WeatherAnimation({
Key? key,
required this.weatherType,
this.size = 300,
this.onTap,
}) : super(key: key);
State<WeatherAnimation> createState() => _WeatherAnimationState();
}
class _WeatherAnimationState extends State<WeatherAnimation> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late List<Particle> _particles;
final Random _random = Random();
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat();
_initParticles();
}
void _initParticles() {
_particles = [];
int count = widget.weatherType == WeatherType.rainy ? 100 : 50;
for (int i = 0; i < count; i++) {
_particles.add(Particle(
x: _random.nextDouble() * widget.size,
y: _random.nextDouble() * widget.size,
size: widget.weatherType == WeatherType.rainy
? _random.nextDouble() * 2 + 1
: _random.nextDouble() * 3 + 2,
speed: widget.weatherType == WeatherType.rainy
? _random.nextDouble() * 3 + 2
: _random.nextDouble() * 1 + 0.5,
opacity: _random.nextDouble() * 0.7 + 0.3,
));
}
}
void dispose() {
_controller.dispose();
super.dispose();
}
void didUpdateWidget(covariant WeatherAnimation oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.weatherType != widget.weatherType || oldWidget.size != widget.size) {
_initParticles();
}
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
_updateParticles();
return CustomPaint(
size: Size(widget.size, widget.size),
painter: WeatherPainter(
weatherType: widget.weatherType,
particles: _particles,
),
);
},
),
);
}
void _updateParticles() {
for (var particle in _particles) {
if (widget.weatherType == WeatherType.rainy) {
// 雨滴:快速下落
particle.y += particle.speed * 5;
if (particle.y > widget.size) {
particle.y = 0;
particle.x = _random.nextDouble() * widget.size;
}
} else if (widget.weatherType == WeatherType.snowy) {
// 雪花:缓慢飘落,带点左右摆动
particle.y += particle.speed;
particle.x += sin(particle.y * 0.01) * 0.5;
if (particle.y > widget.size) {
particle.y = 0;
particle.x = _random.nextDouble() * widget.size;
}
}
}
}
}
class Particle {
double x;
double y;
double size;
double speed;
double opacity;
Particle({
required this.x,
required this.y,
required this.size,
required this.speed,
required this.opacity,
});
}
class WeatherPainter extends CustomPainter {
final WeatherType weatherType;
final List<Particle> particles;
WeatherPainter({
required this.weatherType,
required this.particles,
});
void paint(Canvas canvas, Size size) {
// 绘制背景
_drawBackground(canvas, size);
// 绘制天气元素
if (weatherType == WeatherType.rainy) {
_drawRain(canvas);
} else if (weatherType == WeatherType.snowy) {
_drawSnow(canvas);
} else if (weatherType == WeatherType.sunny) {
_drawSun(canvas, size);
}
}
void _drawBackground(Canvas canvas, Size size) {
Color backgroundColor;
switch (weatherType) {
case WeatherType.sunny:
backgroundColor = Colors.blue.shade300;
break;
case WeatherType.rainy:
backgroundColor = Colors.grey.shade700;
break;
case WeatherType.snowy:
backgroundColor = Colors.grey.shade200;
break;
}
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = backgroundColor,
);
}
void _drawSun(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
// 绘制太阳
canvas.drawCircle(
center,
40,
Paint()..color = Colors.yellow,
);
// 绘制太阳光芒
for (int i = 0; i < 8; i++) {
double angle = (i * pi / 4);
double x = cos(angle) * 60;
double y = sin(angle) * 60;
canvas.drawLine(
center,
Offset(center.dx + x, center.dy + y),
Paint()
..color = Colors.yellow
..strokeWidth = 3
..strokeCap = StrokeCap.round,
);
}
}
void _drawRain(Canvas canvas) {
for (var particle in particles) {
canvas.drawLine(
Offset(particle.x, particle.y),
Offset(particle.x, particle.y + particle.size * 5),
Paint()
..color = Colors.blue.withOpacity(particle.opacity)
..strokeWidth = particle.size
..strokeCap = StrokeCap.round,
);
}
}
void _drawSnow(Canvas canvas) {
for (var particle in particles) {
canvas.drawCircle(
Offset(particle.x, particle.y),
particle.size,
Paint()
..color = Colors.white.withOpacity(particle.opacity)
..style = PaintingStyle.fill,
);
}
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
实现要点:
-
天气类型定义:使用
enum WeatherType定义晴天、雨天、雪天三种天气类型。 -
粒子系统:使用
List<Particle>管理雨滴和雪花的属性,包括位置、大小、速度和透明度。 -
动画控制:使用
AnimationController控制动画帧更新,通过_updateParticles()方法更新粒子位置。 -
自定义绘制:通过
WeatherPainter自定义绘制天气效果,包括背景色、太阳、雨滴和雪花。 -
天气切换:通过
didUpdateWidget监听天气类型变化,自动重新初始化粒子系统。 -
交互支持:使用
GestureDetector包装动画组件,支持点击回调功能。 -
资源管理:在
dispose方法中释放动画控制器,避免内存泄漏。
WeatherAnimationExample 示例组件
WeatherAnimationExample 组件展示了如何在实际场景中使用 WeatherAnimation,包含了天气类型切换的交互效果。
实现代码:
class WeatherAnimationExample extends StatefulWidget {
const WeatherAnimationExample({Key? key}) : super(key: key);
State<WeatherAnimationExample> createState() => _WeatherAnimationExampleState();
}
class _WeatherAnimationExampleState extends State<WeatherAnimationExample> {
WeatherType _currentWeather = WeatherType.sunny;
void _toggleWeather() {
setState(() {
switch (_currentWeather) {
case WeatherType.sunny:
_currentWeather = WeatherType.rainy;
break;
case WeatherType.rainy:
_currentWeather = WeatherType.snowy;
break;
case WeatherType.snowy:
_currentWeather = WeatherType.sunny;
break;
}
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('天气动画示例'),
centerTitle: true,
backgroundColor: Colors.blue,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'天气动画示例',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 40),
// 天气动画
WeatherAnimation(
weatherType: _currentWeather,
size: 300,
onTap: _toggleWeather,
),
SizedBox(height: 30),
// 天气信息
Text(
_getWeatherText(_currentWeather),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
// 提示信息
Text(
'点击动画区域切换天气',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
),
);
}
String _getWeatherText(WeatherType weatherType) {
switch (weatherType) {
case WeatherType.sunny:
return '晴天';
case WeatherType.rainy:
return '雨天';
case WeatherType.snowy:
return '雪天';
}
}
}
使用场景:
-
天气应用:作为天气应用的核心视觉元素,展示当前天气状况。
-
交互式演示:作为交互式演示组件,展示不同天气的视觉效果。
-
游戏场景:作为游戏中的天气效果,增强游戏的沉浸感。
-
教育应用:作为教育应用中的天气教学元素,帮助理解不同天气现象。
首页集成实现
在 main.dart 文件中,我们将天气动画直接集成到首页,无需通过按钮跳转即可展示效果。
集成代码:
import 'package:flutter/material.dart';
import 'components/weather_animation.dart';
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> {
WeatherType _currentWeather = WeatherType.sunny;
void _toggleWeather() {
setState(() {
switch (_currentWeather) {
case WeatherType.sunny:
_currentWeather = WeatherType.rainy;
break;
case WeatherType.rainy:
_currentWeather = WeatherType.snowy;
break;
case WeatherType.snowy:
_currentWeather = WeatherType.sunny;
break;
}
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
centerTitle: true,
backgroundColor: Colors.blue,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'天气动画示例',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 40),
// 天气动画
WeatherAnimation(
weatherType: _currentWeather,
size: 300,
onTap: _toggleWeather,
),
SizedBox(height: 30),
// 天气信息
Text(
_getWeatherText(_currentWeather),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
// 提示信息
Text(
'点击动画区域切换天气',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
),
);
}
String _getWeatherText(WeatherType weatherType) {
switch (weatherType) {
case WeatherType.sunny:
return '晴天';
case WeatherType.rainy:
return '雨天';
case WeatherType.snowy:
return '雪天';
}
}
}
集成要点:
-
组件导入:通过
import 'components/weather_animation.dart'导入天气动画组件。 -
状态管理:使用
_currentWeather变量管理当前天气类型,通过_toggleWeather方法切换天气。 -
布局优化:使用
Column和SizedBox实现合理的间距和布局结构。 -
交互实现:通过
onTap回调将天气动画与天气切换功能关联,实现点击反馈。 -
信息展示:添加天气信息显示和操作提示,提高用户体验。
开发中容易遇到的问题
1. 粒子系统性能优化
问题描述:在使用大量粒子模拟天气效果时,可能会影响应用性能,导致动画卡顿。
解决方案:
- 合理控制粒子数量,雨天使用 100 个粒子,雪天使用 50 个粒子。
- 使用
AnimatedBuilder避免不必要的 UI 重建。 - 确保
shouldRepaint方法正确返回重绘状态,避免过度绘制。
示例代码:
// 控制粒子数量
int count = widget.weatherType == WeatherType.rainy ? 100 : 50;
// 使用 AnimatedBuilder
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
_updateParticles();
return CustomPaint(
// ...
);
},
);
2. 天气切换时的状态管理
问题描述:天气类型切换时,粒子系统可能没有正确更新,导致动画效果异常。
解决方案:
- 重写
didUpdateWidget方法,监听天气类型和大小变化。 - 在天气类型变化时,重新初始化粒子系统,确保粒子属性与当前天气匹配。
示例代码:
void didUpdateWidget(covariant WeatherAnimation oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.weatherType != widget.weatherType || oldWidget.size != widget.size) {
_initParticles();
}
}
3. 动画控制器生命周期管理
问题描述:在使用 AnimationController 时,容易忘记在组件销毁时释放资源,导致内存泄漏。
解决方案:
- 必须在
dispose方法中调用_controller.dispose()释放动画控制器。 - 使用
late关键字延迟初始化动画控制器,确保在initState中正确初始化。
示例代码:
void dispose() {
_controller.dispose();
super.dispose();
}
4. 天气效果真实性
问题描述:天气动画效果可能不够真实,与实际天气现象有差异。
解决方案:
- 雨滴效果:使用直线绘制,快速垂直下落,密度较大。
- 雪花效果:使用圆形绘制,缓慢飘落并带有左右摆动,密度较小。
- 晴天效果:绘制黄色太阳和光芒,背景为蓝色。
- 背景色:根据天气类型设置不同的背景色,增强视觉效果。
示例代码:
// 雨滴下落
particle.y += particle.speed * 5;
// 雪花飘落带摆动
particle.y += particle.speed;
particle.x += sin(particle.y * 0.01) * 0.5;
5. 跨平台兼容性
问题描述:在不同平台上,天气动画效果可能不一致,特别是在鸿蒙平台上。
解决方案:
- 使用 Flutter 跨平台 API,避免使用平台特定的功能。
- 确保
CustomPaint和AnimationController在鸿蒙平台上正常工作。 - 测试不同平台上的动画效果,确保一致性。
示例代码:
// 使用 Flutter 跨平台 API
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
// 避免平台特定代码
总结开发中用到的技术点
1. Flutter 动画系统
- AnimationController:核心动画控制器,用于控制动画的持续时间和重复方式。
- AnimatedBuilder:监听动画变化并重建 UI,是实现自定义动画效果的强大工具。
- TickerProviderStateMixin:提供动画帧调度,确保动画流畅运行。
2. 自定义绘制
- CustomPaint:用于自定义绘制图形和动画效果。
- CustomPainter:通过
paint方法实现具体的绘制逻辑。 - Canvas:提供绘制操作,如绘制线条、圆形和矩形。
- Paint:定义绘制的样式,如颜色、线条宽度和填充方式。
3. 粒子系统
- 粒子管理:使用
List<Particle>管理大量粒子的属性。 - 粒子更新:通过
_updateParticles()方法更新粒子位置和状态。 - 粒子渲染:根据粒子属性绘制不同类型的天气元素。
4. 状态管理
- StatefulWidget:用于管理包含动画和天气状态的组件。
- setState:用于更新组件状态,触发 UI 重绘。
- late 关键字:用于延迟初始化动画相关变量,提高代码可读性。
- didUpdateWidget:监听组件属性变化,更新内部状态。
5. 布局与交互
- Column:垂直布局组件,用于组织多个天气动画元素。
- SizedBox:用于控制组件间距,提高布局美观度。
- GestureDetector:用于实现点击交互功能,支持
onTap回调。 - Center:用于居中对齐子组件,确保动画在屏幕中央显示。
6. 组件化开发
- 抽离独立组件:将天气动画封装为独立的
WeatherAnimation组件,提高代码复用性。 - 参数化设计:通过构造函数参数,使组件具有良好的可配置性。
- 示例组件:创建
WeatherAnimationExample组件,展示组件的使用方法和效果。 - 首页集成:直接在首页集成动画效果,无需额外跳转。
7. 性能与内存管理
- 资源释放:在
dispose方法中释放动画控制器,避免内存泄漏。 - 粒子数量控制:根据天气类型合理控制粒子数量,优化性能。
- 绘制优化:使用
AnimatedBuilder和合理的shouldRepaint实现,避免过度绘制。
8. Flutter for OpenHarmony 跨平台开发
- 代码结构:遵循 Flutter 标准项目结构,确保代码在鸿蒙平台上的兼容性。
- 平台适配:使用 Flutter 跨平台 API,确保动画效果在鸿蒙平台上的一致性。
- 直接集成:将动画组件直接集成到首页,无需额外的平台特定代码。
- 资源管理:正确管理动画资源,确保在鸿蒙平台上的性能表现。
通过本次实战开发,我们不仅实现了一个功能完整、交互友好的天气动画组件,还掌握了 Flutter 自定义绘制和粒子系统的核心原理和最佳实践。这些技术点不仅适用于鸿蒙平台,也可以直接应用到 Android 和 iOS 平台的开发中,体现了 Flutter 跨平台开发的优势。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)