From c95996c251519387847041f916362c3a15507e96 Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Sat, 12 Sep 2026 20:06:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=20iOS=20=E4=B8=8A=E5=88=87=E5=88=B0?= =?UTF-8?q?=E5=90=8C=E4=BC=A0/=E9=9D=A2=E5=AF=B9=E9=9D=A2=E6=97=B6?= =?UTF-8?q?=E5=AD=97=E5=B9=95=E5=B0=8F=E7=AA=97=E5=85=B3=E4=B8=8D=E6=8E=89?= =?UTF-8?q?=E3=80=81=E4=B8=94=E4=B8=AD=E6=96=AD=20changeTranslationMode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _floatingWindowManager 是 late 且只在 onInit 的 `Platform.isAndroid` 分支里赋值 (iOS 走 initPlatformState 那套 SubtitlePip)。全项目 5 处引用里 4 处都带平台守卫, **只有 _disableFloatingWindow() 那处没有** —— 而它恰恰会被 changeTranslationMode 调到 (从多媒体/通话翻译切到同传或面对面时,小窗不受支持要自动关掉)。 iOS 上后果是连锁的,且没有一条指向真因:LateInitializationError 从 changeTranslationMode 中途抛出,后面的 _reinitializeAsrService() 再也走不到; 小窗又没真的停掉,继续浮在同传/面对面页上盖住顶部的标题与按钮;偏偏小窗开关按钮 只在 call/audioVideo 两个模式才渲染,用户连关都关不掉。 - _disableFloatingWindow() 分平台,并收敛成 toggle / onClose / changeTranslationMode 三处共用的唯一出口;Android 分支补 Get.isRegistered 守卫。 - 切到不支持小窗的模式时改成**无条件**关一次,不再依赖 isFloatingWindowEnabled: iOS 上这个标志位由 initPlatformState 里那次异步的 SubtitlePip.isActive() 回填, 和 onInit 紧接着调用的 changeTranslationMode 是竞态,回填晚一步就整个跳过。 没开着时多关一次是无害的。 真机验证(iPhone 12 / iOS 26.6.2):改前在运行中的 App 里直接取 _floatingWindowManager 确实抛 LateInitializationError;改后走完整路径(多媒体翻译 → 开小窗 → 切同传), changeTranslationMode 正常返回、pip=false、widget 树里 ErrorWidget 数为 0。 安卓侧(华为 ALN-AL80,debug + release)同传/面对面两页渲染不受影响。 Co-Authored-By: Claude Opus 5 (1M context) --- .../controllers/translation_controller.dart | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/apps/client/lib/modules/translation/controllers/translation_controller.dart b/apps/client/lib/modules/translation/controllers/translation_controller.dart index 6731f7d5..75ab3ec6 100644 --- a/apps/client/lib/modules/translation/controllers/translation_controller.dart +++ b/apps/client/lib/modules/translation/controllers/translation_controller.dart @@ -273,12 +273,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// 切换悬浮窗功能 Future toggleFloatingWindow() async { if (isFloatingWindowEnabled.value) { - if (Platform.isAndroid) { - await _disableFloatingWindow(); - } else if (Platform.isIOS) { - await SubtitlePip.stop(); - isFloatingWindowEnabled.value = false; - } + await _disableFloatingWindow(); } else { if (Platform.isAndroid) { await _enableFloatingWindow(); @@ -364,9 +359,24 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - /// 禁用悬浮窗功能 + /// 关掉悬浮字幕窗。 + /// + /// ⚠️ **必须分平台**:[_floatingWindowManager] 是 `late` 且只在 + /// [onInit] 的 `Platform.isAndroid` 分支里赋值(iOS 走 + /// [initPlatformState] 那套 SubtitlePip),在 iOS 上碰它就是 + /// LateInitializationError。而本方法会被 [changeTranslationMode] 调到 + /// ——从多媒体/通话翻译切到同传或面对面时,小窗不受支持要自动关掉。 + /// 少了这层判断,iOS 上的后果是连锁的、且没有一条指向真因: + /// 异常从 [changeTranslationMode] 中途抛出,后面的 + /// [_reinitializeAsrService] 再也走不到;小窗又没真的停掉, + /// 继续浮在同传/面对面页上盖住顶部的标题与按钮;偏偏小窗开关按钮 + /// 只在 call/audioVideo 两个模式才渲染,用户连关都关不掉。 Future _disableFloatingWindow() async { - await _floatingWindowManager.disableFloatingWindow(); + if (Platform.isIOS) { + await SubtitlePip.stop(); + } else if (Get.isRegistered()) { + await _floatingWindowManager.disableFloatingWindow(); + } isFloatingWindowEnabled.value = false; // Get.snackbar( @@ -451,24 +461,15 @@ class TranslationController extends GetxController with WidgetsBindingObserver { await _ttsService.setTtsMode('normal'); } - // 检查悬浮窗支持情况 - if (isFloatingWindowEnabled.value && !_isFloatingWindowSupportedMode()) { - // 当前模式不支持悬浮窗,自动禁用 + // 小窗只支持 call / audioVideo,切到别的模式一律关掉。 + // ⚠️ **不要再加 `isFloatingWindowEnabled.value &&` 这个前置条件**: + // iOS 上这个标志位是 [initPlatformState] 里那次异步的 + // `SubtitlePip.isActive()` 回填的,而 onInit 紧接着就调本方法, + // 两者是竞态——回填晚一步,这里就当成「没开」直接跳过,小窗继续浮在 + // 同传/面对面页上盖住顶部按钮,而小窗开关只在 call/audioVideo 渲染, + // 用户连关都关不掉。没开着时多关一次是无害的。 + if (!_isFloatingWindowSupportedMode()) { await _disableFloatingWindow(); - // Get.snackbar( - // '模式已切换', - // '悬浮窗仅支持通话和音视频翻译模式', - // snackPosition: SnackPosition.TOP, - // ); - } else if (!isFloatingWindowEnabled.value && - _isFloatingWindowSupportedMode()) { - // 切换到支持悬浮窗的模式,提示用户可以启用 - // Get.snackbar( - // '模式已切换', - // '可以启用悬浮窗显示翻译结果', - // snackPosition: SnackPosition.TOP, - // duration: const Duration(seconds: 4), - // ); } if (mode == 'call') { @@ -1087,11 +1088,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { // _asrService.dispose(); // _astService.dispose(); - if (Platform.isAndroid) { - _disableFloatingWindow(); - } else if (Platform.isIOS) { - unawaited(SubtitlePip.stop()); - } + unawaited(_disableFloatingWindow()); // 释放管理器资源 _timerManager.dispose();