yumi-flutter/lib/services/gift/gift_animation_manager.dart
2026-05-07 10:06:02 +08:00

352 lines
9.6 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'package:flutter/cupertino.dart';
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
class GiftAnimationManager extends ChangeNotifier {
static const int maxVisibleSlots = 4;
static const int _maxPendingAnimations = 24;
static const Duration _activeComboIdleDismissDelay = Duration(
milliseconds: 3200,
);
Queue<LGiftModel> pendingAnimationsQueue = Queue<LGiftModel>();
List<LGiftScrollingScreenAnimsBean> animationControllerList = [];
//每个控件正在播放的动画
Map<int, LGiftModel?> giftMap = {0: null, 1: null, 2: null, 3: null};
int _visibleSlotCount = 0;
bool get _controllersReady =>
_visibleSlotCount <= 0 ||
animationControllerList.length >= _visibleSlotCount;
GiftAnimationSlotSnapshot? slotSnapshotAt(int index) {
if (index < 0 || index >= _visibleSlotCount) {
return null;
}
final gift = giftMap[index];
if (gift == null || animationControllerList.length <= index) {
return null;
}
final bean = animationControllerList[index];
return GiftAnimationSlotSnapshot(
index: index,
bean: bean,
sendUserName: gift.sendUserName,
sendToUserName: gift.sendToUserName,
sendUserPic: gift.sendUserPic,
giftPic: gift.giftPic,
giftName: gift.giftName,
giftCount: gift.giftCount,
rewardAmountText: gift.rewardAmountText,
rewardAmount: gift.rewardAmount,
showLuckyRewardFrame: gift.showLuckyRewardFrame,
labelId: gift.labelId,
giftCountStepUnit: gift.giftCountStepUnit,
);
}
void setVisibleSlotCount(int count) {
final nextCount = count.clamp(0, maxVisibleSlots).toInt();
if (nextCount == _visibleSlotCount) {
return;
}
final oldCount = _visibleSlotCount;
if (nextCount < oldCount) {
for (var index = oldCount - 1; index >= nextCount; index -= 1) {
final gift = giftMap[index];
if (gift == null) {
continue;
}
_cancelSlotDismissTimer(index);
giftMap[index] = null;
pendingAnimationsQueue.addFirst(gift);
}
_trimPendingAnimationsFromEnd();
}
_visibleSlotCount = nextCount;
notifyListeners();
proceedToNextAnimation();
}
void enqueueGiftAnimation(LGiftModel giftModel) {
if (_mergeIntoActiveAnimation(giftModel)) {
return;
}
if (_mergeIntoPendingAnimation(giftModel)) {
return;
}
_trimPendingAnimations();
pendingAnimationsQueue.add(giftModel);
proceedToNextAnimation();
}
void clearActiveAnimations() {
pendingAnimationsQueue.clear();
var changed = false;
for (final index in giftMap.keys.toList()) {
_cancelSlotDismissTimer(index);
if (giftMap[index] != null) {
giftMap[index] = null;
changed = true;
}
}
if (changed) {
notifyListeners();
}
}
void _trimPendingAnimations() {
while (pendingAnimationsQueue.length >= _maxPendingAnimations) {
pendingAnimationsQueue.removeFirst();
}
}
void _trimPendingAnimationsFromEnd() {
while (pendingAnimationsQueue.length > _maxPendingAnimations) {
pendingAnimationsQueue.removeLast();
}
}
bool _mergeIntoActiveAnimation(LGiftModel incoming) {
for (var index = 0; index < _visibleSlotCount; index += 1) {
final current = giftMap[index];
if (current == null || current.labelId != incoming.labelId) {
continue;
}
_mergeGiftModel(target: current, incoming: incoming);
notifyListeners();
_refreshSlotAnimation(index, restartEntry: false);
return true;
}
return false;
}
bool _mergeIntoPendingAnimation(LGiftModel incoming) {
for (final pending in pendingAnimationsQueue) {
if (pending.labelId != incoming.labelId) {
continue;
}
_mergeGiftModel(target: pending, incoming: incoming);
return true;
}
return false;
}
void _mergeGiftModel({
required LGiftModel target,
required LGiftModel incoming,
}) {
target.giftCount = (target.giftCount) + (incoming.giftCount);
if (target.sendUserName.isEmpty) {
target.sendUserName = incoming.sendUserName;
}
if (target.sendToUserName.isEmpty) {
target.sendToUserName = incoming.sendToUserName;
}
if (target.sendUserPic.isEmpty) {
target.sendUserPic = incoming.sendUserPic;
}
if (target.giftPic.isEmpty) {
target.giftPic = incoming.giftPic;
}
if (target.giftName.isEmpty) {
target.giftName = incoming.giftName;
}
target.rewardAmount = target.rewardAmount + incoming.rewardAmount;
if (incoming.rewardAmountText.isNotEmpty) {
target.rewardAmountText = incoming.rewardAmountText;
}
target.showLuckyRewardFrame =
target.showLuckyRewardFrame || incoming.showLuckyRewardFrame;
if (incoming.giftCountStepUnit > 0) {
target.giftCountStepUnit = incoming.giftCountStepUnit;
}
}
///开始播放
proceedToNextAnimation() {
if (pendingAnimationsQueue.isEmpty ||
!_controllersReady ||
_visibleSlotCount <= 0) {
return;
}
var changed = false;
while (pendingAnimationsQueue.isNotEmpty) {
final playGift = pendingAnimationsQueue.first;
var consumed = false;
for (var index = 0; index < _visibleSlotCount; index += 1) {
final value = giftMap[index];
if (value == null) {
giftMap[index] = playGift;
pendingAnimationsQueue.removeFirst();
changed = true;
consumed = true;
_refreshSlotAnimation(index, restartEntry: true);
break;
}
if (value.labelId == playGift.labelId) {
_mergeGiftModel(target: value, incoming: playGift);
pendingAnimationsQueue.removeFirst();
changed = true;
consumed = true;
_refreshSlotAnimation(index, restartEntry: false);
break;
}
}
if (!consumed) {
break;
}
}
if (changed) {
notifyListeners();
}
}
void attachAnimationControllers(List<LGiftScrollingScreenAnimsBean> anins) {
animationControllerList = anins;
if (_visibleSlotCount > animationControllerList.length) {
setVisibleSlotCount(animationControllerList.length);
return;
}
proceedToNextAnimation();
}
void cleanupAnimationResources() {
pendingAnimationsQueue.clear();
for (final key in giftMap.keys) {
giftMap[key] = null;
}
_visibleSlotCount = 0;
for (var element in animationControllerList) {
element.dismissTimer?.cancel();
element.controller.dispose();
element.countPulseController.dispose();
}
animationControllerList.clear();
}
void markAnimationAsFinished(int index) {
_cancelSlotDismissTimer(index);
if (giftMap[index] == null) {
return;
}
giftMap[index] = null;
notifyListeners();
proceedToNextAnimation();
}
void _refreshSlotAnimation(int index, {required bool restartEntry}) {
if (animationControllerList.length <= index) {
return;
}
final bean = animationControllerList[index];
_restartSlotDismissTimer(index);
bean.countPulseController.forward(from: 0);
if (restartEntry) {
bean.controller.forward(from: 0);
}
}
void _restartSlotDismissTimer(int index) {
if (animationControllerList.length <= index) {
return;
}
final bean = animationControllerList[index];
bean.dismissTimer?.cancel();
bean.dismissTimer = Timer(_activeComboIdleDismissDelay, () {
if (giftMap[index] == null) {
return;
}
markAnimationAsFinished(index);
});
}
void _cancelSlotDismissTimer(int index) {
if (animationControllerList.length <= index) {
return;
}
animationControllerList[index].dismissTimer?.cancel();
animationControllerList[index].dismissTimer = null;
}
}
@immutable
class GiftAnimationSlotSnapshot {
const GiftAnimationSlotSnapshot({
required this.index,
required this.bean,
required this.sendUserName,
required this.sendToUserName,
required this.sendUserPic,
required this.giftPic,
required this.giftName,
required this.giftCount,
required this.rewardAmountText,
required this.rewardAmount,
required this.showLuckyRewardFrame,
required this.labelId,
required this.giftCountStepUnit,
});
final int index;
final LGiftScrollingScreenAnimsBean bean;
final String sendUserName;
final String sendToUserName;
final String sendUserPic;
final String giftPic;
final String giftName;
final num giftCount;
final String rewardAmountText;
final num rewardAmount;
final bool showLuckyRewardFrame;
final String labelId;
final num giftCountStepUnit;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is GiftAnimationSlotSnapshot &&
other.index == index &&
identical(other.bean, bean) &&
other.sendUserName == sendUserName &&
other.sendToUserName == sendToUserName &&
other.sendUserPic == sendUserPic &&
other.giftPic == giftPic &&
other.giftName == giftName &&
other.giftCount == giftCount &&
other.rewardAmountText == rewardAmountText &&
other.rewardAmount == rewardAmount &&
other.showLuckyRewardFrame == showLuckyRewardFrame &&
other.labelId == labelId &&
other.giftCountStepUnit == giftCountStepUnit;
}
@override
int get hashCode => Object.hash(
index,
bean,
sendUserName,
sendToUserName,
sendUserPic,
giftPic,
giftName,
giftCount,
rewardAmountText,
rewardAmount,
showLuckyRewardFrame,
labelId,
giftCountStepUnit,
);
}