From d2f2459a96b2337ffe7998d98f96407195b265fb Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Wed, 21 Oct 2026 14:54:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(player):=20=E5=9B=BA=E5=AE=9A=E8=AF=84?= =?UTF-8?q?=E8=AE=BA=E8=BE=93=E5=85=A5=E6=B5=AE=E5=B1=82=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E6=94=B6=E9=94=AE=E7=9B=98=E8=B7=B3=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/comment_composer_sheet.dart | 50 ++++++++++++++--- test/comment/drama_comments_sheet_test.dart | 54 +++++++++++++++++++ 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/lib/pages/player/widgets/comment_composer_sheet.dart b/lib/pages/player/widgets/comment_composer_sheet.dart index 6a9e0dc..ff2f41e 100644 --- a/lib/pages/player/widgets/comment_composer_sheet.dart +++ b/lib/pages/player/widgets/comment_composer_sheet.dart @@ -20,6 +20,8 @@ const double _sheetRadius = 20; const double _inputRadius = 12; const double _postButtonHeight = 32; const int _maxCommentLength = 300; +const Duration _keyboardSettleDelay = Duration(milliseconds: 80); +const double _maxKeyboardInsetRatio = 0.6; /// 独立评论输入弹窗,复用原评论控制器的发布与列表更新能力。 class CommentComposerSheet extends StatefulWidget { @@ -44,11 +46,13 @@ class _CommentComposerSheetState extends State { final _input = TextEditingController(); final _inputFocus = FocusNode(); final _keyboardInset = KeyboardInsetNotifier.instance; + Timer? _keyboardSettleTimer; double _pinnedKeyboardInset = 0; int _insetBaseline = 0; bool _pinnedInitialized = false; bool _insetReady = false; bool _useChannelInset = false; + bool _keyboardVisible = false; @override void initState() { @@ -62,9 +66,12 @@ class _CommentComposerSheetState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - if (_pinnedInitialized) return; - _pinnedInitialized = true; - _pinnedKeyboardInset = MediaQuery.viewInsetsOf(context).bottom; + if (!_pinnedInitialized) { + _pinnedInitialized = true; + _pinnedKeyboardInset = + _normalizeKeyboardInset(MediaQuery.viewInsetsOf(context).bottom) ?? 0; + } + _trackKeyboardInset(_keyboardHeight(context)); } Future _prepareInput() async { @@ -89,6 +96,7 @@ class _CommentComposerSheetState extends State { _input.removeListener(_onInputChanged); _keyboardInset.removeListener(_onKeyboardInsetChanged); widget.controller.removeListener(_onControllerChanged); + _keyboardSettleTimer?.cancel(); _inputFocus.unfocus(); _input.dispose(); _inputFocus.dispose(); @@ -108,7 +116,7 @@ class _CommentComposerSheetState extends State { if (_keyboardInset.value != _insetBaseline) { _useChannelInset = true; } - setState(() {}); + _trackKeyboardInset(_keyboardHeight(context)); } double _keyboardHeight(BuildContext context) { @@ -119,6 +127,38 @@ class _CommentComposerSheetState extends State { return math.max(viewInset, channelInset); } + double? _normalizeKeyboardInset(double inset) { + final maxInset = MediaQuery.sizeOf(context).height * _maxKeyboardInsetRatio; + if (!inset.isFinite || inset > maxInset) return null; + return math.max(0, inset).roundToDouble(); + } + + void _trackKeyboardInset(double inset) { + final visible = inset > 0; + if (visible == _keyboardVisible) return; + _keyboardVisible = visible; + _keyboardSettleTimer?.cancel(); + _keyboardSettleTimer = null; + if (!visible) return; + _keyboardSettleTimer = Timer( + _keyboardSettleDelay, + _captureSettledKeyboardInset, + ); + } + + void _captureSettledKeyboardInset() { + _keyboardSettleTimer = null; + if (!mounted) return; + final inset = _keyboardHeight(context); + if (inset <= 0) { + _keyboardVisible = false; + return; + } + final next = _normalizeKeyboardInset(inset); + if (next == null || next == _pinnedKeyboardInset) return; + setState(() => _pinnedKeyboardInset = next); + } + Future _submit() async { if (widget.controller.submitting) return; final text = _input.text.trim(); @@ -148,8 +188,6 @@ class _CommentComposerSheetState extends State { @override Widget build(BuildContext context) { - final currentKeyboardInset = _keyboardHeight(context); - _pinnedKeyboardInset = math.max(_pinnedKeyboardInset, currentKeyboardInset); final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; final hasContent = _input.text.trim().isNotEmpty; diff --git a/test/comment/drama_comments_sheet_test.dart b/test/comment/drama_comments_sheet_test.dart index bb65995..6ac6907 100644 --- a/test/comment/drama_comments_sheet_test.dart +++ b/test/comment/drama_comments_sheet_test.dart @@ -469,6 +469,60 @@ void main() { expect(tester.getBottomLeft(composer).dy, closeTo(844, 0.1)); }); + testWidgets( + 'should_keep_composer_position_when_keyboard_insets_change_after_settling', + (tester) async { + tester.view.physicalSize = const Size(390, 2200); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + addTearDown(tester.view.resetViewInsets); + final repository = FakeCommentRepository( + pages: [ + CommentPage( + comments: [commentFixture(id: '10', content: 'A comment')], + ), + ], + ); + + await _pumpSheet(tester, repository: repository); + await tester.tap(find.byKey(const Key('comment-input-placeholder'))); + await tester.pumpAndSettle(); + + final composer = find.byKey(const Key('comment-composer-sheet')); + expect(composer, findsOneWidget); + + tester.view.viewInsets = const FakeViewPadding(bottom: 1100); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 90)); + final settledTop = tester.getTopLeft(composer).dy; + final settledBottom = tester.getBottomLeft(composer).dy; + final settledHeight = tester.getSize(composer).height; + expect(settledBottom, closeTo(2200 - 1100, 0.1)); + + tester.view.viewInsets = const FakeViewPadding(bottom: 1150); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 90)); + expect(tester.getTopLeft(composer).dy, closeTo(settledTop, 0.1)); + expect(tester.getBottomLeft(composer).dy, closeTo(settledBottom, 0.1)); + expect(tester.getSize(composer).height, closeTo(settledHeight, 0.1)); + + tester.view.viewInsets = const FakeViewPadding(bottom: 600); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 90)); + expect(tester.getTopLeft(composer).dy, closeTo(settledTop, 0.1)); + expect(tester.getBottomLeft(composer).dy, closeTo(settledBottom, 0.1)); + expect(tester.getSize(composer).height, closeTo(settledHeight, 0.1)); + + tester.view.viewInsets = const FakeViewPadding(); + await tester.pumpAndSettle(); + expect(composer, findsOneWidget); + expect(tester.getTopLeft(composer).dy, closeTo(settledTop, 0.1)); + expect(tester.getBottomLeft(composer).dy, closeTo(settledBottom, 0.1)); + expect(tester.getSize(composer).height, closeTo(settledHeight, 0.1)); + }, + ); + testWidgets('should_keep_composer_open_after_input_loses_focus', ( tester, ) async {