Fix the position of the Android-style spell check toolbar (#124897)
The spell check menu now appears directly below the misspelled word on Android.
This commit is contained in:
parent
3ab8cd2615
commit
98aaf00a09
@ -10,7 +10,6 @@ import 'adaptive_text_selection_toolbar.dart';
|
||||
import 'colors.dart';
|
||||
import 'material.dart';
|
||||
import 'spell_check_suggestions_toolbar_layout_delegate.dart';
|
||||
import 'text_selection_toolbar.dart';
|
||||
import 'text_selection_toolbar_text_button.dart';
|
||||
|
||||
// The default height of the SpellCheckSuggestionsToolbar, which
|
||||
@ -74,10 +73,6 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
/// suggestions toolbar.
|
||||
final List<ContextMenuButtonItem> buttonItems;
|
||||
|
||||
/// Padding between the toolbar and the anchor. Eyeballed on Pixel 4 emulator
|
||||
/// running Android API 31.
|
||||
static const double kToolbarContentDistanceBelow = TextSelectionToolbar.kHandleSize - 3.0;
|
||||
|
||||
/// Builds the button items for the toolbar based on the available
|
||||
/// spell check suggestions.
|
||||
static List<ContextMenuButtonItem>? buildButtonItems(
|
||||
@ -153,6 +148,8 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
|
||||
/// Determines the Offset that the toolbar will be anchored to.
|
||||
static Offset getToolbarAnchor(TextSelectionToolbarAnchors anchors) {
|
||||
// Since this will be positioned below the anchor point, use the secondary
|
||||
// anchor by default.
|
||||
return anchors.secondaryAnchor == null ? anchors.primaryAnchor : anchors.secondaryAnchor!;
|
||||
}
|
||||
|
||||
@ -190,24 +187,26 @@ class SpellCheckSuggestionsToolbar extends StatelessWidget {
|
||||
final double spellCheckSuggestionsToolbarHeight =
|
||||
_kDefaultToolbarHeight - (48.0 * (4 - buttonItems.length));
|
||||
// Incorporate the padding distance between the content and toolbar.
|
||||
final Offset anchorPadded =
|
||||
anchor + const Offset(0.0, kToolbarContentDistanceBelow);
|
||||
final MediaQueryData mediaQueryData = MediaQuery.of(context);
|
||||
final double softKeyboardViewInsetsBottom = mediaQueryData.viewInsets.bottom;
|
||||
final double paddingAbove = mediaQueryData.padding.top + CupertinoTextSelectionToolbar.kToolbarScreenPadding;
|
||||
final double paddingAbove = mediaQueryData.padding.top
|
||||
+ CupertinoTextSelectionToolbar.kToolbarScreenPadding;
|
||||
// Makes up for the Padding.
|
||||
final Offset localAdjustment = Offset(CupertinoTextSelectionToolbar.kToolbarScreenPadding, paddingAbove);
|
||||
final Offset localAdjustment = Offset(
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
||||
paddingAbove,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
||||
kToolbarContentDistanceBelow,
|
||||
paddingAbove,
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding,
|
||||
CupertinoTextSelectionToolbar.kToolbarScreenPadding + softKeyboardViewInsetsBottom,
|
||||
),
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: SpellCheckSuggestionsToolbarLayoutDelegate(
|
||||
anchor: anchorPadded - localAdjustment,
|
||||
anchor: anchor - localAdjustment,
|
||||
),
|
||||
child: AnimatedSize(
|
||||
// This duration was eyeballed on a Pixel 2 emulator running Android
|
||||
|
@ -3994,7 +3994,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|| platformNotSupported
|
||||
|| widget.readOnly
|
||||
|| _selectionOverlay == null
|
||||
|| !_spellCheckResultsReceived) {
|
||||
|| !_spellCheckResultsReceived
|
||||
|| findSuggestionSpanAtCursorIndex(textEditingValue.selection.extentOffset) == null) {
|
||||
// Only attempt to show the spell check suggestions toolbar if there
|
||||
// is a toolbar specified and spell check suggestions available to show.
|
||||
return false;
|
||||
|
@ -2210,12 +2210,12 @@ class TextSelectionGestureDetectorBuilder {
|
||||
// On desktop platforms the selection is set on tap down.
|
||||
case TargetPlatform.android:
|
||||
editableText.hideToolbar();
|
||||
editableText.showSpellCheckSuggestionsToolbar();
|
||||
if (isShiftPressedValid) {
|
||||
_extendSelection(details.globalPosition, SelectionChangedCause.tap);
|
||||
return;
|
||||
}
|
||||
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
|
||||
editableText.showSpellCheckSuggestionsToolbar();
|
||||
case TargetPlatform.fuchsia:
|
||||
editableText.hideToolbar();
|
||||
if (isShiftPressedValid) {
|
||||
@ -2276,8 +2276,7 @@ class TextSelectionGestureDetectorBuilder {
|
||||
} else {
|
||||
editableText.toggleToolbar(false);
|
||||
}
|
||||
}
|
||||
else if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed) || (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame)) && renderEditable.hasFocus) {
|
||||
} else if (((_positionWasOnSelectionExclusive(textPosition) && !previousSelection.isCollapsed) || (_positionWasOnSelectionInclusive(textPosition) && previousSelection.isCollapsed && isAffinityTheSame)) && renderEditable.hasFocus) {
|
||||
editableText.toggleToolbar(false);
|
||||
} else {
|
||||
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
|
||||
|
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/cupertino.dart' show CupertinoTextSelectionToolbar;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -50,9 +49,6 @@ void main() {
|
||||
|
||||
testWidgets('positions toolbar below anchor when it fits above bottom view padding', (WidgetTester tester) async {
|
||||
// We expect the toolbar to be positioned right below the anchor with padding accounted for.
|
||||
const double expectedToolbarY =
|
||||
_kAnchor + (2 * SpellCheckSuggestionsToolbar.kToolbarContentDistanceBelow) - CupertinoTextSelectionToolbar.kToolbarScreenPadding;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
@ -65,13 +61,12 @@ void main() {
|
||||
);
|
||||
|
||||
final double toolbarY = tester.getTopLeft(findSpellCheckSuggestionsToolbar()).dy;
|
||||
expect(toolbarY, equals(expectedToolbarY));
|
||||
expect(toolbarY, equals(_kAnchor));
|
||||
});
|
||||
|
||||
testWidgets('re-positions toolbar higher below anchor when it does not fit above bottom view padding', (WidgetTester tester) async {
|
||||
// We expect the toolbar to be positioned _kTestToolbarOverlap pixels above the anchor with padding accounted for.
|
||||
const double expectedToolbarY =
|
||||
_kAnchor + (2 * SpellCheckSuggestionsToolbar.kToolbarContentDistanceBelow) - CupertinoTextSelectionToolbar.kToolbarScreenPadding - _kTestToolbarOverlap;
|
||||
// We expect the toolbar to be positioned _kTestToolbarOverlap pixels above the anchor.
|
||||
const double expectedToolbarY = _kAnchor - _kTestToolbarOverlap;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
@ -15542,6 +15542,67 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
|
||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
|
||||
skip: kIsWeb, // [intended]
|
||||
);
|
||||
|
||||
testWidgets('tapping on a misspelled word hides the handles', (WidgetTester tester) async {
|
||||
tester.binding.platformDispatcher.nativeSpellCheckServiceDefinedTestValue =
|
||||
true;
|
||||
controller.value = const TextEditingValue(
|
||||
// All misspellings of "test". One the same length, one shorter, and one
|
||||
// longer.
|
||||
text: 'test test testt',
|
||||
selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 0, extentOffset: 4),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
showSelectionHandles: true,
|
||||
spellCheckConfiguration:
|
||||
const SpellCheckConfiguration(
|
||||
misspelledTextStyle: TextField.materialMisspelledTextStyle,
|
||||
spellCheckSuggestionsToolbarBuilder: TextField.defaultSpellCheckSuggestionsToolbarBuilder,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state =
|
||||
tester.state<EditableTextState>(find.byType(EditableText));
|
||||
|
||||
state.spellCheckResults = SpellCheckResults(
|
||||
controller.value.text,
|
||||
const <SuggestionSpan>[
|
||||
SuggestionSpan(TextRange(start: 10, end: 15), <String>['test']),
|
||||
]);
|
||||
await tester.tapAt(textOffsetToPosition(tester, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.showSpellCheckSuggestionsToolbar(), isFalse);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('test'), findsNothing);
|
||||
expect(state.selectionOverlay!.handlesAreVisible, isTrue);
|
||||
|
||||
await tester.tapAt(textOffsetToPosition(tester, 12));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.showSpellCheckSuggestionsToolbar(), isTrue);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('test'), findsOneWidget);
|
||||
expect(state.selectionOverlay!.handlesAreVisible, isFalse);
|
||||
|
||||
await tester.tapAt(textOffsetToPosition(tester, 5));
|
||||
await tester.pumpAndSettle();
|
||||
expect(state.showSpellCheckSuggestionsToolbar(), isFalse);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('test'), findsNothing);
|
||||
expect(state.selectionOverlay!.handlesAreVisible, isTrue);
|
||||
},
|
||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||
skip: kIsWeb, // [intended]
|
||||
);
|
||||
});
|
||||
|
||||
group('magnifier', () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user