merge: 合并 K86.21 评论输入浮层稳定定位修复
This commit is contained in:
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 {
|
||||||
|
|||||||
Reference in new issue
Block a user