merge: 合并 K86.21 评论输入浮层稳定定位修复

This commit is contained in:
Zhou Weidong committed 2026-10-21 14:54:12 +08:00
commit 036bd29855
2 files changed
+98 -6

No files matched your search

@@ -20,6 +20,8 @@ const double _sheetRadius = 20;
const double _inputRadius = 12; const double _inputRadius = 12;
const double _postButtonHeight = 32; const double _postButtonHeight = 32;
const int _maxCommentLength = 300; const int _maxCommentLength = 300;
const Duration _keyboardSettleDelay = Duration(milliseconds: 80);
const double _maxKeyboardInsetRatio = 0.6;
/// 独立评论输入弹窗,复用原评论控制器的发布与列表更新能力。 /// 独立评论输入弹窗,复用原评论控制器的发布与列表更新能力。
class CommentComposerSheet extends StatefulWidget { class CommentComposerSheet extends StatefulWidget {
@@ -44,11 +46,13 @@ class _CommentComposerSheetState extends State<CommentComposerSheet> {
final _input = TextEditingController(); final _input = TextEditingController();
final _inputFocus = FocusNode(); final _inputFocus = FocusNode();
final _keyboardInset = KeyboardInsetNotifier.instance; final _keyboardInset = KeyboardInsetNotifier.instance;
Timer? _keyboardSettleTimer;
double _pinnedKeyboardInset = 0; double _pinnedKeyboardInset = 0;
int _insetBaseline = 0; int _insetBaseline = 0;
bool _pinnedInitialized = false; bool _pinnedInitialized = false;
bool _insetReady = false; bool _insetReady = false;
bool _useChannelInset = false; bool _useChannelInset = false;
bool _keyboardVisible = false;
@override @override
void initState() { void initState() {
@@ -62,9 +66,12 @@ class _CommentComposerSheetState extends State<CommentComposerSheet> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
if (_pinnedInitialized) return; if (!_pinnedInitialized) {
_pinnedInitialized = true; _pinnedInitialized = true;
_pinnedKeyboardInset = MediaQuery.viewInsetsOf(context).bottom; _pinnedKeyboardInset =
_normalizeKeyboardInset(MediaQuery.viewInsetsOf(context).bottom) ?? 0;
}
_trackKeyboardInset(_keyboardHeight(context));
} }
Future<void> _prepareInput() async { Future<void> _prepareInput() async {
@@ -89,6 +96,7 @@ class _CommentComposerSheetState extends State<CommentComposerSheet> {
_input.removeListener(_onInputChanged); _input.removeListener(_onInputChanged);
_keyboardInset.removeListener(_onKeyboardInsetChanged); _keyboardInset.removeListener(_onKeyboardInsetChanged);
widget.controller.removeListener(_onControllerChanged); widget.controller.removeListener(_onControllerChanged);
_keyboardSettleTimer?.cancel();
_inputFocus.unfocus(); _inputFocus.unfocus();
_input.dispose(); _input.dispose();
_inputFocus.dispose(); _inputFocus.dispose();
@@ -108,7 +116,7 @@ class _CommentComposerSheetState extends State<CommentComposerSheet> {
if (_keyboardInset.value != _insetBaseline) { if (_keyboardInset.value != _insetBaseline) {
_useChannelInset = true; _useChannelInset = true;
} }
setState(() {}); _trackKeyboardInset(_keyboardHeight(context));
} }
double _keyboardHeight(BuildContext context) { double _keyboardHeight(BuildContext context) {
@@ -119,6 +127,38 @@ class _CommentComposerSheetState extends State<CommentComposerSheet> {
return math.max(viewInset, channelInset); 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<void> _submit() async { Future<void> _submit() async {
if (widget.controller.submitting) return; if (widget.controller.submitting) return;
final text = _input.text.trim(); final text = _input.text.trim();
@@ -148,8 +188,6 @@ class _CommentComposerSheetState extends State<CommentComposerSheet> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentKeyboardInset = _keyboardHeight(context);
_pinnedKeyboardInset = math.max(_pinnedKeyboardInset, currentKeyboardInset);
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final signedIn = context.watch<AuthController>().isSignedIn; final signedIn = context.watch<AuthController>().isSignedIn;
final hasContent = _input.text.trim().isNotEmpty; final hasContent = _input.text.trim().isNotEmpty;
@@ -469,6 +469,60 @@ void main() {
expect(tester.getBottomLeft(composer).dy, closeTo(844, 0.1)); 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', ( testWidgets('should_keep_composer_open_after_input_loses_focus', (
tester, tester,
) async { ) async {