mirror of
https://github.com/flutter/flutter
synced 2024-10-04 07:19:46 +00:00
SelectableText keep alive only when it has selection (#94493)
SelectableText defers to EditableText for wantKeepAlive, plus improved docs.
This commit is contained in:
parent
80f732bc4e
commit
e9553cd5df
|
@ -177,6 +177,8 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe
|
|||
/// rounded rectangle border around the text field. If you set the [decoration]
|
||||
/// property to null, the decoration will be removed entirely.
|
||||
///
|
||||
/// {@macro flutter.material.textfield.wantKeepAlive}
|
||||
///
|
||||
/// Remember to call [TextEditingController.dispose] when it is no longer
|
||||
/// needed. This will ensure we discard any resources used by the object.
|
||||
///
|
||||
|
|
|
@ -123,6 +123,8 @@ class _SelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestur
|
|||
/// behavior is useful, for example, to make the text bold while using the
|
||||
/// default font family and size.
|
||||
///
|
||||
/// {@macro flutter.material.textfield.wantKeepAlive}
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// ```dart
|
||||
|
@ -451,7 +453,7 @@ class SelectableText extends StatefulWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _SelectableTextState extends State<SelectableText> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
|
||||
class _SelectableTextState extends State<SelectableText> implements TextSelectionGestureDetectorBuilderDelegate {
|
||||
EditableTextState? get _editableText => editableTextKey.currentState;
|
||||
|
||||
late _TextSpanEditingController _controller;
|
||||
|
@ -579,12 +581,8 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
|
|||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context); // See AutomaticKeepAliveClientMixin.
|
||||
// TODO(garyq): Assert to block WidgetSpans from being used here are removed,
|
||||
// but we still do not yet have nice handling of things like carets, clipboard,
|
||||
// and other features. We should add proper support. Currently, caret handling
|
||||
|
|
|
@ -168,6 +168,13 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
|
|||
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
|
||||
/// consider using [TextFormField].
|
||||
///
|
||||
/// {@template flutter.material.textfield.wantKeepAlive}
|
||||
/// When the widget has focus, it will prevent itself from disposing via its
|
||||
/// underlying [EditableText]'s [AutomaticKeepAliveClientMixin.wantKeepAlive] in
|
||||
/// order to avoid losing the selection. Removing the focus will allow it to be
|
||||
/// disposed.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
|
||||
/// when it is no longer needed. This will ensure we discard any resources used
|
||||
/// by the object.
|
||||
|
|
|
@ -31,6 +31,8 @@ export 'package:flutter/services.dart' show SmartQuotesType, SmartDashesType;
|
|||
/// If a [controller] is not specified, [initialValue] can be used to give
|
||||
/// the automatically generated controller an initial value.
|
||||
///
|
||||
/// {@macro flutter.material.textfield.wantKeepAlive}
|
||||
///
|
||||
/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
|
||||
/// when it is no longer needed. This will ensure we discard any resources used
|
||||
/// by the object.
|
||||
|
|
|
@ -344,6 +344,10 @@ class ToolbarOptions {
|
|||
/// [onSubmitted] can be used to manually move focus to another input widget
|
||||
/// when a user finishes with the currently focused input widget.
|
||||
///
|
||||
/// When the widget has focus, it will prevent itself from disposing via
|
||||
/// [AutomaticKeepAliveClientMixin.wantKeepAlive] in order to avoid losing the
|
||||
/// selection. Removing the focus will allow it to be disposed.
|
||||
///
|
||||
/// Rather than using this widget directly, consider using [TextField], which
|
||||
/// is a full-featured, material-design text input field with placeholder text,
|
||||
/// labels, and [Form] integration.
|
||||
|
|
|
@ -4869,4 +4869,91 @@ void main() {
|
|||
matchesGoldenFile('selectable_text_golden.TextSelectionStyle.2.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('keeps alive when has focus', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder:
|
||||
(BuildContext context, bool innerBoxIsScrolled) {
|
||||
return <Widget>[
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 200,
|
||||
color: Colors.black12,
|
||||
child: const Center(child: Text('Sliver 1')),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: TabBar(
|
||||
labelColor: Colors.black,
|
||||
tabs: <Tab>[
|
||||
Tab(text: 'Sliver Tab 1'),
|
||||
Tab(text: 'Sliver Tab 2'),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
];
|
||||
},
|
||||
body: const TabBarView(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 100.0),
|
||||
child: Text('Regular Text'),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 100.0),
|
||||
child: SelectableText('Selectable Text'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Without any selection, the offscreen widget is disposed and can't be
|
||||
// found, for both Text and SelectableText.
|
||||
expect(find.text('Regular Text', skipOffstage: false), findsOneWidget);
|
||||
expect(find.byType(SelectableText, skipOffstage: false), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Sliver Tab 2'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Regular Text', skipOffstage: false), findsNothing);
|
||||
expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Sliver Tab 1'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Regular Text', skipOffstage: false), findsOneWidget);
|
||||
expect(find.byType(SelectableText, skipOffstage: false), findsNothing);
|
||||
|
||||
// Switch back to tab 2 and select some text in SelectableText.
|
||||
await tester.tap(find.text('Sliver Tab 2'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Regular Text', skipOffstage: false), findsNothing);
|
||||
expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);
|
||||
|
||||
final EditableText editableText = tester.widget(find.byType(EditableText));
|
||||
expect(editableText.controller.selection.isValid, isFalse);
|
||||
await tester.tapAt(textOffsetToPosition(tester, 4));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(textOffsetToPosition(tester, 4));
|
||||
await tester.pumpAndSettle();
|
||||
expect(editableText.controller.selection.isValid, isTrue);
|
||||
expect(editableText.controller.selection.baseOffset, 0);
|
||||
expect(editableText.controller.selection.extentOffset, 'Selectable'.length);
|
||||
|
||||
// Switch back to tab 1. The SelectableText remains because it is preserving
|
||||
// its selection.
|
||||
await tester.tap(find.text('Sliver Tab 1'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Regular Text', skipOffstage: false), findsOneWidget);
|
||||
expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue