You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
832 lines
26 KiB
832 lines
26 KiB
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
/// 消息气泡组件
|
|
class MessageBubble extends StatelessWidget {
|
|
final bool isUser;
|
|
final String message;
|
|
final DateTime timestamp;
|
|
final bool isRecognizing;
|
|
|
|
const MessageBubble({
|
|
Key? key,
|
|
required this.isUser,
|
|
required this.message,
|
|
required this.timestamp,
|
|
this.isRecognizing = false,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final timeFormat = DateFormat('HH:mm');
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (!isUser) _buildAvatar(isUser, context),
|
|
const SizedBox(width: 8),
|
|
Flexible(
|
|
child: Column(
|
|
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: _getBubbleColor(context),
|
|
borderRadius: BorderRadius.circular(18).copyWith(
|
|
bottomLeft: isUser ? const Radius.circular(18) : const Radius.circular(4),
|
|
bottomRight: !isUser ? const Radius.circular(18) : const Radius.circular(4),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 2,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: _buildMessageContent(context),
|
|
),
|
|
const SizedBox(height: 4),
|
|
// 识别中的消息不显示时间
|
|
if (!isRecognizing)
|
|
Text(
|
|
timeFormat.format(timestamp),
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: Colors.grey[500],
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
if (isUser) _buildAvatar(isUser, context),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 获取气泡颜色
|
|
Color _getBubbleColor(BuildContext context) {
|
|
if (isUser) {
|
|
return isRecognizing
|
|
? Color(0xFF3B82F6).withOpacity(0.8) // 识别中的用户消息使用半透明的蓝色
|
|
: Color(0xFF3B82F6); // 使用更明亮的蓝色
|
|
} else {
|
|
return Color(0xFFF1F5F9); // 更淡的灰色背景
|
|
}
|
|
}
|
|
|
|
/// 构建头像
|
|
Widget _buildAvatar(bool isUser, BuildContext context) {
|
|
return Container(
|
|
width: 36,
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: isUser ? Colors.grey[300] : Theme.of(context).primaryColor,
|
|
borderRadius: BorderRadius.circular(18),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 2,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
isUser ? Icons.person : Icons.smart_toy_outlined,
|
|
color: isUser ? Colors.grey[700] : Colors.white,
|
|
size: 20,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建消息内容
|
|
Widget _buildMessageContent(BuildContext context) {
|
|
// 如果是识别中的消息,添加特殊样式
|
|
if (isRecognizing) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Flexible(
|
|
child: Text(
|
|
message,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontStyle: FontStyle.italic,
|
|
fontSize: 14,
|
|
height: 1.4,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_buildTypingIndicator(),
|
|
],
|
|
);
|
|
}
|
|
|
|
// 判断消息内容,显示特殊卡片
|
|
if (!isUser) {
|
|
if (message.contains('播放列表') || message.contains('音乐')) {
|
|
return _buildMusicContent(context);
|
|
} else if (message.contains('景点') || message.contains('旅游')) {
|
|
return _buildTravelContent(context);
|
|
} else if (message.contains('天气') || message.contains('气温') || message.contains('预报')) {
|
|
return _buildWeatherContent(context);
|
|
}
|
|
}
|
|
|
|
return Text(
|
|
message,
|
|
style: TextStyle(
|
|
color: isUser ? Colors.white : Colors.black87,
|
|
fontSize: 14,
|
|
height: 1.4,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建输入中的指示器
|
|
Widget _buildTypingIndicator() {
|
|
return SizedBox(
|
|
width: 40,
|
|
height: 20,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
_buildDot(100),
|
|
_buildDot(300),
|
|
_buildDot(500),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建动画点
|
|
Widget _buildDot(int milliseconds) {
|
|
return _AnimatedDot(delayMilliseconds: milliseconds);
|
|
}
|
|
|
|
/// 构建音乐内容卡片
|
|
Widget _buildMusicContent(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'以下是一些推荐的音乐:',
|
|
style: TextStyle(
|
|
color: Colors.black87,
|
|
fontSize: 14,
|
|
height: 1.4,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 3,
|
|
spreadRadius: 1,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.music_note,
|
|
size: 16,
|
|
color: Theme.of(context).primaryColor.withOpacity(0.7),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'雨天时光 · 播放列表',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
// 歌曲示例
|
|
_buildMusicItem('雨声与钢琴曲', '纯音乐', context),
|
|
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
|
|
_buildMusicItem('Rainy Day', 'Coldplay', context),
|
|
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
|
|
_buildMusicItem('雨天的爵士咖啡馆', '爵士合集', context),
|
|
|
|
const SizedBox(height: 8),
|
|
Center(
|
|
child: TextButton(
|
|
onPressed: () {},
|
|
style: TextButton.styleFrom(
|
|
minimumSize: Size.zero,
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
),
|
|
child: Text(
|
|
'查看更多推荐',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Theme.of(context).primaryColor,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: Icon(Icons.play_arrow, size: 16),
|
|
label: Text('播放全部'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Theme.of(context).primaryColor,
|
|
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
textStyle: TextStyle(fontSize: 12),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: Icon(Icons.shuffle, size: 16),
|
|
label: Text('随机播放'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.grey[700],
|
|
side: BorderSide(color: Colors.grey[400]!),
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
textStyle: TextStyle(fontSize: 12),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 构建旅游内容卡片
|
|
Widget _buildTravelContent(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'以下是一些推荐的景点:',
|
|
style: TextStyle(
|
|
color: Colors.black87,
|
|
fontSize: 14,
|
|
height: 1.4,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 3,
|
|
spreadRadius: 1,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.place,
|
|
size: 16,
|
|
color: Color(0xFFE11D48),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'热门景点 · 东京',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
// 景点列表
|
|
_buildLocationItem('东京塔', '标志性建筑,可俯瞰东京全景', context),
|
|
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
|
|
_buildLocationItem('涩谷十字路口', '世界上最繁忙的十字路口之一', context),
|
|
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
|
|
_buildLocationItem('浅草寺', '东京最古老的寺庙', context),
|
|
|
|
const SizedBox(height: 8),
|
|
Center(
|
|
child: TextButton(
|
|
onPressed: () {},
|
|
style: TextButton.styleFrom(
|
|
minimumSize: Size.zero,
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
),
|
|
child: Text(
|
|
'查看更多景点',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Theme.of(context).primaryColor,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: Icon(Icons.map, size: 16),
|
|
label: Text('查看地图'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Theme.of(context).primaryColor,
|
|
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
textStyle: TextStyle(fontSize: 12),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: Icon(Icons.bookmark_border, size: 16),
|
|
label: Text('保存到收藏'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.grey[700],
|
|
side: BorderSide(color: Colors.grey[400]!),
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
textStyle: TextStyle(fontSize: 12),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 构建音乐项目
|
|
Widget _buildMusicItem(String title, String artist, BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 38,
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
Icons.music_note,
|
|
size: 18,
|
|
color: Theme.of(context).primaryColor,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[800],
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 2),
|
|
Text(
|
|
artist,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {},
|
|
icon: Icon(Icons.play_circle, color: Theme.of(context).primaryColor),
|
|
padding: EdgeInsets.zero,
|
|
constraints: BoxConstraints(),
|
|
iconSize: 24,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建地点项目
|
|
Widget _buildLocationItem(String name, String description, BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 38,
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue[50],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
Icons.place,
|
|
size: 18,
|
|
color: Color(0xFFE11D48),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
name,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[800],
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 2),
|
|
Text(
|
|
description,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {},
|
|
icon: Icon(Icons.arrow_forward, color: Colors.grey[400]),
|
|
padding: EdgeInsets.zero,
|
|
constraints: BoxConstraints(),
|
|
iconSize: 20,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建天气内容卡片
|
|
Widget _buildWeatherContent(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'当前天气情况:',
|
|
style: TextStyle(
|
|
color: Colors.black87,
|
|
fontSize: 14,
|
|
height: 1.4,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)],
|
|
),
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'北京',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
SizedBox(height: 4),
|
|
Text(
|
|
'晴朗',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'25°',
|
|
style: TextStyle(
|
|
fontSize: 36,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'最高: 29°',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
Text(
|
|
'最低: 18°',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
Icon(
|
|
Icons.wb_sunny,
|
|
size: 48,
|
|
color: Colors.white,
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 16),
|
|
Container(
|
|
height: 1,
|
|
color: Colors.white.withOpacity(0.2),
|
|
),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'未来预报',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
SizedBox(height: 8),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
_buildForecastDay('周一', Icons.wb_sunny, '28°'),
|
|
_buildForecastDay('周二', Icons.cloud, '25°'),
|
|
_buildForecastDay('周三', Icons.wb_cloudy, '26°'),
|
|
_buildForecastDay('周四', Icons.grain, '24°'),
|
|
_buildForecastDay('周五', Icons.wb_sunny, '27°'),
|
|
],
|
|
),
|
|
SizedBox(height: 16),
|
|
Container(
|
|
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.info_outline,
|
|
size: 16,
|
|
color: Colors.white,
|
|
),
|
|
SizedBox(width: 6),
|
|
Flexible(
|
|
child: Text(
|
|
'今日紫外线指数较高,外出请做好防晒',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: Icon(Icons.location_on, size: 16),
|
|
label: Text('更多地区'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Theme.of(context).primaryColor,
|
|
side: BorderSide(color: Theme.of(context).primaryColor.withOpacity(0.5)),
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
textStyle: TextStyle(fontSize: 12),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {},
|
|
icon: Icon(Icons.calendar_today, size: 16),
|
|
label: Text('15天预报'),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.grey[700],
|
|
side: BorderSide(color: Colors.grey[400]!),
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
textStyle: TextStyle(fontSize: 12),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 构建单日天气预报
|
|
Widget _buildForecastDay(String day, IconData icon, String temp) {
|
|
return Column(
|
|
children: [
|
|
Text(
|
|
day,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
SizedBox(height: 4),
|
|
Icon(
|
|
icon,
|
|
size: 20,
|
|
color: Colors.white,
|
|
),
|
|
SizedBox(height: 4),
|
|
Text(
|
|
temp,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 动画点组件
|
|
class _AnimatedDot extends StatefulWidget {
|
|
final int delayMilliseconds;
|
|
|
|
const _AnimatedDot({required this.delayMilliseconds});
|
|
|
|
@override
|
|
_AnimatedDotState createState() => _AnimatedDotState();
|
|
}
|
|
|
|
class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1500),
|
|
);
|
|
|
|
_animation = Tween<double>(begin: 0, end: 6).animate(
|
|
CurvedAnimation(
|
|
parent: _controller,
|
|
curve: Interval(0.0, 0.5, curve: Curves.easeOut),
|
|
reverseCurve: Interval(0.5, 1.0, curve: Curves.easeIn),
|
|
),
|
|
)..addListener(() {
|
|
setState(() {});
|
|
});
|
|
|
|
Future.delayed(Duration(milliseconds: widget.delayMilliseconds), () {
|
|
if (mounted) {
|
|
_controller.repeat();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: 6,
|
|
height: 6,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(3),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.white.withOpacity(0.5),
|
|
blurRadius: _animation.value,
|
|
spreadRadius: _animation.value / 2,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|