增加悬浮图标的声波动画效果

This commit is contained in:
2025-08-12 17:40:39 +08:00
parent 4035804b73
commit 15680677fc
6 changed files with 153 additions and 36 deletions

BIN
assets/images/ai1_hd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
assets/images/ai2_hd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,9 +1,9 @@
import 'package:ai_chat_assistant/services/vehicle_state_service.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/message_service.dart';
import '../screens/full_screen.dart';
import '../screens/part_screen.dart';
import 'floating_icon_with_wave.dart';
class FloatingIcon extends StatefulWidget {
const FloatingIcon({super.key});
@@ -12,15 +12,28 @@ class FloatingIcon extends StatefulWidget {
State<FloatingIcon> createState() => _FloatingIconState();
}
class _FloatingIconState extends State<FloatingIcon> {
class _FloatingIconState extends State<FloatingIcon>
with SingleTickerProviderStateMixin {
Offset _position = const Offset(10, 120);
final iconSize = 80.0;
bool _isShowPartScreen = false;
late AnimationController _waveAnimationController;
@override
void initState() {
super.initState();
// VehicleStateService();
_waveAnimationController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
}
@override
void dispose() {
_waveAnimationController.dispose();
super.dispose();
}
void _showPartScreen() {
@@ -67,13 +80,30 @@ class _FloatingIconState extends State<FloatingIcon> {
right: _position.dx,
child: Consumer<MessageService>(
builder: (context, messageService, child) {
// 立即响应录音状态变化,控制动画
WidgetsBinding.instance.addPostFrameCallback((_) {
if (messageService.isRecording) {
if (!_waveAnimationController.isAnimating) {
_waveAnimationController.repeat();
}
} else {
if (_waveAnimationController.isAnimating) {
_waveAnimationController.stop();
}
}
});
return GestureDetector(
onTap: _showFullScreen,
onLongPress: () async {
_showPartScreen();
// 立即启动动画
_waveAnimationController.repeat();
await messageService.startVoiceInput();
},
onLongPressUp: () async {
// 立即停止动画
_waveAnimationController.stop();
await messageService.stopAndProcessVoiceInput();
final hasMessage = messageService.messages.isNotEmpty;
if (!hasMessage) {
@@ -81,13 +111,17 @@ class _FloatingIconState extends State<FloatingIcon> {
}
},
onPanUpdate: (details) => _updatePosition(details.delta),
child: Image.asset(
messageService.isRecording
? 'assets/images/ai2.png'
: 'assets/images/ai1.png',
width: iconSize,
height: iconSize,
),
child: messageService.isRecording
? FloatingIconWithWave(
animationController: _waveAnimationController,
iconSize: iconSize,
waveColor: Colors.white,
)
: Image.asset(
'assets/images/ai1_hd.png',
width: iconSize,
height: iconSize,
),
);
},
),

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'mini_audio_wave.dart';
/// 浮动图标与声波动画组合组件
///
/// 将ai_hd_clean.png图片和AudioWaveLargeMini声波效果组合成一个独立组件
class FloatingIconWithWave extends StatelessWidget {
/// 动画控制器,用于驱动声波动画
final AnimationController animationController;
/// 图标大小
final double iconSize;
/// 声波颜色,默认为白色
final Color waveColor;
/// 构造函数
const FloatingIconWithWave({
super.key,
required this.animationController,
required this.iconSize,
this.waveColor = Colors.white,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: iconSize,
height: iconSize,
child: Stack(
alignment: Alignment.center,
children: [
// 背景图片
Image.asset(
'assets/images/ai_hd_clean.png',
width: iconSize,
height: iconSize,
fit: BoxFit.contain,
),
// 声波动画距离底部13px的位置
Positioned(
bottom: 13 * (iconSize / 198), // 按比例计算距离底部的位置
child: SizedBox(
width: 96 * (iconSize / 144), // 最宽不超过96px按图片原始宽度比例
height: 27 * (iconSize / 198), // 最高不超过27px按图片原始高度比例
child: AudioWaveLargeMini(
animationController: animationController,
waveColor: waveColor,
barSpacing: 2.5, // 减小间距以适应更小的宽度
minHeightRatio: 0.4, // 调整最小高度比例
maxHeightRatio: 1.0, // 保持最大高度比例
),
),
),
],
),
);
}
}

View File

@@ -3,7 +3,7 @@ import 'dart:math' as math;
/// 小型按钮波形动画组件
///
/// 用于显示适合小按钮的波形动画,包含8个竖线动画效果由外部动画控制器驱动
/// 用于显示适合小按钮的波形动画,竖线数量根据容器宽度自动计算
class AudioWaveLargeMini extends StatelessWidget {
/// 动画控制器,用于驱动波形动画
final AnimationController animationController;
@@ -11,11 +11,23 @@ class AudioWaveLargeMini extends StatelessWidget {
/// 波形颜色,默认为白色
final Color waveColor;
/// 波形条间距默认为3.0px
final double barSpacing;
/// 最小高度比例默认为0.3(相对于容器高度)
final double minHeightRatio;
/// 最大高度比例默认为1.0(相对于容器高度)
final double maxHeightRatio;
/// 构造函数
const AudioWaveLargeMini({
super.key,
required this.animationController,
this.waveColor = Colors.white,
this.barSpacing = 3.0,
this.minHeightRatio = 0.3,
this.maxHeightRatio = 1.0,
});
@override
@@ -23,34 +35,46 @@ class AudioWaveLargeMini extends StatelessWidget {
return AnimatedBuilder(
animation: animationController, // 使用传入的动画控制器
builder: (context, child) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: List.generate(8, (index) { // 生成8个波形条
// 为每个竖线设置不同的相位,使动画更自然
final phase = index * 0.4;
// 设置不同的频率变化
final frequency = 1.0 + (index % 3) * 0.2;
return LayoutBuilder(
builder: (context, constraints) {
// 根据容器宽度动态计算竖线数量
final containerWidth = constraints.maxWidth;
final containerHeight = constraints.maxHeight;
final barWidthWithSpacing = 2 + barSpacing;
final barCount = (containerWidth / barWidthWithSpacing).floor().clamp(3, 15);
// 计算波形高度最小4.0最大14.0
final height = 4.0 + 10.0 * math.sin(
(animationController.value * 2 * math.pi * frequency) + phase
).abs();
// 构建单个波形条
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.0), // 左右间隔4px
child: Container(
width: 2, // 波形条宽度2px
height: height, // 高度随动画变化
decoration: BoxDecoration(
color: waveColor,
borderRadius: BorderRadius.circular(2), // 圆角效果
),
),
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, // 改为垂直居中和AudioWaveLarge一致
children: List.generate(barCount, (index) {
// 为每个竖线设置不同的相位和AudioWaveLarge保持一致
final phase = index * 0.3; // 改为0.3和AudioWaveLarge一致
// 设置不同的频率变化
final frequency = 1.0 + (index % 3) * 0.2;
// 根据容器高度计算波形高度使用和AudioWaveLarge相同的算法
final minHeight = containerHeight * minHeightRatio;
final maxHeight = containerHeight * maxHeightRatio;
final height = minHeight + (maxHeight - minHeight) * math.sin(
(animationController.value * 2 * math.pi * frequency) + phase
).abs();
// 构建单个波形条
return Padding(
padding: EdgeInsets.symmetric(horizontal: barSpacing / 2),
child: Container(
width: 2, // 波形条宽度2px
height: height, // 高度随动画变化
decoration: BoxDecoration(
color: waveColor,
borderRadius: BorderRadius.circular(1), // 圆角效果
),
),
);
}),
);
}),
},
);
},
);