feat: 增加动画变量,处理多个变量控制动画逻辑

This commit is contained in:
speakeloudest 2025-12-01 19:41:38 -08:00
parent 86687cac58
commit aa0fd94cb2
2 changed files with 176 additions and 144 deletions

View File

@ -90,6 +90,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// //
final kr_isLatency = false.obs; final kr_isLatency = false.obs;
final kr_isPositionAnimating = false.obs;
/// ///
var kr_cutTag = ''.obs; var kr_cutTag = ''.obs;
var kr_cutSeletedTag = ''.obs; var kr_cutSeletedTag = ''.obs;
@ -217,14 +219,17 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
await _kr_prepareCountrySelectionBeforeStart(); await _kr_prepareCountrySelectionBeforeStart();
final selectedAfter = final selectedAfter =
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG'); await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
KRLogUtil.kr_i('准备后 SELECTED_NODE_TAG_autoConnect: ${selectedAfter ?? ''}', KRLogUtil.kr_i(
'准备后 SELECTED_NODE_TAG_autoConnect: ${selectedAfter ?? ''}',
tag: 'HomeController'); tag: 'HomeController');
KRLogUtil.kr_i('准备后 kr_currentNodeName_autoConnect: ${kr_currentNodeName.value}', KRLogUtil.kr_i(
'准备后 kr_currentNodeName_autoConnect: ${kr_currentNodeName.value}',
tag: 'HomeController'); tag: 'HomeController');
KRLogUtil.kr_i('准备后 kr_cutTag_autoConnect: ${kr_cutTag.value}', KRLogUtil.kr_i('准备后 kr_cutTag_autoConnect: ${kr_cutTag.value}',
tag: 'HomeController'); tag: 'HomeController');
KRLogUtil.kr_i('准备后 kr_cutSeletedTag_autoConnect: ${kr_cutSeletedTag.value}', KRLogUtil.kr_i(
'准备后 kr_cutSeletedTag_autoConnect: ${kr_cutSeletedTag.value}',
tag: 'HomeController'); tag: 'HomeController');
await kr_performNodeSwitch(selectedAfter!); await kr_performNodeSwitch(selectedAfter!);
await KRSingBoxImp.instance.kr_start(); await KRSingBoxImp.instance.kr_start();
@ -315,11 +320,42 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
}); });
// 🔧 Android 15 5 // 🔧 Android 15 5
Future.delayed(const Duration(seconds: 5), () { // Future.delayed(const Duration(seconds: 5), () {
if (kr_bottomPanelHeight.value < 100) { // if (kr_bottomPanelHeight.value < 100) {
KRLogUtil.kr_w('检测到底部面板高度异常(${kr_bottomPanelHeight.value}),强制修正', // KRLogUtil.kr_w('检测到底部面板高度异常(${kr_bottomPanelHeight.value}),强制修正',
tag: 'HomeController'); // tag: 'HomeController');
kr_updateBottomPanelHeight(); // kr_updateBottomPanelHeight();
// }
// });
_initShowAnimationWatcher();
}
bool _lastIsShow = false;
void _initShowAnimationWatcher() {
_lastIsShow = kr_currentNodeLatency.value == -1 || kr_isConnected.value;
everAll([kr_currentNodeLatency, kr_isConnected], (_) {
final v = kr_currentNodeLatency.value == -1 || kr_isConnected.value;
if (v != _lastIsShow) {
_triggerPositionAnim();
_lastIsShow = v;
}
});
}
void kr_onPositionAnimationEnd() {
kr_isPositionAnimating.value = false;
}
int _positionAnimToken = 0;
void _triggerPositionAnim() {
_positionAnimToken++;
final token = _positionAnimToken;
kr_isPositionAnimating.value = true;
Future.delayed(const Duration(milliseconds: 450), () {
if (_positionAnimToken == token) {
kr_isPositionAnimating.value = false;
} }
}); });
} }
@ -938,6 +974,26 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
if (kDebugMode) {} if (kDebugMode) {}
} }
DateTime? _lastConnectClickAt;
static const Duration _connectClickCooldown = Duration(milliseconds: 600);
bool kr_canProcessConnectClick() {
final now = DateTime.now();
if (kr_isPositionAnimating.value) {
return false;
}
final st = KRSingBoxImp.instance.kr_status.value;
if (st is SingboxStarting || st is SingboxStopping) {
return false;
}
if (_lastConnectClickAt != null &&
now.difference(_lastConnectClickAt!) < _connectClickCooldown) {
return false;
}
_lastConnectClickAt = now;
return true;
}
/// 🔧 /// 🔧
Future<void> _waitForStatus(Type expectedType, {int maxSeconds = 3}) async { Future<void> _waitForStatus(Type expectedType, {int maxSeconds = 3}) async {
if (kDebugMode) {} if (kDebugMode) {}
@ -1148,7 +1204,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
/// ///
void _kr_handleManualMode(dynamic element) { void _kr_handleManualMode(dynamic element) {
try { try {
KRLogUtil.kr_d('处理手动模式 - 内核选择: ${element.selected}, 用户选择: ${kr_cutTag.value}', tag: 'HomeController'); KRLogUtil.kr_d(
'处理手动模式 - 内核选择: ${element.selected}, 用户选择: ${kr_cutTag.value}',
tag: 'HomeController');
// 🔧 selected // 🔧 selected
// //
@ -1160,12 +1218,15 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// UI // UI
if (element.selected == kr_cutTag.value) { if (element.selected == kr_cutTag.value) {
kr_cutSeletedTag.value = element.selected; kr_cutSeletedTag.value = element.selected;
kr_currentNodeName.value = kr_truncateText(element.selected, maxLength: 25); kr_currentNodeName.value =
kr_truncateText(element.selected, maxLength: 25);
// kr_moveToSelectedNode(); // kr_moveToSelectedNode();
KRLogUtil.kr_d('✅ 内核确认节点切换成功: ${element.selected}', tag: 'HomeController'); KRLogUtil.kr_d('✅ 内核确认节点切换成功: ${element.selected}',
tag: 'HomeController');
} else { } else {
// //
KRLogUtil.kr_d('⏳ 等待内核切换到用户选择的节点: ${kr_cutTag.value}', tag: 'HomeController'); KRLogUtil.kr_d('⏳ 等待内核切换到用户选择的节点: ${kr_cutTag.value}',
tag: 'HomeController');
} }
} catch (e) { } catch (e) {
KRLogUtil.kr_e('处理手动模式出错: $e', tag: 'HomeController'); KRLogUtil.kr_e('处理手动模式出错: $e', tag: 'HomeController');
@ -1364,9 +1425,12 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// 🔒 2 // 🔒 2
final now = DateTime.now(); final now = DateTime.now();
if (_lastSwitchTime != null && now.difference(_lastSwitchTime!) < _switchThrottleDuration) { if (_lastSwitchTime != null &&
final remainingTime = _switchThrottleDuration.inMilliseconds - now.difference(_lastSwitchTime!).inMilliseconds; now.difference(_lastSwitchTime!) < _switchThrottleDuration) {
KRLogUtil.kr_w('⚠️ 切换过于频繁,请等待 ${remainingTime}ms', tag: 'HomeController'); final remainingTime = _switchThrottleDuration.inMilliseconds -
now.difference(_lastSwitchTime!).inMilliseconds;
KRLogUtil.kr_w('⚠️ 切换过于频繁,请等待 ${remainingTime}ms',
tag: 'HomeController');
KRCommonUtil.kr_showToast('切换过于频繁,请稍后再试'); KRCommonUtil.kr_showToast('切换过于频繁,请稍后再试');
return false; return false;
} }
@ -1393,7 +1457,9 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// 🔧 便VPN启动时应用 // 🔧 便VPN启动时应用
KRLogUtil.kr_i('💾 保存节点选择以便稍后应用: $tag', tag: 'HomeController'); KRLogUtil.kr_i('💾 保存节点选择以便稍后应用: $tag', tag: 'HomeController');
KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: tag).then((_) { KRSecureStorage()
.kr_saveData(key: 'SELECTED_NODE_TAG', value: tag)
.then((_) {
KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'HomeController'); KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'HomeController');
}).catchError((e) { }).catchError((e) {
KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'HomeController'); KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'HomeController');
@ -1412,11 +1478,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// 🔧 // 🔧
KRLogUtil.kr_i('💾 保存新节点选择: $tag', tag: 'HomeController'); KRLogUtil.kr_i('💾 保存新节点选择: $tag', tag: 'HomeController');
await KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: tag); await KRSecureStorage()
.kr_saveData(key: 'SELECTED_NODE_TAG', value: tag);
// 🚀 使 selectOutbound hiddify-app // 🚀 使 selectOutbound hiddify-app
// VPNVPN开关不闪烁 // VPNVPN开关不闪烁
KRLogUtil.kr_i('🔄 [热切换] 调用 selectOutbound 切换节点...', tag: 'HomeController'); KRLogUtil.kr_i('🔄 [热切换] 调用 selectOutbound 切换节点...',
tag: 'HomeController');
// 🔧 selector tag // 🔧 selector tag
// selectOutbound(groupTag, outboundTag) - tagtag // selectOutbound(groupTag, outboundTag) - tagtag
@ -1427,12 +1495,14 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
for (var group in activeGroups) { for (var group in activeGroups) {
if (group.type == ProxyType.selector) { if (group.type == ProxyType.selector) {
selectorGroupTag = group.tag; selectorGroupTag = group.tag;
KRLogUtil.kr_i('🔍 找到 selector 组: $selectorGroupTag', tag: 'HomeController'); KRLogUtil.kr_i('🔍 找到 selector 组: $selectorGroupTag',
tag: 'HomeController');
break; break;
} }
} }
KRLogUtil.kr_i('📡 调用 selectOutbound("$selectorGroupTag", "$tag")', tag: 'HomeController'); KRLogUtil.kr_i('📡 调用 selectOutbound("$selectorGroupTag", "$tag")',
tag: 'HomeController');
// sing-box selectOutbound API // sing-box selectOutbound API
final result = await KRSingBoxImp.instance.kr_singBox final result = await KRSingBoxImp.instance.kr_singBox
@ -1441,12 +1511,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// //
result.fold( result.fold(
(error) { (error) {
// //
KRLogUtil.kr_e('❌ selectOutbound 调用失败: $error', tag: 'HomeController'); KRLogUtil.kr_e('❌ selectOutbound 调用失败: $error',
tag: 'HomeController');
throw Exception('节点切换失败: $error'); throw Exception('节点切换失败: $error');
}, },
(_) { (_) {
// //
KRLogUtil.kr_i('✅ selectOutbound 调用成功', tag: 'HomeController'); KRLogUtil.kr_i('✅ selectOutbound 调用成功', tag: 'HomeController');
}, },
@ -1466,15 +1537,18 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
try { try {
final updatedGroups = KRSingBoxImp.instance.kr_activeGroups; final updatedGroups = KRSingBoxImp.instance.kr_activeGroups;
final selectGroup = updatedGroups.firstWhere( final selectGroup = updatedGroups.firstWhere(
(group) => group.type == ProxyType.selector, (group) => group.type == ProxyType.selector,
orElse: () => throw Exception('未找到 selector 组'), orElse: () => throw Exception('未找到 selector 组'),
); );
KRLogUtil.kr_i('📊 [验证] ${selectGroup.tag}组当前选中: ${selectGroup.selected}', tag: 'HomeController'); KRLogUtil.kr_i(
'📊 [验证] ${selectGroup.tag}组当前选中: ${selectGroup.selected}',
tag: 'HomeController');
KRLogUtil.kr_i('📊 [验证] 目标节点: $tag', tag: 'HomeController'); KRLogUtil.kr_i('📊 [验证] 目标节点: $tag', tag: 'HomeController');
if (selectGroup.selected != tag) { if (selectGroup.selected != tag) {
KRLogUtil.kr_w('⚠️ [验证] 节点选择验证失败,实际选中: ${selectGroup.selected}', tag: 'HomeController'); KRLogUtil.kr_w('⚠️ [验证] 节点选择验证失败,实际选中: ${selectGroup.selected}',
tag: 'HomeController');
// //
} else { } else {
KRLogUtil.kr_i('✅ [验证] 节点选择验证成功!', tag: 'HomeController'); KRLogUtil.kr_i('✅ [验证] 节点选择验证成功!', tag: 'HomeController');
@ -1488,7 +1562,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
KRLogUtil.kr_i('✅ 节点热切换成功VPN保持连接: $tag', tag: 'HomeController'); KRLogUtil.kr_i('✅ 节点热切换成功VPN保持连接: $tag', tag: 'HomeController');
return true; return true;
} catch (switchError) { } catch (switchError) {
// //
KRLogUtil.kr_e('❌ 后台节点切换失败: $switchError', tag: 'HomeController'); KRLogUtil.kr_e('❌ 后台节点切换失败: $switchError', tag: 'HomeController');
@ -1500,7 +1573,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// //
try { try {
await KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: originalTag); await KRSecureStorage()
.kr_saveData(key: 'SELECTED_NODE_TAG', value: originalTag);
} catch (e) { } catch (e) {
KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'HomeController'); KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'HomeController');
} }

View File

@ -22,113 +22,96 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() {
final delay = controller.kr_currentNodeLatency.value; final _ = KRSingBoxImp.instance.kr_status.value;
// 🔧 : observable
final _ = KRSingBoxImp.instance.kr_status.value; //
final isConnected = controller.kr_isConnected.value; // 使 controller
//
final status = KRSingBoxImp.instance.kr_status.value;
final isSwitching = status is SingboxStarting || status is SingboxStopping;
print('🔵 Switch UI 更新: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching');
final isShow = delay == -1 || isConnected;
final Color buttonColor = Theme.of(context).primaryColor;
final double screenWidth = Get.width; final double screenWidth = Get.width;
final double screenHeight = Get.height; final double screenHeight = Get.height;
final double buttonSize = 340; final double buttonSize = 340;
final double currentButtonSize = isShow ? 270 : buttonSize; final double currentButtonSize = 270;
return Stack( return Stack(
children: [ children: [
AnimatedPositioned( AnimatedPositioned(
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
curve: Curves.linear, curve: Curves.linear,
left: isShow ? (screenWidth - currentButtonSize) / 2 : -60, left: (screenWidth - currentButtonSize) / 2,
bottom: isShow ? screenHeight * 0.2 : -40, bottom: screenHeight * 0.2,
onEnd: () {
controller.kr_onPositionAnimationEnd();
},
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
/// if (controller.kr_isPositionAnimating.value)
if (isShow)
Positioned.fill( Positioned.fill(
child: OverflowBox( child: OverflowBox(
maxWidth: buttonSize * 3, maxWidth: buttonSize * 3,
maxHeight: buttonSize * 3, maxHeight: buttonSize * 3,
child: ContinuousRippleEffect( child: ContinuousRippleEffect(
color: Theme.of(context).primaryColor color: Theme.of(context).primaryColor),
),
), ),
), ),
if (!isShow)
Positioned(
top: -20, // 10.w
left: 115, // 10.w
child: KrLocalImage(
imageName: "hi-home-slogan",
imageType: ImageType.svg,
),
),
///
Material( Material(
shape: const CircleBorder(), shape: const CircleBorder(),
color: isShow ? Colors.transparent : Theme.of(context).primaryColor, color: Colors.transparent,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: AbsorbPointer(
onTap: () { absorbing: controller.kr_isPositionAnimating.value,
if(isSwitching) { child: InkWell(
print('🔵 Switch UI 正在更新,切换中点击了按钮: status=${status.runtimeType}, isConnected=$isConnected, isSwitching=$isSwitching'); onTap: () {
return; if (!controller.kr_canProcessConnectClick()) {
} return;
final current = controller.kr_subscribeService.kr_currentSubscribe.value;
bool hasValidSubscription = false;
if (current != null) {
DateTime? expire;
try {
expire = DateTime.parse(current.expireTime);
} catch (_) {
expire = null;
} }
hasValidSubscription = expire != null && expire.isAfter(DateTime.now()); final current = controller
} .kr_subscribeService.kr_currentSubscribe.value;
if (hasValidSubscription) { bool hasValidSubscription = false;
controller.kr_toggleSwitch(!controller.kr_isConnected.value); if (current != null) {
} else { DateTime? expire;
HIDialog.show( try {
customMessageWidget: Padding( expire = DateTime.parse(current.expireTime);
padding: EdgeInsets.only(top: 16.w), } catch (_) {
child: Text( expire = null;
'尚未购买套餐,请前往购买页面下单后即可开始极速网络体验', }
textAlign: TextAlign.left, hasValidSubscription =
style: KrAppTextStyle( expire != null && expire.isAfter(DateTime.now());
color: Theme.of(context).textTheme.bodyMedium?.color, }
fontSize: 14.sp, if (hasValidSubscription) {
fontWeight: FontWeight.w600, controller.kr_toggleSwitch(
!controller.kr_isConnected.value);
} else {
HIDialog.show(
customMessageWidget: Padding(
padding: EdgeInsets.only(top: 16.w),
child: Text(
'尚未购买套餐,请前往购买页面下单后即可开始极速网络体验',
textAlign: TextAlign.left,
style: KrAppTextStyle(
color: Theme.of(context)
.textTheme
.bodyMedium
?.color,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
), ),
), ),
), cancelText: '取消',
cancelText: '取消', confirmText: '前往',
confirmText: '前往', onConfirm: () {
onConfirm: () { GlobalOverlayService.instance
GlobalOverlayService.instance.triggerSubscriptionAnimation(); .triggerSubscriptionAnimation();
}, },
); );
} }
}, },
child: SizedBox( child: SizedBox(
width: currentButtonSize, width: currentButtonSize,
height: currentButtonSize, height: currentButtonSize,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
if (isShow)
/// isShow = true
Stack( Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
@ -148,52 +131,25 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
controller.kr_connectText.value == '已连接' controller.kr_connectText.value == '已连接'
? '已连接\n点击断开' ? '已连接\n点击断开'
: controller.kr_connectText.value, : controller.kr_connectText.value,
key: ValueKey(controller.kr_connectText.value), key: ValueKey(
controller.kr_connectText.value),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
color: Colors.black, color: Colors.black,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
height: 1 height: 1,
), ),
), ),
), ),
], ],
)
else
/// isShow = false
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
KrLocalImage(
imageName: "hi-home-logo",
width: 104,
imageType: ImageType.svg,
),
SizedBox(height: 2.h),
Text(
controller.kr_connectText.value == '已连接'
? '已连接\n点击断开'
: controller.kr_connectText.value,
key: ValueKey(controller.kr_connectText.value),
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
],
), ),
], ],
),
), ),
), ),
), ),
), ),
], ],
), ),
), ),
@ -202,6 +158,7 @@ class HIAnimatedConnectButton extends GetView<KRHomeController> {
}); });
} }
} }
/// 270270~47910% /// 270270~47910%
/// 270~470 /// 270~470
class ContinuousRippleEffect extends StatefulWidget { class ContinuousRippleEffect extends StatefulWidget {
@ -232,10 +189,11 @@ class _ContinuousRippleEffectState extends State<ContinuousRippleEffect>
// //
final duration = Duration(milliseconds: 3000 + i * 1200); final duration = Duration(milliseconds: 3000 + i * 1200);
final controller = final controller = AnimationController(vsync: this, duration: duration)
AnimationController(vsync: this, duration: duration)..repeat(reverse: true); ..repeat(reverse: true);
final curved = CurvedAnimation(parent: controller, curve: Curves.easeInOutSine); final curved =
CurvedAnimation(parent: controller, curve: Curves.easeInOutSine);
_controllers.add(controller); _controllers.add(controller);
_animations.add(curved); _animations.add(curved);
} }