550 lines
16 KiB
Dart
550 lines
16 KiB
Dart
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_debouncer/flutter_debouncer.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../../../chatvibe_core/at_lk_event_bus.dart';
|
|
import '../../../chatvibe_core/constants/at_global_config.dart';
|
|
import '../../../chatvibe_domain/models/res/gift_res.dart';
|
|
import '../../../chatvibe_managers/gift_system_manager.dart';
|
|
import '../../../chatvibe_managers/rtc_manager.dart';
|
|
import '../../../main.dart';
|
|
import '../../components/at_compontent.dart';
|
|
|
|
class GiveRoomLuckPage extends StatefulWidget {
|
|
ChatVibeGiftRes checkedGift;
|
|
List<String> acceptUserIds = [];
|
|
Function() call;
|
|
|
|
GiveRoomLuckPage(this.checkedGift, this.acceptUserIds, this.call);
|
|
|
|
@override
|
|
_GiveRoomLuckPageState createState() => _GiveRoomLuckPageState();
|
|
}
|
|
|
|
class _GiveRoomLuckPageState extends State<GiveRoomLuckPage>
|
|
with TickerProviderStateMixin {
|
|
static const int _maxOverlayAnimations = 10;
|
|
var debouncer = Debouncer();
|
|
final Set<AnimationController> _runningControllers = <AnimationController>{};
|
|
final Set<OverlayEntry> _activeOverlays = <OverlayEntry>{};
|
|
final Queue<_LuckFanOutTask> _pendingFanOutTasks = Queue<_LuckFanOutTask>();
|
|
|
|
// bool isFasheOk = true;
|
|
final GlobalKey _sourceKey1 = GlobalKey();
|
|
final GlobalKey _targetKey1 = GlobalKey();
|
|
|
|
bool isFashe = false;
|
|
|
|
late AnimationController _fasheController;
|
|
late Animation<double> _fasheAnimation;
|
|
|
|
Timer? timer;
|
|
int startFasheTime = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
Provider.of<GiftProvider>(
|
|
navigatorKey.currentContext!,
|
|
listen: false,
|
|
).reset();
|
|
Provider.of<GiftProvider>(
|
|
navigatorKey.currentContext!,
|
|
listen: false,
|
|
).updateHideLGiftAnimal(true);
|
|
initFasheAnim();
|
|
// initScanAnim();
|
|
startTimer();
|
|
// GiftVapSvgaManager().pauseAnim();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
eventBus.fire(ATGiveRoomLuckPageDisposeEvent());
|
|
_disposeTransientEffects();
|
|
_fasheController.dispose();
|
|
timer?.cancel();
|
|
|
|
// GiftVapSvgaManager().resumeAnim();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
top: false,
|
|
child: Stack(
|
|
alignment: Alignment.bottomCenter,
|
|
children: [
|
|
GestureDetector(
|
|
child: Container(color: Colors.transparent),
|
|
onTap: () {
|
|
SmartDialog.dismiss(tag: "showGiftControl");
|
|
},
|
|
),
|
|
Positioned(
|
|
right: ATGlobalConfig.lang == "ar" ? null : 0,
|
|
left: ATGlobalConfig.lang == "ar" ? 0 : null,
|
|
bottom: 0,
|
|
child: Transform.rotate(
|
|
angle: ATGlobalConfig.lang == "ar" ? 0.5 : -0.5,
|
|
child: Transform.translate(
|
|
offset:
|
|
ATGlobalConfig.lang == "ar"
|
|
? Offset(-22, 35)
|
|
: Offset(22, 35 ),
|
|
child: AnimatedBuilder(
|
|
animation: _fasheAnimation,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _fasheAnimation.value,
|
|
child: SizedBox(
|
|
width: 200.w,
|
|
height: 200.w,
|
|
child: Stack(
|
|
children: [
|
|
Image.asset(
|
|
"atu_images/room/at_icon_luck_gift_daiji.webp",
|
|
),
|
|
isFashe
|
|
? Image.asset(
|
|
"atu_images/room/at_icon_luck_fashe.webp",
|
|
)
|
|
: Container(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
Positioned(
|
|
bottom: 350.w,
|
|
child: SizedBox(width: 100.w, height: 100.w, key: _targetKey1),
|
|
),
|
|
|
|
Positioned(
|
|
bottom: 160.w,
|
|
right: ATGlobalConfig.lang == "ar" ? null : 105.w,
|
|
left: ATGlobalConfig.lang == "ar" ? 105.w : null,
|
|
child: SizedBox(key: _sourceKey1, width: 35.w, height: 35.w,),
|
|
),
|
|
Positioned(
|
|
right: ATGlobalConfig.lang == "ar" ? null : 0,
|
|
left: ATGlobalConfig.lang == "ar" ? 0 : null,
|
|
child: GestureDetector(
|
|
child: Container(
|
|
width: 100.w,
|
|
height: 100.w,
|
|
color: Colors.transparent,
|
|
),
|
|
onTap: () {
|
|
startFashe();
|
|
debouncer.debounce(
|
|
duration: Duration(milliseconds: 600),
|
|
onDebounce: () {
|
|
isFashe = false;
|
|
setState(() {});
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
///发射动画
|
|
void startFashe() {
|
|
startTimer();
|
|
_fasheController.reset();
|
|
_fasheController.forward();
|
|
|
|
int now = DateTime.now().millisecondsSinceEpoch;
|
|
if (now - startFasheTime < 150) {
|
|
return;
|
|
}
|
|
startFasheTime = now;
|
|
isFashe = true;
|
|
_startA1();
|
|
}
|
|
|
|
void fasheComlete() {
|
|
widget.call();
|
|
}
|
|
|
|
void startTimer() {
|
|
timer?.cancel();
|
|
timer = Timer(Duration(seconds: 5), () {
|
|
SmartDialog.dismiss(tag: "showGiveRoomLuckPage");
|
|
});
|
|
}
|
|
|
|
void _startA1() {
|
|
// 确保上下文有效
|
|
final Offset? sourceCenter = _centerFromKey(_sourceKey1);
|
|
final Offset? targetCenter = _centerFromKey(_targetKey1);
|
|
if (sourceCenter == null || targetCenter == null) {
|
|
return;
|
|
}
|
|
|
|
// 计算图片中心点偏移(使图片中心对准控件中心)
|
|
final double imageOffsetX = 55.w / 2;
|
|
final double imageOffsetY = 55.w / 2;
|
|
|
|
final AnimationController controller = AnimationController(
|
|
vsync: this,
|
|
duration: Duration(milliseconds: 1100),
|
|
);
|
|
_runningControllers.add(controller);
|
|
OverlayEntry? overlayEntry;
|
|
controller.addStatusListener((status) {
|
|
if (status == AnimationStatus.completed) {
|
|
if (overlayEntry != null) {
|
|
_removeOverlay(overlayEntry);
|
|
overlayEntry = null;
|
|
bool hasUser = false;
|
|
for (var uid in widget.acceptUserIds) {
|
|
GlobalKey<State<StatefulWidget>>? targetKey =
|
|
Provider.of<RtcProvider>(
|
|
context,
|
|
listen: false,
|
|
).getSeatGlobalKeyByIndex(uid);
|
|
if (targetKey?.currentContext != null) {
|
|
hasUser = true;
|
|
}
|
|
}
|
|
fasheComlete();
|
|
|
|
///麦上都没人
|
|
if (!hasUser) {
|
|
return;
|
|
}
|
|
_startA2();
|
|
}
|
|
_disposeTransientController(controller);
|
|
}
|
|
});
|
|
|
|
// 创建位置动画:从起始位置到目标位置
|
|
final Animation<Offset> positionAnimationX1 = Tween<Offset>(
|
|
begin: Offset(
|
|
sourceCenter.dx - imageOffsetX,
|
|
sourceCenter.dy - imageOffsetY,
|
|
),
|
|
end: Offset(
|
|
targetCenter.dx - imageOffsetX,
|
|
targetCenter.dy - imageOffsetY,
|
|
),
|
|
).animate(
|
|
CurvedAnimation(
|
|
parent: controller,
|
|
curve: const Interval(0.2, 0.4, curve: Curves.easeInOut),
|
|
),
|
|
);
|
|
// 创建缩放动画
|
|
final Animation<double> scaleAnimationX1 = Tween<double>(
|
|
begin: 0.5,
|
|
end: 1.1, // 可以根据需要调整
|
|
).animate(
|
|
CurvedAnimation(
|
|
parent: controller,
|
|
curve: const Interval(0, 0.2, curve: Curves.easeInOut),
|
|
),
|
|
);
|
|
|
|
// 创建缩放动画
|
|
final Animation<double> scaleAnimationX2 = Tween<double>(
|
|
begin: 1,
|
|
end: 1.5, // 可以根据需要调整
|
|
).animate(
|
|
CurvedAnimation(
|
|
parent: controller,
|
|
curve: const Interval(0.4, 0.7, curve: Curves.easeInOut),
|
|
),
|
|
);
|
|
|
|
// 创建浮动控件
|
|
overlayEntry = OverlayEntry(
|
|
builder:
|
|
(context) => AnimatedBuilder(
|
|
animation: controller,
|
|
child: RepaintBoundary(
|
|
child: netImage(
|
|
url: widget.checkedGift.giftPhoto ?? "",
|
|
width: 55.w,
|
|
height: 55.w,
|
|
),
|
|
),
|
|
builder: (context, child) {
|
|
return Positioned(
|
|
left: positionAnimationX1.value.dx,
|
|
top: positionAnimationX1.value.dy,
|
|
child: Transform.scale(
|
|
scale: scaleAnimationX1.value,
|
|
child: Transform.scale(
|
|
scale: scaleAnimationX2.value,
|
|
child: child,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
// 插入Overlay并启动动画
|
|
if (!_insertOverlay(overlayEntry!)) {
|
|
_disposeTransientController(controller);
|
|
return;
|
|
}
|
|
controller.forward(from: 0);
|
|
}
|
|
|
|
void _startA2() {
|
|
// 确保上下文有效
|
|
final Offset? sourceCenter = _centerFromKey(_targetKey1);
|
|
if (sourceCenter == null) return;
|
|
final RtcProvider rtc = Provider.of<RtcProvider>(context, listen: false);
|
|
final List<String> targetUserIds = _pickTargetUserIds(widget.acceptUserIds);
|
|
for (var userId in targetUserIds) {
|
|
final GlobalKey<State<StatefulWidget>>? targetKey = rtc
|
|
.getSeatGlobalKeyByIndex(userId);
|
|
final Offset? targetCenter =
|
|
targetKey == null ? null : _centerFromKey(targetKey);
|
|
if (targetCenter == null) {
|
|
continue;
|
|
}
|
|
_pendingFanOutTasks.add(
|
|
_LuckFanOutTask(
|
|
sourceCenter: sourceCenter,
|
|
targetCenter: targetCenter,
|
|
giftUrl: widget.checkedGift.giftPhoto ?? "",
|
|
),
|
|
);
|
|
}
|
|
_drainFanOutQueue();
|
|
}
|
|
|
|
void _drainFanOutQueue() {
|
|
while (mounted &&
|
|
_activeOverlays.length < _maxOverlayAnimations &&
|
|
_pendingFanOutTasks.isNotEmpty) {
|
|
_startFanOutTask(_pendingFanOutTasks.removeFirst());
|
|
}
|
|
}
|
|
|
|
void _startFanOutTask(_LuckFanOutTask task) {
|
|
final double imageOffsetX = 55.w / 2;
|
|
final double imageOffsetY = 55.w / 2;
|
|
|
|
final AnimationController controller = AnimationController(
|
|
vsync: this,
|
|
duration: Duration(milliseconds: 1100),
|
|
);
|
|
_runningControllers.add(controller);
|
|
OverlayEntry? overlayEntry;
|
|
controller.addStatusListener((status) {
|
|
if (status == AnimationStatus.completed) {
|
|
if (overlayEntry != null) {
|
|
_removeOverlay(overlayEntry);
|
|
overlayEntry = null;
|
|
}
|
|
_disposeTransientController(controller);
|
|
_drainFanOutQueue();
|
|
}
|
|
});
|
|
|
|
// 创建位置动画:从起始位置到目标位置
|
|
final Animation<Offset> positionAnimationX1 = Tween<Offset>(
|
|
begin: Offset(
|
|
task.sourceCenter.dx - imageOffsetX,
|
|
task.sourceCenter.dy - imageOffsetY,
|
|
),
|
|
end: Offset(
|
|
task.targetCenter.dx - imageOffsetX,
|
|
task.targetCenter.dy - imageOffsetY,
|
|
),
|
|
).animate(
|
|
CurvedAnimation(
|
|
parent: controller,
|
|
curve: const Interval(0.3, 0.7, curve: Curves.easeInOut),
|
|
),
|
|
);
|
|
|
|
// 创建缩放动画
|
|
final Animation<double> scaleAnimationX1 = Tween<double>(
|
|
begin: 1.1,
|
|
end: 0.7, // 可以根据需要调整
|
|
).animate(
|
|
CurvedAnimation(
|
|
parent: controller,
|
|
curve: const Interval(0.7, 0.8, curve: Curves.easeInOut),
|
|
),
|
|
);
|
|
|
|
// 创建浮动控件
|
|
overlayEntry = OverlayEntry(
|
|
builder:
|
|
(context) => AnimatedBuilder(
|
|
animation: controller,
|
|
child: RepaintBoundary(
|
|
child: netImage(url: task.giftUrl, width: 55.w, height: 55.w),
|
|
),
|
|
builder: (context, child) {
|
|
return Positioned(
|
|
left: positionAnimationX1.value.dx,
|
|
top: positionAnimationX1.value.dy,
|
|
child: Transform.scale(
|
|
scale: scaleAnimationX1.value,
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
// 插入Overlay并启动动画
|
|
if (!_insertOverlay(overlayEntry!)) {
|
|
_disposeTransientController(controller);
|
|
_drainFanOutQueue();
|
|
return;
|
|
}
|
|
controller.forward(from: 0);
|
|
}
|
|
|
|
void _disposeTransientEffects() {
|
|
for (final OverlayEntry entry in _activeOverlays.toList()) {
|
|
entry.remove();
|
|
}
|
|
_activeOverlays.clear();
|
|
for (final AnimationController controller in _runningControllers.toList()) {
|
|
controller.dispose();
|
|
}
|
|
_runningControllers.clear();
|
|
_pendingFanOutTasks.clear();
|
|
}
|
|
|
|
void _removeOverlay(OverlayEntry? entry) {
|
|
if (entry == null) {
|
|
return;
|
|
}
|
|
if (_activeOverlays.remove(entry)) {
|
|
entry.remove();
|
|
}
|
|
}
|
|
|
|
void _disposeTransientController(AnimationController controller) {
|
|
if (_runningControllers.remove(controller)) {
|
|
controller.dispose();
|
|
}
|
|
}
|
|
|
|
bool _insertOverlay(OverlayEntry entry) {
|
|
if (!mounted || _activeOverlays.length >= _maxOverlayAnimations) {
|
|
return false;
|
|
}
|
|
final OverlayState? overlayState = Overlay.of(context, rootOverlay: true);
|
|
if (overlayState == null) {
|
|
return false;
|
|
}
|
|
overlayState.insert(entry);
|
|
_activeOverlays.add(entry);
|
|
return true;
|
|
}
|
|
|
|
Offset? _centerFromKey(GlobalKey key) {
|
|
final BuildContext? keyContext = key.currentContext;
|
|
if (keyContext == null) {
|
|
return null;
|
|
}
|
|
final RenderObject? renderObject = keyContext.findRenderObject();
|
|
if (renderObject is! RenderBox) {
|
|
return null;
|
|
}
|
|
final Offset position = renderObject.localToGlobal(Offset.zero);
|
|
final Size size = renderObject.size;
|
|
return Offset(position.dx + size.width / 2, position.dy + size.height / 2);
|
|
}
|
|
|
|
List<String> _pickTargetUserIds(List<String> userIds) {
|
|
final List<String> uniqueUserIds = <String>[];
|
|
final Set<String> duplicateFilter = <String>{};
|
|
for (final String userId in userIds) {
|
|
if (userId.isEmpty || !duplicateFilter.add(userId)) {
|
|
continue;
|
|
}
|
|
uniqueUserIds.add(userId);
|
|
}
|
|
return uniqueUserIds;
|
|
}
|
|
|
|
void initFasheAnim() {
|
|
_fasheController = AnimationController(
|
|
duration: const Duration(milliseconds: 500),
|
|
vsync: this,
|
|
);
|
|
// 使用TweenSequence来定义先缩小后放大的动画
|
|
_fasheAnimation = TweenSequence<double>([
|
|
TweenSequenceItem(
|
|
tween: Tween<double>(begin: 1.0, end: 1.1), // 缩小到0.5
|
|
weight: 1, // 权重,这里两个阶段各占一半
|
|
),
|
|
TweenSequenceItem(
|
|
tween: Tween<double>(begin: 1.1, end: 1.0), // 放大到1.0
|
|
weight: 1,
|
|
),
|
|
]).animate(_fasheController);
|
|
}
|
|
|
|
// void initScanAnim() {
|
|
// _giftController = AnimationController(
|
|
// duration: const Duration(milliseconds: 600),
|
|
// vsync: this,
|
|
// )..addStatusListener((status) {
|
|
// if (status == AnimationStatus.completed) {
|
|
// isShowGiftIcon1 = false;
|
|
// setState(() {});
|
|
// }
|
|
// });
|
|
//
|
|
// // 创建缩放动画
|
|
// _scalegiftAnimation1 = Tween<double>(
|
|
// begin: 1,
|
|
// end: 1.2, // 可以根据需要调整
|
|
// ).animate(
|
|
// CurvedAnimation(
|
|
// parent: _giftController,
|
|
// curve: const Interval(0, 0.4, curve: Curves.easeInOut),
|
|
// ),
|
|
// );
|
|
// _scalegiftAnimation2 = Tween<double>(
|
|
// begin: 1.2,
|
|
// end: 1, // 可以根据需要调整
|
|
// ).animate(
|
|
// CurvedAnimation(
|
|
// parent: _giftController,
|
|
// curve: const Interval(0.4, 0.8, curve: Curves.easeInOut),
|
|
// ),
|
|
// );
|
|
// }
|
|
}
|
|
|
|
class _LuckFanOutTask {
|
|
final Offset sourceCenter;
|
|
final Offset targetCenter;
|
|
final String giftUrl;
|
|
|
|
const _LuckFanOutTask({
|
|
required this.sourceCenter,
|
|
required this.targetCenter,
|
|
required this.giftUrl,
|
|
});
|
|
}
|