272 lines
7.5 KiB
Dart
272 lines
7.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:yumi/app_localizations.dart';
|
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
|
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:yumi/services/audio/rtm_manager.dart';
|
|
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
|
|
|
|
|
class RoomAnimationQueueScreen extends StatefulWidget {
|
|
const RoomAnimationQueueScreen({super.key});
|
|
|
|
@override
|
|
State<RoomAnimationQueueScreen> createState() =>
|
|
_RoomAnimationQueueScreenState();
|
|
}
|
|
|
|
class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
|
final List<AnimationTask> _animationQueue = [];
|
|
bool _isQueueProcessing = false;
|
|
final Map<int, GlobalKey<_RoomEntranceAnimationState>> _animationKeys = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
Provider.of<RtmProvider>(context, listen: false).msgUserJoinListener =
|
|
_msgUserJoinListener;
|
|
}
|
|
|
|
_msgUserJoinListener(Msg msg) {
|
|
Future.delayed(Duration(milliseconds: 550), () {
|
|
_addToQueue(msg);
|
|
});
|
|
}
|
|
|
|
void _addToQueue(Msg msg) {
|
|
setState(() {
|
|
final taskId = DateTime.now().millisecondsSinceEpoch;
|
|
final animationKey = GlobalKey<_RoomEntranceAnimationState>();
|
|
|
|
_animationKeys[taskId] = animationKey;
|
|
|
|
final task = AnimationTask(
|
|
id: taskId,
|
|
msg: msg,
|
|
onComplete: () {
|
|
if (_animationQueue.isNotEmpty &&
|
|
_animationQueue.first.id == taskId) {
|
|
setState(() {
|
|
_animationQueue.removeAt(0);
|
|
_animationKeys.remove(taskId);
|
|
});
|
|
|
|
if (_animationQueue.isNotEmpty) {
|
|
_startNextAnimation();
|
|
} else {
|
|
setState(() => _isQueueProcessing = false);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
_animationQueue.add(task);
|
|
});
|
|
|
|
if (!_isQueueProcessing && _animationQueue.isNotEmpty) {
|
|
setState(() => _isQueueProcessing = true);
|
|
_startNextAnimation();
|
|
}
|
|
}
|
|
|
|
void _startNextAnimation({int retryCount = 0}) {
|
|
if (_animationQueue.isEmpty) return;
|
|
|
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
|
if (_animationQueue.isNotEmpty) {
|
|
final task = _animationQueue.first;
|
|
final key = _animationKeys[task.id];
|
|
|
|
if (key?.currentState != null) {
|
|
key!.currentState!._startAnimation();
|
|
} else if (retryCount < 3) {
|
|
// 有限次重试,避免无限循环
|
|
Future.delayed(const Duration(milliseconds: 50), () {
|
|
_startNextAnimation(retryCount: retryCount + 1);
|
|
});
|
|
} else {
|
|
// 重试多次失败后,跳过当前动画
|
|
print("动画启动失败,跳过当前任务");
|
|
task.onComplete();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _clearQueue() {
|
|
setState(() {
|
|
_animationQueue.clear();
|
|
_animationKeys.clear();
|
|
_isQueueProcessing = false;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Stack(
|
|
children: [
|
|
if (_animationQueue.isNotEmpty)
|
|
RoomEntranceAnimation(
|
|
key: _animationKeys[_animationQueue.first.id],
|
|
msg: _animationQueue.first.msg,
|
|
onComplete: _animationQueue.first.onComplete,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AnimationTask {
|
|
final int id;
|
|
final Msg msg;
|
|
final VoidCallback onComplete;
|
|
|
|
AnimationTask({
|
|
required this.id,
|
|
required this.msg,
|
|
required this.onComplete,
|
|
});
|
|
}
|
|
|
|
class RoomEntranceAnimation extends StatefulWidget {
|
|
final VoidCallback onComplete;
|
|
final Msg msg;
|
|
|
|
const RoomEntranceAnimation({
|
|
super.key,
|
|
required this.onComplete,
|
|
required this.msg,
|
|
});
|
|
|
|
@override
|
|
State<RoomEntranceAnimation> createState() => _RoomEntranceAnimationState();
|
|
}
|
|
|
|
class _RoomEntranceAnimationState extends State<RoomEntranceAnimation>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<Offset> _offsetAnimation;
|
|
bool _isAnimating = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: const Duration(seconds: 4),
|
|
vsync: this,
|
|
);
|
|
|
|
_offsetAnimation = TweenSequence<Offset>([
|
|
TweenSequenceItem(
|
|
tween: Tween<Offset>(
|
|
begin: const Offset(1.0, 0.0),
|
|
end: const Offset(0.0, 0.0),
|
|
).chain(CurveTween(curve: Curves.easeOut)),
|
|
weight: 40.0,
|
|
),
|
|
TweenSequenceItem(
|
|
tween: ConstantTween<Offset>(const Offset(0.0, 0.0)),
|
|
weight: 20.0,
|
|
),
|
|
TweenSequenceItem(
|
|
tween: Tween<Offset>(
|
|
begin: const Offset(0.0, 0.0),
|
|
end: const Offset(-1.0, 0.0),
|
|
).chain(CurveTween(curve: Curves.easeIn)),
|
|
weight: 40.0,
|
|
),
|
|
]).animate(_controller);
|
|
|
|
_controller.addStatusListener((status) {
|
|
if (status == AnimationStatus.completed) {
|
|
setState(() => _isAnimating = false);
|
|
widget.onComplete();
|
|
}
|
|
});
|
|
}
|
|
|
|
void _startAnimation() {
|
|
if (_isAnimating) return;
|
|
|
|
setState(() => _isAnimating = true);
|
|
_controller.reset();
|
|
_controller.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SlideTransition(
|
|
position: _offsetAnimation,
|
|
child: Container(
|
|
width: ScreenUtil().screenWidth * 0.8,
|
|
height: 37.w,
|
|
decoration: BoxDecoration(
|
|
image: DecorationImage(
|
|
image: AssetImage(
|
|
getEntranceBg(""),
|
|
),
|
|
fit: BoxFit.fill,
|
|
),
|
|
),
|
|
child: Row(
|
|
textDirection: TextDirection.ltr,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.all(2.w),
|
|
child: netImage(
|
|
url: widget.msg.user?.userAvatar ?? "",
|
|
width: 28.w,
|
|
height: 28.w,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
SizedBox(width: 5.w),
|
|
Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(height: 2.w),
|
|
socialchatNickNameText(
|
|
maxWidth: 150.w,
|
|
widget.msg.user?.userNickname ?? "",
|
|
fontSize: 12.sp,
|
|
type: "",
|
|
needScroll:
|
|
(widget.msg.user?.userNickname?.characters.length ?? 0) >
|
|
15,
|
|
),
|
|
text(
|
|
SCAppLocalizations.of(context)!.enterTheRoom,
|
|
fontSize: 12.sp,
|
|
lineHeight: 0.9,
|
|
textColor: Colors.white,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String getEntranceBg(String type) {
|
|
return "sc_images/room/entrance/sc_icon_room_entrance_no_vip_bg.png";
|
|
}
|
|
}
|