
在移动应用设计中,信息展示的美感与效率往往决定了用户的留存意愿。天气应用作为最常见的一类工具,如何在简洁中体现个性、在静态中注入动态?答案在于——上下文感知的视觉反馈与流畅的交互过渡。
🌐 加入社区 欢迎加入 开源鸿蒙跨平台开发者社区,获取最新资源与技术支持: 👉 开源鸿蒙跨平台开发者社区
完整效果



该应用的核心思想是 “天气决定界面基调”:
💡 目标:让用户一眼感受到当前城市的天气氛围,而不仅是读取数据。
WeatherCondition 枚举:单一数据源原则enum WeatherCondition {
sunny, cloudy, rainy, stormy, snowy, foggy;
String get icon => ...;
String get description => ...;
Color get backgroundColor => ...;
Color get textColor => ...;
}
✅ 这是 “面向对象设计”在 Flutter 中的优雅实践。
CityWeatherclass CityWeather {
final String name;
final int temperature;
final WeatherCondition condition;
final int humidity;
final double windSpeed;
}
final 字段确保不可变性;AnimatedContainerAnimatedContainer(
duration: const Duration(milliseconds: 400),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
condition.backgroundColor.withOpacity(0.7),
condition.backgroundColor.withOpacity(0.95),
],
),
),
// ...
)
scaffoldBackgroundColor 的柔和底色。shadows: [
Shadow(color: Colors.black.withOpacity(0.1), offset: Offset(0, 2), blurRadius: 4)
]onHorizontalDragEnd: (details) {
if (details.primaryVelocity! > 0) _prevCity(); // 右滑
else if (details.primaryVelocity! < 0) _nextCity(); // 左滑
}
primaryVelocity 判断方向,避免误触。centerFloat):醒目且不遮挡内容;天气 | 背景色 | 文字色 | 心理暗示 |
|---|---|---|---|
☀️ 晴朗 | #FFF8E1(暖黄) | #5D4037(深棕) | 温暖、明亮、活力 |
⛅ 多云 | #ECEFF1(灰蓝) | #455A64(冷灰) | 平静、温和、中性 |
🌧️ 小雨 | #E3F2FD(浅蓝) | #0D47A1(深蓝) | 清凉、湿润、宁静 |
⛈️ 雷暴 | #BBDEFB(深蓝) | #01579B(更深蓝) | 强烈、能量、紧张 |
❄️ 小雪 | #E1F5FE(冰蓝) | #01579B | 寒冷、纯净、空灵 |
🌫️ 雾 | #E0E0E0(中灰) | #212121(纯黑) | 模糊、朦胧、神秘 |
🎨 所有配色均来自 Material Design 调色板,确保视觉和谐。
Wrap 布局:自动换行湿度与风速信息,适配不同屏幕宽度;Spacer():将底部指示器推至视图底部,保证主信息区垂直居中;Padding + SizedBox:精确控制元素间距,形成呼吸感。这个天气卡片应用虽小,却完整体现了 “数据驱动 UI” 的现代开发理念。通过枚举将业务逻辑与视觉表现解耦,通过动画赋予静态信息以生命力,通过手势与按钮提供自然的交互路径。
完整代码
import 'package:flutter/material.dart';
void main() {
runApp(const WeatherCardApp());
}
class WeatherCardApp extends StatelessWidget {
const WeatherCardApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: '🌤️ 天气卡片',
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.blue,
scaffoldBackgroundColor: const Color(0xFFE6F4FF),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
foregroundColor: Colors.blue,
elevation: 0,
),
),
home: const WeatherCardScreen(),
);
}
}
// 天气类型枚举
enum WeatherCondition {
sunny,
cloudy,
rainy,
stormy,
snowy,
foggy;
String get icon {
switch (this) {
case WeatherCondition.sunny:
return '☀️';
case WeatherCondition.cloudy:
return '⛅';
case WeatherCondition.rainy:
return '🌧️';
case WeatherCondition.stormy:
return '⛈️';
case WeatherCondition.snowy:
return '❄️';
case WeatherCondition.foggy:
return '🌫️';
}
}
String get description {
switch (this) {
case WeatherCondition.sunny:
return '晴朗';
case WeatherCondition.cloudy:
return '多云';
case WeatherCondition.rainy:
return '小雨';
case WeatherCondition.stormy:
return '雷暴';
case WeatherCondition.snowy:
return '小雪';
case WeatherCondition.foggy:
return '雾';
}
}
Color get backgroundColor {
switch (this) {
case WeatherCondition.sunny:
return const Color(0xFFFFF8E1); // 淡黄
case WeatherCondition.cloudy:
return const Color(0xFFECEFF1); // 浅灰蓝
case WeatherCondition.rainy:
return const Color(0xFFE3F2FD); // 雨天蓝
case WeatherCondition.stormy:
return const Color(0xFFBBDEFB); // 雷暴深蓝
case WeatherCondition.snowy:
return const Color(0xFFE1F5FE); // 雪天冰蓝
case WeatherCondition.foggy:
return const Color(0xFFE0E0E0); // 雾天灰
}
}
Color get textColor {
switch (this) {
case WeatherCondition.sunny:
return const Color(0xFF5D4037); // 深棕
case WeatherCondition.cloudy:
return const Color(0xFF455A64); // 灰蓝
case WeatherCondition.rainy:
return const Color(0xFF0D47A1); // 深蓝
case WeatherCondition.stormy:
return const Color(0xFF01579B); // 更深蓝
case WeatherCondition.snowy:
return const Color(0xFF01579B);
case WeatherCondition.foggy:
return const Color(0xFF212121); // 纯黑
}
}
}
// 城市数据模型
class CityWeather {
final String name;
final int temperature;
final WeatherCondition condition;
final int humidity;
final double windSpeed; // m/s
CityWeather({
required this.name,
required this.temperature,
required this.condition,
required this.humidity,
required this.windSpeed,
});
}
class WeatherCardScreen extends StatefulWidget {
const WeatherCardScreen({super.key});
@override
State<WeatherCardScreen> createState() => _WeatherCardScreenState();
}
class _WeatherCardScreenState extends State<WeatherCardScreen> {
// 预设 6 个虚拟城市天气(覆盖所有天气类型)
static final List<CityWeather> _cities = [
CityWeather(
name: '杭州',
temperature: 28,
condition: WeatherCondition.sunny,
humidity: 60,
windSpeed: 2.3),
CityWeather(
name: '成都',
temperature: 22,
condition: WeatherCondition.cloudy,
humidity: 75,
windSpeed: 1.8),
CityWeather(
name: '广州',
temperature: 31,
condition: WeatherCondition.rainy,
humidity: 85,
windSpeed: 3.1),
CityWeather(
name: '武汉',
temperature: 33,
condition: WeatherCondition.stormy,
humidity: 90,
windSpeed: 5.7),
CityWeather(
name: '哈尔滨',
temperature: -5,
condition: WeatherCondition.snowy,
humidity: 50,
windSpeed: 4.2),
CityWeather(
name: '重庆',
temperature: 25,
condition: WeatherCondition.foggy,
humidity: 88,
windSpeed: 0.9),
];
int _currentIndex = 0;
void _nextCity() {
setState(() {
_currentIndex = (_currentIndex + 1) % _cities.length;
});
}
void _prevCity() {
setState(() {
_currentIndex = (_currentIndex - 1 + _cities.length) % _cities.length;
});
}
@override
Widget build(BuildContext context) {
final city = _cities[_currentIndex];
final condition = city.condition;
return Scaffold(
appBar: AppBar(
title: const Text(
'今日天气',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: Colors.transparent,
),
body: GestureDetector(
onHorizontalDragEnd: (details) {
if (details.primaryVelocity! > 0) {
_prevCity(); // 右滑 → 上一个
} else if (details.primaryVelocity! < 0) {
_nextCity(); // 左滑 → 下一个
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 400),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
condition.backgroundColor.withOpacity(0.7),
condition.backgroundColor.withOpacity(0.95),
],
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 城市名
Text(
city.name,
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: condition.textColor,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.1),
offset: const Offset(0, 2),
blurRadius: 4,
),
],
),
),
const SizedBox(height: 12),
// 天气图标 + 描述
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
condition.icon,
style: const TextStyle(fontSize: 64),
),
const SizedBox(width: 16),
Text(
condition.description,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w500,
color: condition.textColor.withOpacity(0.9),
),
),
],
),
const SizedBox(height: 24),
// 温度
Text(
'${city.temperature}°',
style: TextStyle(
fontSize: 80,
fontWeight: FontWeight.bold,
color: condition.textColor,
height: 0.9,
),
),
const SizedBox(height: 16),
// 湿度 & 风速
Wrap(
spacing: 24,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
_buildDetailItem('💧 湿度', '${city.humidity}%'),
_buildDetailItem('💨 风速', '${city.windSpeed}m/s'),
],
),
const Spacer(),
// 切换指示器
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_cities.length, (index) {
return Container(
width: 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: index == _currentIndex
? Colors.white
: Colors.white.withOpacity(0.5),
),
);
}),
),
const SizedBox(height: 24),
],
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _nextCity,
backgroundColor: Colors.white,
foregroundColor: Colors.blue,
child: const Icon(Icons.navigate_next, size: 32),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
Widget _buildDetailItem(String label, String value) {
final city = _cities[_currentIndex];
return Column(
children: [
Text(
label,
style: TextStyle(
fontSize: 16,
color: city.condition.textColor.withOpacity(0.8),
),
),
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: city.condition.textColor,
),
),
],
);
}
}