forked from firka/flutter
Make textfields restorable (#63401)
This commit is contained in:
committed by
GitHub
parent
03dcd1bd5a
commit
37ddad61ca
@@ -273,6 +273,7 @@ class CupertinoTextField extends StatefulWidget {
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
this.autofillHints,
|
||||
this.restorationId,
|
||||
}) : assert(textAlign != null),
|
||||
assert(readOnly != null),
|
||||
assert(autofocus != null),
|
||||
@@ -600,6 +601,9 @@ class CupertinoTextField extends StatefulWidget {
|
||||
/// {@macro flutter.services.autofill.autofillHints}
|
||||
final Iterable<String> autofillHints;
|
||||
|
||||
/// {@macro flutter.material.textfield.restorationId}
|
||||
final String restorationId;
|
||||
|
||||
@override
|
||||
_CupertinoTextFieldState createState() => _CupertinoTextFieldState();
|
||||
|
||||
@@ -641,11 +645,11 @@ class CupertinoTextField extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
|
||||
class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
|
||||
final GlobalKey _clearGlobalKey = GlobalKey();
|
||||
|
||||
TextEditingController _controller;
|
||||
TextEditingController get _effectiveController => widget.controller ?? _controller;
|
||||
RestorableTextEditingController _controller;
|
||||
TextEditingController get _effectiveController => widget.controller ?? _controller.value;
|
||||
|
||||
FocusNode _focusNode;
|
||||
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
||||
@@ -670,8 +674,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
super.initState();
|
||||
_selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this);
|
||||
if (widget.controller == null) {
|
||||
_controller = TextEditingController();
|
||||
_controller.addListener(updateKeepAlive);
|
||||
_createLocalController();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,9 +682,10 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
void didUpdateWidget(CupertinoTextField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller == null && oldWidget.controller != null) {
|
||||
_controller = TextEditingController.fromValue(oldWidget.controller.value);
|
||||
_controller.addListener(updateKeepAlive);
|
||||
_createLocalController(oldWidget.controller.value);
|
||||
} else if (widget.controller != null && oldWidget.controller == null) {
|
||||
unregisterFromRestoration(_controller);
|
||||
_controller.dispose();
|
||||
_controller = null;
|
||||
}
|
||||
final bool isEnabled = widget.enabled ?? true;
|
||||
@@ -691,10 +695,36 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
|
||||
if (_controller != null) {
|
||||
_registerController();
|
||||
}
|
||||
}
|
||||
|
||||
void _registerController() {
|
||||
assert(_controller != null);
|
||||
registerForRestoration(_controller, 'controller');
|
||||
_controller.value.addListener(updateKeepAlive);
|
||||
}
|
||||
|
||||
void _createLocalController([TextEditingValue value]) {
|
||||
assert(_controller == null);
|
||||
_controller = value == null
|
||||
? RestorableTextEditingController()
|
||||
: RestorableTextEditingController.fromValue(value);
|
||||
if (!restorePending) {
|
||||
_registerController();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get restorationId => widget.restorationId;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode?.dispose();
|
||||
_controller?.removeListener(updateKeepAlive);
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -736,7 +766,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => _controller?.text?.isNotEmpty == true;
|
||||
bool get wantKeepAlive => _controller?.value?.text?.isNotEmpty == true;
|
||||
|
||||
bool _shouldShowAttachment({
|
||||
OverlayVisibilityMode attachment,
|
||||
@@ -927,57 +957,61 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
final Widget paddedEditable = Padding(
|
||||
padding: widget.padding,
|
||||
child: RepaintBoundary(
|
||||
child: EditableText(
|
||||
key: editableTextKey,
|
||||
controller: controller,
|
||||
readOnly: widget.readOnly,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
showCursor: widget.showCursor,
|
||||
showSelectionHandles: _showSelectionHandles,
|
||||
focusNode: _effectiveFocusNode,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction,
|
||||
textCapitalization: widget.textCapitalization,
|
||||
style: textStyle,
|
||||
strutStyle: widget.strutStyle,
|
||||
textAlign: widget.textAlign,
|
||||
autofocus: widget.autofocus,
|
||||
obscuringCharacter: widget.obscuringCharacter,
|
||||
obscureText: widget.obscureText,
|
||||
autocorrect: widget.autocorrect,
|
||||
smartDashesType: widget.smartDashesType,
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
enableSuggestions: widget.enableSuggestions,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
expands: widget.expands,
|
||||
selectionColor: selectionColor,
|
||||
selectionControls: widget.selectionEnabled
|
||||
? cupertinoTextSelectionControls : null,
|
||||
onChanged: widget.onChanged,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
inputFormatters: formatters,
|
||||
rendererIgnoresPointer: true,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorRadius: widget.cursorRadius,
|
||||
cursorColor: cursorColor,
|
||||
cursorOpacityAnimates: true,
|
||||
cursorOffset: cursorOffset,
|
||||
paintCursorAboveText: true,
|
||||
autocorrectionTextRectColor: selectionColor,
|
||||
backgroundCursorColor: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context),
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
scrollPadding: widget.scrollPadding,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
scrollController: widget.scrollController,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
autofillHints: widget.autofillHints,
|
||||
child: UnmanagedRestorationScope(
|
||||
bucket: bucket,
|
||||
child: EditableText(
|
||||
key: editableTextKey,
|
||||
controller: controller,
|
||||
readOnly: widget.readOnly,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
showCursor: widget.showCursor,
|
||||
showSelectionHandles: _showSelectionHandles,
|
||||
focusNode: _effectiveFocusNode,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction,
|
||||
textCapitalization: widget.textCapitalization,
|
||||
style: textStyle,
|
||||
strutStyle: widget.strutStyle,
|
||||
textAlign: widget.textAlign,
|
||||
autofocus: widget.autofocus,
|
||||
obscuringCharacter: widget.obscuringCharacter,
|
||||
obscureText: widget.obscureText,
|
||||
autocorrect: widget.autocorrect,
|
||||
smartDashesType: widget.smartDashesType,
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
enableSuggestions: widget.enableSuggestions,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
expands: widget.expands,
|
||||
selectionColor: selectionColor,
|
||||
selectionControls: widget.selectionEnabled
|
||||
? cupertinoTextSelectionControls : null,
|
||||
onChanged: widget.onChanged,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
inputFormatters: formatters,
|
||||
rendererIgnoresPointer: true,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorRadius: widget.cursorRadius,
|
||||
cursorColor: cursorColor,
|
||||
cursorOpacityAnimates: true,
|
||||
cursorOffset: cursorOffset,
|
||||
paintCursorAboveText: true,
|
||||
autocorrectionTextRectColor: selectionColor,
|
||||
backgroundCursorColor: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context),
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
scrollPadding: widget.scrollPadding,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
scrollController: widget.scrollController,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
autofillHints: widget.autofillHints,
|
||||
restorationId: 'editable',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -372,6 +372,7 @@ class TextField extends StatefulWidget {
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
this.autofillHints,
|
||||
this.restorationId,
|
||||
}) : assert(textAlign != null),
|
||||
assert(readOnly != null),
|
||||
assert(autofocus != null),
|
||||
@@ -787,6 +788,25 @@ class TextField extends StatefulWidget {
|
||||
/// {@macro flutter.services.autofill.autofillHints}
|
||||
final Iterable<String> autofillHints;
|
||||
|
||||
/// {@template flutter.material.textfield.restorationId}
|
||||
/// Restoration ID to save and restore the state of the text field.
|
||||
///
|
||||
/// If non-null, the text field will persist and restore its current scroll
|
||||
/// offset and - if no [controller] has been provided - the content of the
|
||||
/// text field. If a [controller] has been provided, it is the responsibility
|
||||
/// of the owner of that controller to persist and restore it, e.g. by using
|
||||
/// a [RestorableTextEditingController].
|
||||
///
|
||||
/// The state of this widget is persisted in a [RestorationBucket] claimed
|
||||
/// from the surrounding [RestorationScope] using the provided restoration ID.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RestorationManager], which explains how state restoration works in
|
||||
/// Flutter.
|
||||
/// {@endtemplate}
|
||||
final String restorationId;
|
||||
|
||||
@override
|
||||
_TextFieldState createState() => _TextFieldState();
|
||||
|
||||
@@ -828,9 +848,9 @@ class TextField extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _TextFieldState extends State<TextField> implements TextSelectionGestureDetectorBuilderDelegate {
|
||||
TextEditingController _controller;
|
||||
TextEditingController get _effectiveController => widget.controller ?? _controller;
|
||||
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate {
|
||||
RestorableTextEditingController _controller;
|
||||
TextEditingController get _effectiveController => widget.controller ?? _controller.value;
|
||||
|
||||
FocusNode _focusNode;
|
||||
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
||||
@@ -937,7 +957,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
|
||||
super.initState();
|
||||
_selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
|
||||
if (widget.controller == null) {
|
||||
_controller = TextEditingController();
|
||||
_createLocalController();
|
||||
}
|
||||
_effectiveFocusNode.canRequestFocus = _isEnabled;
|
||||
}
|
||||
@@ -963,10 +983,13 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
|
||||
@override
|
||||
void didUpdateWidget(TextField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller == null && oldWidget.controller != null)
|
||||
_controller = TextEditingController.fromValue(oldWidget.controller.value);
|
||||
else if (widget.controller != null && oldWidget.controller == null)
|
||||
if (widget.controller == null && oldWidget.controller != null) {
|
||||
_createLocalController(oldWidget.controller.value);
|
||||
} else if (widget.controller != null && oldWidget.controller == null) {
|
||||
unregisterFromRestoration(_controller);
|
||||
_controller.dispose();
|
||||
_controller = null;
|
||||
}
|
||||
_effectiveFocusNode.canRequestFocus = _canRequestFocus;
|
||||
if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) {
|
||||
if(_effectiveController.selection.isCollapsed) {
|
||||
@@ -975,9 +998,35 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
|
||||
if (_controller != null) {
|
||||
_registerController();
|
||||
}
|
||||
}
|
||||
|
||||
void _registerController() {
|
||||
assert(_controller != null);
|
||||
registerForRestoration(_controller, 'controller');
|
||||
}
|
||||
|
||||
void _createLocalController([TextEditingValue value]) {
|
||||
assert(_controller == null);
|
||||
_controller = value == null
|
||||
? RestorableTextEditingController()
|
||||
: RestorableTextEditingController.fromValue(value);
|
||||
if (!restorePending) {
|
||||
_registerController();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get restorationId => widget.restorationId;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode?.dispose();
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -1122,60 +1171,64 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
|
||||
}
|
||||
|
||||
Widget child = RepaintBoundary(
|
||||
child: EditableText(
|
||||
key: editableTextKey,
|
||||
readOnly: widget.readOnly || !_isEnabled,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
showCursor: widget.showCursor,
|
||||
showSelectionHandles: _showSelectionHandles,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction,
|
||||
textCapitalization: widget.textCapitalization,
|
||||
style: style,
|
||||
strutStyle: widget.strutStyle,
|
||||
textAlign: widget.textAlign,
|
||||
textDirection: widget.textDirection,
|
||||
autofocus: widget.autofocus,
|
||||
obscuringCharacter: widget.obscuringCharacter,
|
||||
obscureText: widget.obscureText,
|
||||
autocorrect: widget.autocorrect,
|
||||
smartDashesType: widget.smartDashesType,
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
enableSuggestions: widget.enableSuggestions,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
expands: widget.expands,
|
||||
selectionColor: selectionColor,
|
||||
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
||||
onChanged: widget.onChanged,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onAppPrivateCommand: widget.onAppPrivateCommand,
|
||||
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
||||
inputFormatters: formatters,
|
||||
rendererIgnoresPointer: true,
|
||||
mouseCursor: MouseCursor.defer, // TextField will handle the cursor
|
||||
cursorWidth: widget.cursorWidth,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorRadius: cursorRadius,
|
||||
cursorColor: cursorColor,
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
cursorOpacityAnimates: cursorOpacityAnimates,
|
||||
cursorOffset: cursorOffset,
|
||||
paintCursorAboveText: paintCursorAboveText,
|
||||
backgroundCursorColor: CupertinoColors.inactiveGray,
|
||||
scrollPadding: widget.scrollPadding,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
scrollController: widget.scrollController,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
autofillHints: widget.autofillHints,
|
||||
autocorrectionTextRectColor: autocorrectionTextRectColor,
|
||||
child: UnmanagedRestorationScope(
|
||||
bucket: bucket,
|
||||
child: EditableText(
|
||||
key: editableTextKey,
|
||||
readOnly: widget.readOnly || !_isEnabled,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
showCursor: widget.showCursor,
|
||||
showSelectionHandles: _showSelectionHandles,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction,
|
||||
textCapitalization: widget.textCapitalization,
|
||||
style: style,
|
||||
strutStyle: widget.strutStyle,
|
||||
textAlign: widget.textAlign,
|
||||
textDirection: widget.textDirection,
|
||||
autofocus: widget.autofocus,
|
||||
obscuringCharacter: widget.obscuringCharacter,
|
||||
obscureText: widget.obscureText,
|
||||
autocorrect: widget.autocorrect,
|
||||
smartDashesType: widget.smartDashesType,
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
enableSuggestions: widget.enableSuggestions,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
expands: widget.expands,
|
||||
selectionColor: selectionColor,
|
||||
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
||||
onChanged: widget.onChanged,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onAppPrivateCommand: widget.onAppPrivateCommand,
|
||||
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
||||
inputFormatters: formatters,
|
||||
rendererIgnoresPointer: true,
|
||||
mouseCursor: MouseCursor.defer, // TextField will handle the cursor
|
||||
cursorWidth: widget.cursorWidth,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorRadius: cursorRadius,
|
||||
cursorColor: cursorColor,
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
cursorOpacityAnimates: cursorOpacityAnimates,
|
||||
cursorOffset: cursorOffset,
|
||||
paintCursorAboveText: paintCursorAboveText,
|
||||
backgroundCursorColor: CupertinoColors.inactiveGray,
|
||||
scrollPadding: widget.scrollPadding,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
scrollController: widget.scrollController,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
autofillHints: widget.autofillHints,
|
||||
autocorrectionTextRectColor: autocorrectionTextRectColor,
|
||||
restorationId: 'editable',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -445,6 +445,7 @@ class EditableText extends StatefulWidget {
|
||||
),
|
||||
this.autofillHints,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.restorationId,
|
||||
}) : assert(controller != null),
|
||||
assert(focusNode != null),
|
||||
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
|
||||
@@ -1218,6 +1219,25 @@ class EditableText extends StatefulWidget {
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// Restoration ID to save and restore the scroll offset of the
|
||||
/// [EditableText].
|
||||
///
|
||||
/// If a restoration id is provided, the [EditableText] will persist its
|
||||
/// current scroll offset and restore it during state restoration.
|
||||
///
|
||||
/// The scroll offset is persisted in a [RestorationBucket] claimed from
|
||||
/// the surrounding [RestorationScope] using the provided restoration ID.
|
||||
///
|
||||
/// Persisting and restoring the content of the [EditableText] is the
|
||||
/// responsibilility of the owner of the [controller], who may use a
|
||||
/// [RestorableTextEditingController] for that purpose.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RestorationManager], which explains how state restoration works in
|
||||
/// Flutter.
|
||||
final String restorationId;
|
||||
|
||||
// Infer the keyboard type of an `EditableText` if it's not specified.
|
||||
static TextInputType _inferKeyboardType({
|
||||
@required Iterable<String> autofillHints,
|
||||
@@ -2323,6 +2343,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
controller: _scrollController,
|
||||
physics: widget.scrollPhysics,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
restorationId: widget.restorationId,
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
return CompositedTransformTarget(
|
||||
link: _toolbarLayerLink,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -257,11 +259,26 @@ class RestorableTextEditingController extends RestorableListenable<TextEditingCo
|
||||
return value.text;
|
||||
}
|
||||
|
||||
TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initWithValue(TextEditingController value) {
|
||||
_disposeControllerIfNecessary();
|
||||
_controller = value;
|
||||
super.initWithValue(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (isRegistered) {
|
||||
value.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
_disposeControllerIfNecessary();
|
||||
}
|
||||
|
||||
void _disposeControllerIfNecessary() {
|
||||
if (_controller != null) {
|
||||
// Scheduling a microtask for dispose to give other entities a chance
|
||||
// to remove their listeners first.
|
||||
scheduleMicrotask(_controller.dispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
126
packages/flutter/test/cupertino/text_field_restoration_test.dart
Normal file
126
packages/flutter/test/cupertino/text_field_restoration_test.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
const String text = 'Hello World! How are you? Life is good!';
|
||||
const String alternativeText = 'Everything is awesome!!';
|
||||
|
||||
void main() {
|
||||
testWidgets('CupertinoTextField restoration', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const RootRestorationScope(
|
||||
child: TestWidget(),
|
||||
restorationId: 'root',
|
||||
),
|
||||
);
|
||||
|
||||
await restoreAndVerify(tester);
|
||||
});
|
||||
|
||||
testWidgets('CupertinoTextField restoration with external controller', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const RootRestorationScope(
|
||||
child: TestWidget(
|
||||
useExternal: true,
|
||||
),
|
||||
restorationId: 'root',
|
||||
),
|
||||
);
|
||||
|
||||
await restoreAndVerify(tester);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> restoreAndVerify(WidgetTester tester) async {
|
||||
expect(find.text(text), findsNothing);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 0);
|
||||
|
||||
await tester.enterText(find.byType(CupertinoTextField), text);
|
||||
await skipPastScrollingAnimation(tester);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 0);
|
||||
|
||||
await tester.drag(find.byType(Scrollable), const Offset(0, -80));
|
||||
await skipPastScrollingAnimation(tester);
|
||||
|
||||
expect(find.text(text), findsOneWidget);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
|
||||
|
||||
await tester.restartAndRestore();
|
||||
|
||||
expect(find.text(text), findsOneWidget);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
|
||||
|
||||
final TestRestorationData data = await tester.getRestorationData();
|
||||
|
||||
await tester.enterText(find.byType(CupertinoTextField), alternativeText);
|
||||
await skipPastScrollingAnimation(tester);
|
||||
await tester.drag(find.byType(Scrollable), const Offset(0, 80));
|
||||
await skipPastScrollingAnimation(tester);
|
||||
|
||||
expect(find.text(text), findsNothing);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, isNot(60));
|
||||
|
||||
await tester.restoreFrom(data);
|
||||
|
||||
expect(find.text(text), findsOneWidget);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
|
||||
}
|
||||
|
||||
class TestWidget extends StatefulWidget {
|
||||
const TestWidget({Key key, this.useExternal = false}) : super(key: key);
|
||||
|
||||
final bool useExternal;
|
||||
|
||||
@override
|
||||
TestWidgetState createState() => TestWidgetState();
|
||||
}
|
||||
|
||||
class TestWidgetState extends State<TestWidget> with RestorationMixin {
|
||||
final RestorableTextEditingController controller = RestorableTextEditingController();
|
||||
|
||||
@override
|
||||
String get restorationId => 'widget';
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
|
||||
registerForRestoration(controller, 'controller');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
child: CupertinoTextField(
|
||||
restorationId: 'text',
|
||||
maxLines: 3,
|
||||
controller: widget.useExternal ? controller.value : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> skipPastScrollingAnimation(WidgetTester tester) async {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
}
|
||||
125
packages/flutter/test/material/text_field_restoration_test.dart
Normal file
125
packages/flutter/test/material/text_field_restoration_test.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
const String text = 'Hello World! How are you? Life is good!';
|
||||
const String alternativeText = 'Everything is awesome!!';
|
||||
|
||||
void main() {
|
||||
testWidgets('TextField restoration', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const RootRestorationScope(
|
||||
child: TestWidget(),
|
||||
restorationId: 'root',
|
||||
),
|
||||
);
|
||||
|
||||
await restoreAndVerify(tester);
|
||||
});
|
||||
|
||||
testWidgets('TextField restoration with external controller', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const RootRestorationScope(
|
||||
child: TestWidget(
|
||||
useExternal: true,
|
||||
),
|
||||
restorationId: 'root',
|
||||
),
|
||||
);
|
||||
|
||||
await restoreAndVerify(tester);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> restoreAndVerify(WidgetTester tester) async {
|
||||
expect(find.text(text), findsNothing);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 0);
|
||||
|
||||
await tester.enterText(find.byType(TextField), text);
|
||||
await skipPastScrollingAnimation(tester);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 0);
|
||||
|
||||
await tester.drag(find.byType(Scrollable), const Offset(0, -80));
|
||||
await skipPastScrollingAnimation(tester);
|
||||
|
||||
expect(find.text(text), findsOneWidget);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
|
||||
|
||||
await tester.restartAndRestore();
|
||||
|
||||
expect(find.text(text), findsOneWidget);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
|
||||
|
||||
final TestRestorationData data = await tester.getRestorationData();
|
||||
|
||||
await tester.enterText(find.byType(TextField), alternativeText);
|
||||
await skipPastScrollingAnimation(tester);
|
||||
await tester.drag(find.byType(Scrollable), const Offset(0, 80));
|
||||
await skipPastScrollingAnimation(tester);
|
||||
|
||||
expect(find.text(text), findsNothing);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, isNot(60));
|
||||
|
||||
await tester.restoreFrom(data);
|
||||
|
||||
expect(find.text(text), findsOneWidget);
|
||||
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
|
||||
}
|
||||
|
||||
class TestWidget extends StatefulWidget {
|
||||
const TestWidget({Key key, this.useExternal = false}) : super(key: key);
|
||||
|
||||
final bool useExternal;
|
||||
|
||||
@override
|
||||
TestWidgetState createState() => TestWidgetState();
|
||||
}
|
||||
|
||||
class TestWidgetState extends State<TestWidget> with RestorationMixin {
|
||||
final RestorableTextEditingController controller = RestorableTextEditingController();
|
||||
|
||||
@override
|
||||
String get restorationId => 'widget';
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
|
||||
registerForRestoration(controller, 'controller');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Material(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
child: TextField(
|
||||
restorationId: 'text',
|
||||
maxLines: 3,
|
||||
controller: widget.useExternal ? controller.value : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> skipPastScrollingAnimation(WidgetTester tester) async {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
}
|
||||
Reference in New Issue
Block a user