Flutter for OpenHarmony智能天气APP实战DAY6:OpenHarmony智能天气APP开发之App UI 视觉优化设计与实现
在完成 Flutter for OpenHarmony 天气预报 App 核心功能(自动定位、城市切换、实时天气、未来 7 天预报)后,原生界面样式较为朴素,缺乏质感与视觉层次,加载、空状态展示简陋,温度与日期信息辨识度不高。本文专门针对UI 视觉进行四大专项优化:卡片渐变阴影升级、温度冷暖色区分、日期转为中文星期、加载与空状态美化,全程遵循鸿蒙系统简约圆润的设计风格,不改动原有业务逻辑,只做界面
Flutter for OpenHarmony智能天气APP实战DAY6:OpenHarmony智能天气APP开发之App UI 视觉优化设计与实现
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
前言
基于 Flutter for OpenHarmony 开发的天气预报应用,已完成自动 IP 定位、多城市切换、实时天气、未来 7 天预报等核心业务功能。但初始版本 UI 界面设计偏基础朴素,存在卡片无层次、视觉区分度低、日期展示不友好、加载空状态简陋等问题,整体美观度与鸿蒙系统原生设计质感差距较大。
为提升项目完成度、适配 OpenHarmony 扁平化轻奢设计规范,本文从卡片视觉质感、温度数据色彩可视化、日期人文格式化、全局状态页定制美化四大维度进行深度 UI 视觉优化。
全程不修改原有业务逻辑、不新增第三方依赖,仅通过原生 Flutter 组件实现高阶界面效果,适配鸿蒙模拟器与真机
一、优化目标
- 给 7 天预报卡片、实时信息卡片添加渐变色 + 阴影,提升悬浮质感,适配鸿蒙设计风格;
- 最高温、最低温采用冷暖色调区分,数据一目了然;
- 未来 7 天预报日期由年月日改为周一、周二… 中文星期格式;
- 美化加载中、网络异常、无数据空状态,替换默认简陋样式,增加图标与友好文案。
二、卡片样式升级:渐变 + 阴影 鸿蒙质感优化
核心代码实现
Container(
width: 115,
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
// 圆角适配鸿蒙圆润设计
borderRadius: BorderRadius.circular(14),
// 半透明白色渐变
gradient: LinearGradient(
colors: [
Colors.white.withValues(alpha: 0.25),
Colors.white.withValues(alpha: 0.05)
],
),
// 卡片阴影 营造悬浮立体效果
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 6,
offset: Offset(1, 2),
),
],
),
child: // 卡片内部布局内容
)
优化亮点
- 渐变柔光过渡,不刺眼、高级感强;
- 阴影轻微虚化,实现卡片悬浮效果;
- 统一圆角半径,和鸿蒙系统 UI 风格保持一致。
三、温度颜色区分:冷暖色调可视化
核心代码实现
// 最高温 - 暖色调橙色
Text(
"${day['max']}°",
style: TextStyle(
color: Colors.orangeAccent,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
// 最低温 - 冷色调浅蓝色
Text(
"${day['min']}°",
style: TextStyle(
color: Colors.lightBlueAccent,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
优化亮点
- 不用仔细看数字,看颜色就能分清高温、低温,符合大众视觉认知习惯。
四、日期显示优化:转为中文星期格式
核心工具方法
// 日期字符串 转 中文星期
String getWeekDay(String dateStr) {
DateTime date = DateTime.parse(dateStr);
List<String> weekList = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
return weekList[date.weekday - 1];
}
页面调用展示
Text(
getWeekDay(day['date']),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
)
优化亮点
- 界面更简洁清爽,用户一眼识别是周几的天气,符合日常使用习惯。
五、加载与空状态全局美化
5.1 加载中状态美化
isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
SizedBox(height: 20),
Text(
"正在获取天气数据...",
style: TextStyle(color: Colors.white, fontSize: 16),
),
],
)
5.2 网络异常状态美化
errorMsg.isNotEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.wifi_off, color: Colors.white, size: 60),
const SizedBox(height: 15),
Text(
errorMsg,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// 重新请求天气逻辑
},
child: const Text("重新加载"),
),
],
)
5.3 无预报空状态美化
dailyForecast == null
? const Text(
"暂无预报数据",
style: TextStyle(color: Colors.white, fontSize: 16),
)
优化亮点
- 全场景状态页面视觉统一,有图标、有文案、有操作入口,不再生硬简陋,极大提升用户体验。
六、完整修改后的main.dart代码(可直接复制)
以下是优化后的完整代码,无需修改任何配置文件,直接替换原有main.dart即可,确保鸿蒙模拟器零报错运行。
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'dart:convert';
import 'package:intl/intl.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '鸿蒙天气预报',
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.light,
),
home: const WeatherPage(),
debugShowCheckedModeBanner: false,
);
}
}
class WeatherPage extends StatefulWidget {
const WeatherPage({super.key});
@override
State<WeatherPage> createState() => _WeatherPageState();
}
class _WeatherPageState extends State<WeatherPage> {
Map<String, dynamic>? weatherData;
List<dynamic>? dailyForecast;
bool isLoading = true;
String errorMsg = '';
String currentCityName = "定位中...";
final List<Map<String, dynamic>> cities = [
{"name": "北京", "lat": 39.9042, "lon": 116.4074},
{"name": "上海", "lat": 31.2304, "lon": 121.4737},
{"name": "广州", "lat": 23.1291, "lon": 113.2644},
{"name": "深圳", "lat": 22.5431, "lon": 114.0579},
{"name": "杭州", "lat": 30.2741, "lon": 120.1551},
{"name": "成都", "lat": 30.5723, "lon": 104.0665},
{"name": "重庆", "lat": 29.5630, "lon": 106.5516},
];
late Map<String, dynamic> selectedCity;
@override
void initState() {
super.initState();
selectedCity = cities[0];
getLocationAndWeather();
}
Future<void> getLocationAndWeather() async {
try {
final ipResponse = await get(Uri.parse('https://api.ipify.org?format=json'));
final ip = json.decode(ipResponse.body)['ip'];
final locResponse = await get(Uri.parse('http://ip-api.com/json/$ip?lang=zh-CN'));
final data = json.decode(locResponse.body);
double lat = data['lat'] ?? 39.9042;
double lon = data['lon'] ?? 116.4074;
String city = data['city'] ?? "北京";
setState(() {
currentCityName = city;
selectedCity = {"name": city, "lat": lat, "lon": lon};
});
fetchWeatherData();
} catch (e) {
setState(() {
currentCityName = "北京";
selectedCity = cities[0];
});
fetchWeatherData();
}
}
String get apiUrl {
return 'https://api.open-meteo.com/v1/forecast?latitude=${selectedCity['lat']}&longitude=${selectedCity['lon']}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=Asia/Shanghai&forecast_days=8';
}
void changeCity(Map<String, dynamic> city) {
setState(() {
selectedCity = city;
currentCityName = city['name'];
isLoading = true;
errorMsg = '';
});
fetchWeatherData();
}
Future<void> fetchWeatherData() async {
try {
final response = await get(Uri.parse(apiUrl));
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
weatherData = data['current'];
dailyForecast = data['daily']?['time'] != null
? List.generate(data['daily']['time'].length, (index) {
return {
"date": data['daily']['time'][index],
"max": data['daily']['temperature_2m_max'][index],
"min": data['daily']['temperature_2m_min'][index],
"code": data['daily']['weathercode'][index],
};
})
: null;
isLoading = false;
});
} else {
setState(() {
errorMsg = "数据请求失败";
isLoading = false;
});
}
} catch (e) {
setState(() {
errorMsg = "网络异常,请检查连接";
isLoading = false;
});
}
}
Widget weatherIcon(int code, double size) {
IconData icon;
Color color;
if (code == 0) {
icon = Icons.wb_sunny;
color = Colors.orange;
} else if (code >= 1 && code <= 3) {
icon = Icons.cloud;
color = Colors.grey;
} else if (code >= 45 && code <= 48) {
icon = Icons.foggy;
color = Colors.blueGrey;
} else if (code >= 51 && code <= 67) {
icon = Icons.water_drop;
color = Colors.blue;
} else if (code >= 71 && code <= 77) {
icon = Icons.snowing;
color = Colors.lightBlue;
} else if (code >= 80 && code <= 82) {
icon = Icons.thunderstorm;
color = Colors.deepPurple;
} else {
icon = Icons.cloud;
color = Colors.grey;
}
return Icon(icon, color: color, size: size);
}
String getWeekDay(String dateStr) {
DateTime date = DateTime.parse(dateStr);
List<String> weeks = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
return weeks[date.weekday - 1];
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF64B5F6), Color(0xFFBBDEFB)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
SizedBox(height: 20),
Text(
"正在获取天气数据...",
style: TextStyle(color: Colors.white, fontSize: 16),
),
],
),
)
: errorMsg.isNotEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.wifi_off, color: Colors.white, size: 60),
const SizedBox(height: 15),
Text(
errorMsg,
style: const TextStyle(color: Colors.white, fontSize: 16),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
isLoading = true;
errorMsg = '';
});
getLocationAndWeather();
},
child: const Text("重新加载"),
),
],
),
)
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50),
child: Column(
children: [
const Text(
"鸿蒙天气预报",
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 10),
Text(
"当前城市:$currentCityName",
style: const TextStyle(
fontSize: 17,
color: Colors.white70,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 15),
// 修复 withOpacity 警告:改用 withValues
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Map<String, dynamic>>(
dropdownColor: Colors.blue[400],
style: const TextStyle(color: Colors.white, fontSize: 16),
value: selectedCity,
items: cities.map((c) {
return DropdownMenuItem(
value: c,
child: Text(c['name']),
);
}).toList(),
onChanged: (v) {
if (v != null) changeCity(v);
},
),
),
),
const SizedBox(height: 25),
Text(
DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()),
style: const TextStyle(color: Colors.white70),
),
const SizedBox(height: 25),
weatherIcon(weatherData!['weather_code'], 100),
const SizedBox(height: 10),
Text(
'${weatherData!['temperature_2m']}°C',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [
Colors.white.withValues(alpha: 0.3),
Colors.white.withValues(alpha: 0.1)
],
),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: const Offset(2, 4),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_infoItem("湿度", "${weatherData!['relative_humidity_2m']}%"),
_infoItem("体感温度", "${weatherData!['apparent_temperature']}°C"),
],
),
),
const SizedBox(height: 30),
const Text(
"📅 未来7天天气预报",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 15),
dailyForecast == null
? const Text(
"暂无预报数据",
style: TextStyle(color: Colors.white),
)
: SizedBox(
height: 170,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: dailyForecast!.length - 1,
itemBuilder: (context, index) {
final day = dailyForecast![index + 1];
return _forecastItem(day);
},
),
),
const SizedBox(height: 40),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
onPressed: () {
setState(() {
isLoading = true;
currentCityName = "定位中...";
});
getLocationAndWeather();
},
child: const Text("重新定位", style: TextStyle(fontSize: 16)),
),
],
),
),
),
);
}
Widget _infoItem(String title, String value) {
return Column(
children: [
Text(
title,
style: const TextStyle(color: Colors.white70, fontSize: 15),
),
const SizedBox(height: 5),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _forecastItem(Map day) {
return Container(
width: 115,
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
gradient: LinearGradient(
colors: [
Colors.white.withValues(alpha: 0.25),
Colors.white.withValues(alpha: 0.05)
],
),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 6,
offset: const Offset(1, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
getWeekDay(day['date']),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
weatherIcon(day['code'], 30),
const SizedBox(height: 8),
Text(
"${day['max']}°",
style: const TextStyle(
color: Colors.orangeAccent,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
Text(
"${day['min']}°",
style: const TextStyle(
color: Colors.lightBlueAccent,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
],
),
);
}
}
优化后的效果图:
整体优化总结
本次 UI 视觉优化完全基于原有业务逻辑,不改动接口、不改动功能,只做界面质感升级:
1.卡片渐变 + 阴影,复刻鸿蒙悬浮轻奢风格,界面层次感拉满;
2.高低温冷暖配色,数据可视化,阅读更直观;
3.日期转为中文星期,简洁贴合国人使用习惯;
4.加载、异常、空状态全面美化,交互体验更完整。
所有代码低侵入、可直接复用,适配 Flutter for OpenHarmony 鸿蒙项目,可直接嵌入原有天气预报项目中使用。
更多推荐



所有评论(0)