mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Hide text selection toolbar when dragging handles on mobile (#104274)
This commit is contained in:
parent
dffddf00a2
commit
4ec4c249ea
@ -29,6 +29,11 @@ export 'package:flutter/services.dart' show TextSelectionDelegate;
|
||||
/// called.
|
||||
const Duration _kDragSelectionUpdateThrottle = Duration(milliseconds: 50);
|
||||
|
||||
/// A duration that determines the delay before showing the text selection
|
||||
/// toolbar again after dragging either selection handle on Android. Eyeballed
|
||||
/// on a Pixel 5 Android API 31 emulator.
|
||||
const Duration _kAndroidPostDragShowDelay = Duration(milliseconds: 300);
|
||||
|
||||
/// Signature for when a pointer that's dragging to select text has moved again.
|
||||
///
|
||||
/// The first argument [startDetails] contains the details of the event that
|
||||
@ -782,6 +787,8 @@ class SelectionOverlay {
|
||||
/// Controls the fade-in and fade-out animations for the toolbar and handles.
|
||||
static const Duration fadeDuration = Duration(milliseconds: 150);
|
||||
|
||||
Timer? _androidPostDragShowTimer;
|
||||
|
||||
/// A pair of handles. If this is non-null, there are always 2, though the
|
||||
/// second is hidden when the selection is collapsed.
|
||||
List<OverlayEntry>? _handles;
|
||||
@ -821,7 +828,7 @@ class SelectionOverlay {
|
||||
/// Shows the toolbar by inserting it into the [context]'s overlay.
|
||||
/// {@endtemplate}
|
||||
void showToolbar() {
|
||||
if (_toolbar != null) {
|
||||
if (_toolbar != null || (_hideToolbarWhenDraggingHandles && _isDraggingHandles)) {
|
||||
return;
|
||||
}
|
||||
_toolbar = OverlayEntry(builder: _buildToolbar);
|
||||
@ -888,9 +895,62 @@ class SelectionOverlay {
|
||||
/// Disposes this object and release resources.
|
||||
/// {@endtemplate}
|
||||
void dispose() {
|
||||
_androidPostDragShowTimer?.cancel();
|
||||
_androidPostDragShowTimer = null;
|
||||
hide();
|
||||
}
|
||||
|
||||
final bool _hideToolbarWhenDraggingHandles =
|
||||
<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }.contains(defaultTargetPlatform);
|
||||
|
||||
bool _isDraggingStartHandle = false;
|
||||
bool _isDraggingEndHandle = false;
|
||||
bool get _isDraggingHandles => _isDraggingStartHandle || _isDraggingEndHandle;
|
||||
|
||||
Future<void> _handleDragging() async {
|
||||
// Hide toolbar while dragging either handle on Android and iOS.
|
||||
if (!_hideToolbarWhenDraggingHandles)
|
||||
return;
|
||||
|
||||
if (_isDraggingHandles) {
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
_androidPostDragShowTimer?.cancel();
|
||||
_androidPostDragShowTimer = null;
|
||||
}
|
||||
hideToolbar();
|
||||
} else {
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
_androidPostDragShowTimer = Timer(_kAndroidPostDragShowDelay, showToolbar);
|
||||
} else {
|
||||
showToolbar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onStartHandleDragStart(DragStartDetails details) {
|
||||
_isDraggingStartHandle = true;
|
||||
_handleDragging();
|
||||
onStartHandleDragStart?.call(details);
|
||||
}
|
||||
|
||||
void _onStartHandleDragEnd(DragEndDetails details) {
|
||||
_isDraggingStartHandle = false;
|
||||
_handleDragging();
|
||||
onStartHandleDragEnd?.call(details);
|
||||
}
|
||||
|
||||
void _onEndHandleDragStart(DragStartDetails details) {
|
||||
_isDraggingEndHandle = true;
|
||||
_handleDragging();
|
||||
onEndHandleDragStart?.call(details);
|
||||
}
|
||||
|
||||
void _onEndHandleDragEnd(DragEndDetails details) {
|
||||
_isDraggingEndHandle = false;
|
||||
_handleDragging();
|
||||
onEndHandleDragEnd?.call(details);
|
||||
}
|
||||
|
||||
Widget _buildStartHandle(BuildContext context) {
|
||||
final Widget handle;
|
||||
final TextSelectionControls? selectionControls = this.selectionControls;
|
||||
@ -901,9 +961,9 @@ class SelectionOverlay {
|
||||
type: _startHandleType,
|
||||
handleLayerLink: startHandleLayerLink,
|
||||
onSelectionHandleTapped: onSelectionHandleTapped,
|
||||
onSelectionHandleDragStart: onStartHandleDragStart,
|
||||
onSelectionHandleDragStart: _onStartHandleDragStart,
|
||||
onSelectionHandleDragUpdate: onStartHandleDragUpdate,
|
||||
onSelectionHandleDragEnd: onStartHandleDragEnd,
|
||||
onSelectionHandleDragEnd: _onStartHandleDragEnd,
|
||||
selectionControls: selectionControls,
|
||||
visibility: startHandlesVisible,
|
||||
preferredLineHeight: _lineHeightAtStart,
|
||||
@ -926,9 +986,9 @@ class SelectionOverlay {
|
||||
type: _endHandleType,
|
||||
handleLayerLink: endHandleLayerLink,
|
||||
onSelectionHandleTapped: onSelectionHandleTapped,
|
||||
onSelectionHandleDragStart: onEndHandleDragStart,
|
||||
onSelectionHandleDragStart: _onEndHandleDragStart,
|
||||
onSelectionHandleDragUpdate: onEndHandleDragUpdate,
|
||||
onSelectionHandleDragEnd: onEndHandleDragEnd,
|
||||
onSelectionHandleDragEnd: _onEndHandleDragEnd,
|
||||
selectionControls: selectionControls,
|
||||
visibility: endHandlesVisible,
|
||||
preferredLineHeight: _lineHeightAtEnd,
|
||||
|
||||
@ -3179,6 +3179,11 @@ void main() {
|
||||
expect(controller.selection.extentOffset, 50);
|
||||
|
||||
if (!isContextMenuProvidedByPlatform) {
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
// There should be a delay before the toolbar is shown again on Android.
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
await tester.tap(find.text('Cut'));
|
||||
await tester.pump();
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
|
||||
@ -1553,6 +1553,69 @@ void main() {
|
||||
// toolbar. Until we change that, this test should remain skipped.
|
||||
}, skip: kIsWeb, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android })); // [intended]
|
||||
|
||||
testWidgets('dragging handles hides toolbar on mobile', (WidgetTester tester) async {
|
||||
controller.text = 'blah blah blah';
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: cursorColor,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final EditableTextState state =
|
||||
tester.state<EditableTextState>(find.byType(EditableText));
|
||||
|
||||
// Show the toolbar
|
||||
state.renderEditable.selectWordsInRange(
|
||||
from: Offset.zero,
|
||||
cause: SelectionChangedCause.tap,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(state.showToolbar(), true);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
|
||||
final List<TextSelectionPoint> endpoints = globalize(
|
||||
state.renderEditable.getEndpointsForSelection(state.textEditingValue.selection),
|
||||
state.renderEditable,
|
||||
);
|
||||
expect(endpoints.length, 2);
|
||||
|
||||
// We use a small offset because the endpoint is on the very corner of the
|
||||
// handle.
|
||||
final Offset endHandlePosition = endpoints[1].point + const Offset(1.0, 1.0);
|
||||
|
||||
// Select 2 more characters by dragging end handle.
|
||||
final TestGesture gesture = await tester.startGesture(endHandlePosition);
|
||||
await gesture.moveTo(textOffsetToPosition(tester, 6));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
|
||||
// End drag gesture and expect toolbar to show again.
|
||||
await gesture.up();
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
// There should be a delay before the toolbar is shown again on Android.
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Paste'), findsOneWidget);
|
||||
|
||||
// On web, we don't show the Flutter toolbar and instead rely on the browser
|
||||
// toolbar. Until we change that, this test should remain skipped.
|
||||
},
|
||||
skip: kIsWeb, // [intended]
|
||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }),
|
||||
);
|
||||
|
||||
testWidgets('Paste is shown only when there is something to paste', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
||||
@ -1132,7 +1132,12 @@ void main() {
|
||||
await gesture.moveTo(newHandlePos);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
// There should be a delay before the toolbar is shown again on Android.
|
||||
expect(find.text('Cut'), findsNothing);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.selection.baseOffset, 5);
|
||||
expect(controller.selection.extentOffset, 50);
|
||||
|
||||
@ -1246,6 +1246,8 @@ void main() {
|
||||
await gesture2.up();
|
||||
await tester.pump(const Duration(milliseconds: 20));
|
||||
expect(endDragEndDetails, isNotNull);
|
||||
|
||||
selectionOverlay.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user