Files
ai_chat_assistant/lib/widgets/floating_icon.dart

260 lines
7.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
import 'dart:async';
import 'package:basic_intl/intl.dart';
class FloatingIcon extends StatefulWidget {
const FloatingIcon({super.key});
@override
State<FloatingIcon> createState() => _FloatingIconState();
}
class _FloatingIconState extends State<FloatingIcon> with TickerProviderStateMixin {
Offset _position = const Offset(10, 120);
final iconSize = 80.0;
bool _isShowPartScreen = false;
bool _isDragging = false;
bool _isAnimating = false;
Offset _targetPosition = const Offset(10, 120);
// 水波纹动画控制器
late AnimationController _waveAnimationController;
// PartScreen 显示动画控制器
late AnimationController _partScreenAnimationController;
// 图片切换定时器
Timer? _imageTimer;
// 添加长按状态标记
bool _isLongPressing = false;
// 图片切换相关
int _imageIndex = 0;
late final List<String> _iconImages = [
'assets/images/ai1_hd.png',
'assets/images/ai0_hd.png',
];
late final List<String> _iconImagesEn = [
'assets/images/ai1_hd_en.png',
'assets/images/ai0_hd_en.png',
];
@override
void initState() {
super.initState();
_waveAnimationController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
// PartScreen动画控制器
_partScreenAnimationController = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
);
// 图片切换定时器
_imageTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (mounted && !_isDragging) {
setState(() {
_imageIndex = (_imageIndex + 1) % _iconImages.length;
});
}
});
}
@override
void dispose() {
_waveAnimationController.dispose();
_partScreenAnimationController.dispose();
_imageTimer?.cancel();
super.dispose();
}
void _showPartScreen() {
setState(() {
_isShowPartScreen = true;
});
_partScreenAnimationController.forward();
}
void _hidePartScreen() {
_partScreenAnimationController.reverse().then((_) {
if (mounted) {
setState(() {
_isShowPartScreen = false;
});
}
});
}
// 显示全屏界面
void _showFullScreen() async {
_hidePartScreen();
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const FullScreen(),
),
);
}
// 更新位置
void _updatePosition(Offset delta) {
if (!_isDragging) return;
setState(() {
final screenSize = MediaQuery.of(context).size;
double newX = (_position.dx - delta.dx).clamp(0.0, screenSize.width - iconSize);
double newY = (_position.dy - delta.dy).clamp(0.0, screenSize.height - iconSize);
_position = Offset(newX, newY);
});
}
// 吸附到屏幕边缘
void _snapToEdge() {
final screenSize = MediaQuery.of(context).size;
final centerX = screenSize.width / 2;
// 判断应该吸到左边还是右边
final shouldSnapToLeft = _position.dx < centerX - iconSize / 2;
final targetX = shouldSnapToLeft ? 0.0 : screenSize.width - iconSize;
setState(() {
_targetPosition = Offset(targetX, _position.dy);
_isAnimating = true;
});
// 使用定时器在动画完成后更新状态
Timer(const Duration(milliseconds: 100), () {
if (mounted) {
setState(() {
_position = _targetPosition;
_isAnimating = false;
});
}
});
}
void _onPanStart(DragStartDetails details) {
_isDragging = true;
_waveAnimationController.stop();
}
void _onPanEnd(DragEndDetails details) {
_isDragging = false;
_snapToEdge();
}
@override
Widget build(BuildContext context) {
// 奇怪的代码不应该放在build里面
// WidgetsBinding.instance.addPostFrameCallback((_) {
// if (messageService.isRecording) {
// if (!_waveAnimationController.isAnimating) {
// _waveAnimationController.repeat();
// }
// } else {
// if (_waveAnimationController.isAnimating) {
// _waveAnimationController.stop();
// }
// }
// });
return Stack(
children: [
if (_isShowPartScreen)
FadeTransition(
opacity: _partScreenAnimationController,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _partScreenAnimationController,
curve: Curves.easeOutQuart,
)),
child: PartScreen(
onHide: _hidePartScreen,
floatingIconPosition: _position,
iconSize: iconSize,
)),
),
AnimatedPositioned(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutBack,
bottom: _isAnimating ? _targetPosition.dy : _position.dy,
right: _isAnimating ? _targetPosition.dx : _position.dx,
child: Consumer<MessageService>(
builder: (context, messageService, child) {
return GestureDetector(
onTap: _showFullScreen,
onLongPress: () async {
debugPrint('Long press');
_isLongPressing = true; // 标记开始长按
_showPartScreen();
_waveAnimationController.repeat();
await messageService.startVoiceInput();
},
onLongPressUp: () async {
debugPrint('Long press up');
_isLongPressing = false; // 清除长按标记
_waveAnimationController.stop();
await messageService.stopAndProcessVoiceInput();
final hasMessage = messageService.messages.isNotEmpty;
if (!hasMessage) {
_hidePartScreen();
}
},
onLongPressEnd: (d) {
debugPrint('Long press end $d');
_isLongPressing = false; // 清除长按标记
},
onPanStart: (details) {
// 只有在非长按状态下才允许拖拽
if (!_isLongPressing) {
_onPanStart(details);
}
},
onPanUpdate: (details) {
// 只有在拖拽状态下才更新位置
if (_isDragging && !_isLongPressing) {
_updatePosition(details.delta);
}
},
onPanEnd: (details) {
if (!_isLongPressing) {
_onPanEnd(details);
}
},
child: AnimatedScale(
scale: _isDragging ? 1.1 : 1.0,
duration: const Duration(milliseconds: 150),
child: messageService.isRecording
? FloatingIconWithWave(
animationController: _waveAnimationController,
iconSize: iconSize,
waveColor: Colors.white,
)
: Image.asset(
Intl.getCurrentLocale().startsWith('zh')
? _iconImages[_imageIndex]
: _iconImagesEn[_imageIndex],
width: iconSize,
height: iconSize,
package: 'ai_chat_assistant',
),
),
);
},
),
)
],
);
}
}