From a66cdb2e1ba44ca9e342e0acb0e0513bc08ffc86 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Wed, 21 Oct 2026 14:43:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(player):=20=E4=BF=AE=E6=AD=A3=E8=AF=84?= =?UTF-8?q?=E8=AE=BA=E8=BE=93=E5=85=A5=E6=B5=AE=E5=B1=82=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E9=94=AE=E7=9B=98=E9=AB=98=E5=BA=A6=E4=B8=8E=E8=81=9A=E7=84=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/comment_composer_sheet.dart | 304 ++++++++++-------- test/comment/drama_comments_sheet_test.dart | 46 +++ 2 files changed, 219 insertions(+), 131 deletions(-) diff --git a/lib/pages/player/widgets/comment_composer_sheet.dart b/lib/pages/player/widgets/comment_composer_sheet.dart index 1ccfd0e..6a9e0dc 100644 --- a/lib/pages/player/widgets/comment_composer_sheet.dart +++ b/lib/pages/player/widgets/comment_composer_sheet.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../../core/data/models/comment.dart'; @@ -44,6 +45,10 @@ class _CommentComposerSheetState extends State { final _inputFocus = FocusNode(); final _keyboardInset = KeyboardInsetNotifier.instance; double _pinnedKeyboardInset = 0; + int _insetBaseline = 0; + bool _pinnedInitialized = false; + bool _insetReady = false; + bool _useChannelInset = false; @override void initState() { @@ -51,9 +56,31 @@ class _CommentComposerSheetState extends State { _input.addListener(_onInputChanged); _keyboardInset.addListener(_onKeyboardInsetChanged); widget.controller.addListener(_onControllerChanged); - unawaited(_keyboardInset.start()); + unawaited(_prepareInput()); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_pinnedInitialized) return; + _pinnedInitialized = true; + _pinnedKeyboardInset = MediaQuery.viewInsetsOf(context).bottom; + } + + Future _prepareInput() async { + await _keyboardInset.start(); + if (!mounted) return; + _insetBaseline = _keyboardInset.value; + _insetReady = true; WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) _inputFocus.requestFocus(); + if (!mounted) return; + _inputFocus.requestFocus(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _keyboardHeight(context) > 0) return; + unawaited( + SystemChannels.textInput.invokeMethod('TextInput.show'), + ); + }); }); } @@ -77,14 +104,20 @@ class _CommentComposerSheetState extends State { } void _onKeyboardInsetChanged() { - if (!mounted) return; + if (!mounted || !_insetReady) return; + if (_keyboardInset.value != _insetBaseline) { + _useChannelInset = true; + } setState(() {}); } - double _keyboardHeight(BuildContext context) => math.max( - MediaQuery.viewInsetsOf(context).bottom, - _keyboardInset.value / MediaQuery.devicePixelRatioOf(context), - ); + double _keyboardHeight(BuildContext context) { + final viewInset = MediaQuery.viewInsetsOf(context).bottom; + final channelInset = _useChannelInset + ? _keyboardInset.value / MediaQuery.devicePixelRatioOf(context) + : 0.0; + return math.max(viewInset, channelInset); + } Future _submit() async { if (widget.controller.submitting) return; @@ -123,137 +156,146 @@ class _CommentComposerSheetState extends State { final canPost = signedIn && hasContent && !widget.controller.submitting; final multiline = _input.text.contains('\n'); - return Stack( - key: const Key('comment-composer-overlay'), - fit: StackFit.expand, - children: [ - Positioned.fill( - child: GestureDetector( - key: const Key('comment-composer-dismiss-layer'), - behavior: HitTestBehavior.opaque, - onTap: widget.onDismiss, + return FocusScope( + key: const Key('comment-composer-focus-scope'), + autofocus: true, + child: Stack( + key: const Key('comment-composer-overlay'), + fit: StackFit.expand, + children: [ + Positioned.fill( + child: GestureDetector( + key: const Key('comment-composer-dismiss-layer'), + behavior: HitTestBehavior.opaque, + onTap: widget.onDismiss, + ), ), - ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - child: Material( - key: const Key('comment-composer-panel'), - color: AppColors.card, - clipBehavior: Clip.antiAlias, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(_sheetRadius), - ), - child: SafeArea( - top: false, - child: Padding( - padding: EdgeInsets.only(bottom: _pinnedKeyboardInset), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: Material( + key: const Key('comment-composer-panel'), + color: AppColors.card, + clipBehavior: Clip.antiAlias, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(_sheetRadius), + ), + child: SafeArea( + top: false, child: Padding( - key: const Key('comment-composer-sheet'), - padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), - child: TextFieldTapRegion( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (widget.replyTo != null) - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text( - l10n.replyingTo( - widget.replyTo!.nickname.isEmpty - ? l10n.anonymousUser - : widget.replyTo!.nickname, - ), - style: const TextStyle( - color: AppColors.muted, - fontSize: 12, - ), - ), - ), - Container( - decoration: BoxDecoration( - color: AppColors.commentInput, - borderRadius: BorderRadius.circular(_inputRadius), - ), - child: TextField( - key: const Key('comment-composer-input'), - controller: _input, - focusNode: _inputFocus, - readOnly: !signedIn, - canRequestFocus: signedIn, - minLines: 1, - maxLines: 4, - maxLength: _maxCommentLength, - textAlignVertical: multiline - ? TextAlignVertical.top - : TextAlignVertical.center, - style: TextStyle( - color: AppColors.cream, - fontSize: 15, - height: multiline ? 1.4 : null, - ), - cursorColor: AppColors.blue, - decoration: InputDecoration( - hintText: signedIn - ? l10n.writeComment - : l10n.commentLoginHint, - hintStyle: const TextStyle( - color: AppColors.muted, - fontSize: 14, - ), - counterText: '', - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - ), - onTap: signedIn ? null : _authenticate, - ), - ), - const SizedBox(height: 12), - Align( - alignment: Alignment.centerRight, - child: SizedBox( - height: _postButtonHeight, - child: TextButton( - key: const Key('comment-composer-post'), - onPressed: canPost ? _submit : null, - style: TextButton.styleFrom( - minimumSize: const Size(0, _postButtonHeight), - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - tapTargetSize: - MaterialTapTargetSize.shrinkWrap, - backgroundColor: hasContent - ? AppColors.blue.withValues(alpha: 0.12) - : AppColors.raised, - foregroundColor: AppColors.blue, - disabledForegroundColor: AppColors.muted, - disabledBackgroundColor: AppColors.raised, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), + padding: EdgeInsets.only(bottom: _pinnedKeyboardInset), + child: Padding( + key: const Key('comment-composer-sheet'), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), + child: TextFieldTapRegion( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.replyTo != null) + Padding( + padding: const EdgeInsets.only(bottom: 8), child: Text( - l10n.postComment, + l10n.replyingTo( + widget.replyTo!.nickname.isEmpty + ? l10n.anonymousUser + : widget.replyTo!.nickname, + ), style: const TextStyle( + color: AppColors.muted, + fontSize: 12, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: AppColors.commentInput, + borderRadius: BorderRadius.circular( + _inputRadius, + ), + ), + child: TextField( + key: const Key('comment-composer-input'), + controller: _input, + focusNode: _inputFocus, + readOnly: !signedIn, + canRequestFocus: signedIn, + minLines: 1, + maxLines: 4, + maxLength: _maxCommentLength, + textAlignVertical: multiline + ? TextAlignVertical.top + : TextAlignVertical.center, + style: TextStyle( + color: AppColors.cream, + fontSize: 15, + height: multiline ? 1.4 : null, + ), + cursorColor: AppColors.blue, + decoration: InputDecoration( + hintText: signedIn + ? l10n.writeComment + : l10n.commentLoginHint, + hintStyle: const TextStyle( + color: AppColors.muted, fontSize: 14, - fontWeight: FontWeight.w600, + ), + counterText: '', + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + onTap: signedIn ? null : _authenticate, + ), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: SizedBox( + height: _postButtonHeight, + child: TextButton( + key: const Key('comment-composer-post'), + onPressed: canPost ? _submit : null, + style: TextButton.styleFrom( + minimumSize: const Size( + 0, + _postButtonHeight, + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + backgroundColor: hasContent + ? AppColors.blue.withValues(alpha: 0.12) + : AppColors.raised, + foregroundColor: AppColors.blue, + disabledForegroundColor: AppColors.muted, + disabledBackgroundColor: AppColors.raised, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + l10n.postComment, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), ), ), ), ), - ), - ], + ], + ), ), ), ), @@ -261,8 +303,8 @@ class _CommentComposerSheetState extends State { ), ), ), - ), - ], + ], + ), ); } } diff --git a/test/comment/drama_comments_sheet_test.dart b/test/comment/drama_comments_sheet_test.dart index 2b6bf7e..aad5a5a 100644 --- a/test/comment/drama_comments_sheet_test.dart +++ b/test/comment/drama_comments_sheet_test.dart @@ -2,7 +2,9 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/models/comment.dart'; import 'package:fruitshorts/core/data/models/drama.dart'; @@ -33,6 +35,50 @@ const _drama = Drama( ); void main() { + testWidgets('should_ignore_stale_keyboard_inset_when_composer_opens', ( + tester, + ) async { + const keyboardChannel = MethodChannel('com.stormg.lumadrama/keyboard'); + debugDefaultTargetPlatformOverride = TargetPlatform.android; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + keyboardChannel, + (call) async => call.method == 'get' ? 300 : null, + ); + addTearDown(() { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + keyboardChannel, + null, + ); + debugDefaultTargetPlatformOverride = null; + }); + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + final repository = FakeCommentRepository(pages: [const CommentPage()]); + + 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')); + final panel = find.byKey(const Key('comment-composer-panel')); + final input = find.byKey(const Key('comment-composer-input')); + expect( + find.byKey(const Key('comment-composer-focus-scope')), + findsOneWidget, + ); + expect(tester.getSize(composer).height, inInclusiveRange(100, 150)); + expect( + tester.getSize(panel).height, + closeTo(tester.getSize(composer).height, 1), + ); + expect(tester.getBottomLeft(panel).dy, closeTo(844, 0.1)); + expect(tester.widget(input).focusNode?.hasFocus, isTrue); + expect(tester.testTextInput.isVisible, isTrue); + debugDefaultTargetPlatformOverride = null; + }); + setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.withData({