mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Rationalize text input widgets (#9119)
After this patch, there are three major text input widgets: * EditableText. This widget is a low-level editing control that interacts with the IME and displays a blinking cursor. * TextField. This widget is a Material Design text field, with all the bells and whistles. It is highly configurable and can be reduced down to a fairly simple control by setting its `decoration` property to null. * TextFormField. This widget is a FormField that wraps a TextField. This patch also replaces the InputValue data model for these widgets with a Listenable TextEditingController, which is much more flexible. Fixes #7031
This commit is contained in:
parent
91dbb3c91e
commit
ae8994860e
@ -7,12 +7,12 @@ import 'package:flutter/rendering.dart' show debugDumpRenderTree;
|
|||||||
|
|
||||||
class CardModel {
|
class CardModel {
|
||||||
CardModel(this.value, this.height) {
|
CardModel(this.value, this.height) {
|
||||||
inputValue = new InputValue(text: 'Item $value');
|
textController = new TextEditingController(text: 'Item $value');
|
||||||
}
|
}
|
||||||
int value;
|
int value;
|
||||||
double height;
|
double height;
|
||||||
int get color => ((value % 9) + 1) * 100;
|
int get color => ((value % 9) + 1) * 100;
|
||||||
InputValue inputValue;
|
TextEditingController textController;
|
||||||
Key get key => new ObjectKey(this);
|
Key get key => new ObjectKey(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,11 +245,7 @@ class CardCollectionState extends State<CardCollection> {
|
|||||||
new Center(
|
new Center(
|
||||||
child: new TextField(
|
child: new TextField(
|
||||||
key: new GlobalObjectKey(cardModel),
|
key: new GlobalObjectKey(cardModel),
|
||||||
onChanged: (InputValue value) {
|
controller: cardModel.textController,
|
||||||
setState(() {
|
|
||||||
cardModel.inputValue = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: new DefaultTextStyle.merge(
|
: new DefaultTextStyle.merge(
|
||||||
@ -261,7 +257,7 @@ class CardCollectionState extends State<CardCollection> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new Text(cardModel.inputValue.text, textAlign: _textAlign),
|
new Text(cardModel.textController.text, textAlign: _textAlign),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -26,9 +26,11 @@ class _InputDropdown extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new InkWell(
|
return new InkWell(
|
||||||
onTap: onPressed,
|
onTap: onPressed,
|
||||||
child: new InputContainer(
|
child: new InputDecorator(
|
||||||
labelText: labelText,
|
decoration: new InputDecoration(
|
||||||
style: valueStyle,
|
labelText: labelText,
|
||||||
|
),
|
||||||
|
baseStyle: valueStyle,
|
||||||
child: new Row(
|
child: new Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -133,11 +135,15 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new TextField(
|
new TextField(
|
||||||
labelText: 'Event name',
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Event name',
|
||||||
|
),
|
||||||
style: Theme.of(context).textTheme.display1,
|
style: Theme.of(context).textTheme.display1,
|
||||||
),
|
),
|
||||||
new TextField(
|
new TextField(
|
||||||
labelText: 'Location',
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Location',
|
||||||
|
),
|
||||||
style: Theme.of(context).textTheme.display1.copyWith(fontSize: 20.0),
|
style: Theme.of(context).textTheme.display1.copyWith(fontSize: 20.0),
|
||||||
),
|
),
|
||||||
new _DateTimePicker(
|
new _DateTimePicker(
|
||||||
@ -170,9 +176,11 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
new InputContainer(
|
new InputDecorator(
|
||||||
labelText: 'Activity',
|
decoration: const InputDecoration(
|
||||||
hintText: 'Choose an activity',
|
labelText: 'Activity',
|
||||||
|
hintText: 'Choose an activity',
|
||||||
|
),
|
||||||
isEmpty: _activity == null,
|
isEmpty: _activity == null,
|
||||||
child: new DropdownButton<String>(
|
child: new DropdownButton<String>(
|
||||||
value: _activity,
|
value: _activity,
|
||||||
|
@ -148,10 +148,11 @@ class DemoItem<T> {
|
|||||||
this.hint,
|
this.hint,
|
||||||
this.builder,
|
this.builder,
|
||||||
this.valueToString
|
this.valueToString
|
||||||
});
|
}) : textController = new TextEditingController(text: valueToString(value));
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final String hint;
|
final String hint;
|
||||||
|
final TextEditingController textController;
|
||||||
final DemoItemBodyBuilder<T> builder;
|
final DemoItemBodyBuilder<T> builder;
|
||||||
final ValueToString<T> valueToString;
|
final ValueToString<T> valueToString;
|
||||||
T value;
|
T value;
|
||||||
@ -205,18 +206,20 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
|
|||||||
onCancel: () { Form.of(context).reset(); close(); },
|
onCancel: () { Form.of(context).reset(); close(); },
|
||||||
child: new Padding(
|
child: new Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: new TextField(
|
child: new TextFormField(
|
||||||
hintText: item.hint,
|
controller: item.textController,
|
||||||
labelText: item.name,
|
decoration: new InputDecoration(
|
||||||
initialValue: new InputValue(text: item.value),
|
hintText: item.hint,
|
||||||
onSaved: (InputValue val) { item.value = val.text; },
|
labelText: item.name,
|
||||||
|
),
|
||||||
|
onSaved: (String value) { item.value = value; },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
new DemoItem<_Location>(
|
new DemoItem<_Location>(
|
||||||
name: 'Location',
|
name: 'Location',
|
||||||
@ -229,8 +232,6 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
|
|||||||
item.isExpanded = false;
|
item.isExpanded = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return new Form(
|
return new Form(
|
||||||
child: new Builder(
|
child: new Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
|
@ -27,6 +27,6 @@ export 'slider_demo.dart';
|
|||||||
export 'snack_bar_demo.dart';
|
export 'snack_bar_demo.dart';
|
||||||
export 'tabs_demo.dart';
|
export 'tabs_demo.dart';
|
||||||
export 'tabs_fab_demo.dart';
|
export 'tabs_fab_demo.dart';
|
||||||
export 'text_field_demo.dart';
|
export 'text_form_field_demo.dart';
|
||||||
export 'tooltip_demo.dart';
|
export 'tooltip_demo.dart';
|
||||||
export 'two_level_list_demo.dart';
|
export 'two_level_list_demo.dart';
|
||||||
|
@ -6,13 +6,13 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class TextFieldDemo extends StatefulWidget {
|
class TextFormFieldDemo extends StatefulWidget {
|
||||||
TextFieldDemo({ Key key }) : super(key: key);
|
TextFormFieldDemo({ Key key }) : super(key: key);
|
||||||
|
|
||||||
static const String routeName = '/material/text-field';
|
static const String routeName = '/material/text-form-field';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextFieldDemoState createState() => new TextFieldDemoState();
|
TextFormFieldDemoState createState() => new TextFormFieldDemoState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class PersonData {
|
class PersonData {
|
||||||
@ -21,7 +21,7 @@ class PersonData {
|
|||||||
String password = '';
|
String password = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextFieldDemoState extends State<TextFieldDemo> {
|
class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
PersonData person = new PersonData();
|
PersonData person = new PersonData();
|
||||||
@ -35,7 +35,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||||||
bool _autovalidate = false;
|
bool _autovalidate = false;
|
||||||
bool _formWasEdited = false;
|
bool _formWasEdited = false;
|
||||||
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
|
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
|
||||||
final GlobalKey<FormFieldState<InputValue>> _passwordFieldKey = new GlobalKey<FormFieldState<InputValue>>();
|
final GlobalKey<FormFieldState<String>> _passwordFieldKey = new GlobalKey<FormFieldState<String>>();
|
||||||
void _handleSubmitted() {
|
void _handleSubmitted() {
|
||||||
final FormState form = _formKey.currentState;
|
final FormState form = _formKey.currentState;
|
||||||
if (!form.validate()) {
|
if (!form.validate()) {
|
||||||
@ -47,30 +47,30 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _validateName(InputValue value) {
|
String _validateName(String value) {
|
||||||
_formWasEdited = true;
|
_formWasEdited = true;
|
||||||
if (value.text.isEmpty)
|
if (value.isEmpty)
|
||||||
return 'Name is required.';
|
return 'Name is required.';
|
||||||
final RegExp nameExp = new RegExp(r'^[A-za-z ]+$');
|
final RegExp nameExp = new RegExp(r'^[A-za-z ]+$');
|
||||||
if (!nameExp.hasMatch(value.text))
|
if (!nameExp.hasMatch(value))
|
||||||
return 'Please enter only alphabetical characters.';
|
return 'Please enter only alphabetical characters.';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _validatePhoneNumber(InputValue value) {
|
String _validatePhoneNumber(String value) {
|
||||||
_formWasEdited = true;
|
_formWasEdited = true;
|
||||||
final RegExp phoneExp = new RegExp(r'^\d\d\d-\d\d\d\-\d\d\d\d$');
|
final RegExp phoneExp = new RegExp(r'^\d\d\d-\d\d\d\-\d\d\d\d$');
|
||||||
if (!phoneExp.hasMatch(value.text))
|
if (!phoneExp.hasMatch(value))
|
||||||
return '###-###-#### - Please enter a valid phone number.';
|
return '###-###-#### - Please enter a valid phone number.';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _validatePassword(InputValue value) {
|
String _validatePassword(String value) {
|
||||||
_formWasEdited = true;
|
_formWasEdited = true;
|
||||||
final FormFieldState<InputValue> passwordField = _passwordFieldKey.currentState;
|
final FormFieldState<String> passwordField = _passwordFieldKey.currentState;
|
||||||
if (passwordField.value == null || passwordField.value.text.isEmpty)
|
if (passwordField.value == null || passwordField.value.isEmpty)
|
||||||
return 'Please choose a password.';
|
return 'Please choose a password.';
|
||||||
if (passwordField.value.text != value.text)
|
if (passwordField.value != value)
|
||||||
return 'Passwords don\'t match';
|
return 'Passwords don\'t match';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -104,7 +104,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
title: new Text('Text fields')
|
title: new Text('Text fields'),
|
||||||
),
|
),
|
||||||
body: new Form(
|
body: new Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@ -113,48 +113,58 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||||||
child: new ListView(
|
child: new ListView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new TextField(
|
new TextFormField(
|
||||||
icon: new Icon(Icons.person),
|
decoration: const InputDecoration(
|
||||||
hintText: 'What do people call you?',
|
icon: const Icon(Icons.person),
|
||||||
labelText: 'Name *',
|
hintText: 'What do people call you?',
|
||||||
onSaved: (InputValue val) { person.name = val.text; },
|
labelText: 'Name *',
|
||||||
|
),
|
||||||
|
onSaved: (String value) { person.name = value; },
|
||||||
validator: _validateName,
|
validator: _validateName,
|
||||||
),
|
),
|
||||||
new TextField(
|
new TextFormField(
|
||||||
icon: new Icon(Icons.phone),
|
decoration: const InputDecoration(
|
||||||
hintText: 'Where can we reach you?',
|
icon: const Icon(Icons.phone),
|
||||||
labelText: 'Phone Number *',
|
hintText: 'Where can we reach you?',
|
||||||
|
labelText: 'Phone Number *',
|
||||||
|
),
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
onSaved: (InputValue val) { person.phoneNumber = val.text; },
|
onSaved: (String value) { person.phoneNumber = value; },
|
||||||
validator: _validatePhoneNumber,
|
validator: _validatePhoneNumber,
|
||||||
),
|
),
|
||||||
new TextField(
|
new TextFormField(
|
||||||
hintText: 'Tell us about yourself',
|
decoration: const InputDecoration(
|
||||||
labelText: 'Life story',
|
hintText: 'Tell us about yourself',
|
||||||
|
labelText: 'Life story',
|
||||||
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
new Row(
|
new Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new Expanded(
|
new Expanded(
|
||||||
child: new TextField(
|
child: new TextFormField(
|
||||||
key: _passwordFieldKey,
|
key: _passwordFieldKey,
|
||||||
hintText: 'How do you log in?',
|
decoration: const InputDecoration(
|
||||||
labelText: 'New Password *',
|
hintText: 'How do you log in?',
|
||||||
|
labelText: 'New Password *',
|
||||||
|
),
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
onSaved: (InputValue val) { person.password = val.text; }
|
onSaved: (String value) { person.password = value; },
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16.0),
|
const SizedBox(width: 16.0),
|
||||||
new Expanded(
|
new Expanded(
|
||||||
child: new TextField(
|
child: new TextFormField(
|
||||||
hintText: 'How do you log in?',
|
decoration: const InputDecoration(
|
||||||
labelText: 'Re-type Password *',
|
hintText: 'How do you log in?',
|
||||||
|
labelText: 'Re-type Password *',
|
||||||
|
),
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
validator: _validatePassword,
|
validator: _validatePassword,
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
new Container(
|
new Container(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
@ -168,9 +178,9 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||||||
padding: const EdgeInsets.only(top: 20.0),
|
padding: const EdgeInsets.only(top: 20.0),
|
||||||
child: new Text('* indicates required field', style: Theme.of(context).textTheme.caption),
|
child: new Text('* indicates required field', style: Theme.of(context).textTheme.caption),
|
||||||
),
|
),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -261,8 +261,8 @@ final List<GalleryItem> kAllGalleryItems = <GalleryItem>[
|
|||||||
title: 'Text fields',
|
title: 'Text fields',
|
||||||
subtitle: 'Single line of editable text and numbers',
|
subtitle: 'Single line of editable text and numbers',
|
||||||
category: 'Material Components',
|
category: 'Material Components',
|
||||||
routeName: TextFieldDemo.routeName,
|
routeName: TextFormFieldDemo.routeName,
|
||||||
buildRoute: (BuildContext context) => new TextFieldDemo(),
|
buildRoute: (BuildContext context) => new TextFormFieldDemo(),
|
||||||
),
|
),
|
||||||
new GalleryItem(
|
new GalleryItem(
|
||||||
title: 'Tooltips',
|
title: 'Tooltips',
|
||||||
|
@ -29,22 +29,22 @@ class _NotImplementedDialog extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new Icon(
|
new Icon(
|
||||||
Icons.dvr,
|
Icons.dvr,
|
||||||
size: 18.0
|
size: 18.0,
|
||||||
),
|
),
|
||||||
new Container(
|
new Container(
|
||||||
width: 8.0
|
width: 8.0,
|
||||||
),
|
),
|
||||||
new Text('DUMP APP TO CONSOLE'),
|
new Text('DUMP APP TO CONSOLE'),
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
new FlatButton(
|
new FlatButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context, false);
|
Navigator.pop(context, false);
|
||||||
},
|
},
|
||||||
child: new Text('OH WELL')
|
child: new Text('OH WELL'),
|
||||||
)
|
),
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ class StockHomeState extends State<StockHome> {
|
|||||||
|
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
bool _isSearching = false;
|
bool _isSearching = false;
|
||||||
InputValue _searchQuery = InputValue.empty;
|
final TextEditingController _searchQuery = new TextEditingController();
|
||||||
bool _autorefresh = false;
|
bool _autorefresh = false;
|
||||||
|
|
||||||
void _handleSearchBegin() {
|
void _handleSearchBegin() {
|
||||||
@ -73,9 +73,9 @@ class StockHomeState extends State<StockHome> {
|
|||||||
onRemove: () {
|
onRemove: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSearching = false;
|
_isSearching = false;
|
||||||
_searchQuery = InputValue.empty;
|
_searchQuery.clear();
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
));
|
));
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSearching = true;
|
_isSearching = true;
|
||||||
@ -86,12 +86,6 @@ class StockHomeState extends State<StockHome> {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSearchQueryChanged(InputValue query) {
|
|
||||||
setState(() {
|
|
||||||
_searchQuery = query;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleStockModeChange(StockMode value) {
|
void _handleStockModeChange(StockMode value) {
|
||||||
if (config.updater != null)
|
if (config.updater != null)
|
||||||
config.updater(config.configuration.copyWith(stockMode: value));
|
config.updater(config.configuration.copyWith(stockMode: value));
|
||||||
@ -155,7 +149,7 @@ class StockHomeState extends State<StockHome> {
|
|||||||
trailing: new Radio<StockMode>(
|
trailing: new Radio<StockMode>(
|
||||||
value: StockMode.optimistic,
|
value: StockMode.optimistic,
|
||||||
groupValue: config.configuration.stockMode,
|
groupValue: config.configuration.stockMode,
|
||||||
onChanged: _handleStockModeChange
|
onChanged: _handleStockModeChange,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_handleStockModeChange(StockMode.optimistic);
|
_handleStockModeChange(StockMode.optimistic);
|
||||||
@ -167,7 +161,7 @@ class StockHomeState extends State<StockHome> {
|
|||||||
trailing: new Radio<StockMode>(
|
trailing: new Radio<StockMode>(
|
||||||
value: StockMode.pessimistic,
|
value: StockMode.pessimistic,
|
||||||
groupValue: config.configuration.stockMode,
|
groupValue: config.configuration.stockMode,
|
||||||
onChanged: _handleStockModeChange
|
onChanged: _handleStockModeChange,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_handleStockModeChange(StockMode.pessimistic);
|
_handleStockModeChange(StockMode.pessimistic);
|
||||||
@ -184,8 +178,8 @@ class StockHomeState extends State<StockHome> {
|
|||||||
title: new Text('About'),
|
title: new Text('About'),
|
||||||
onTap: _handleShowAbout,
|
onTap: _handleShowAbout,
|
||||||
),
|
),
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +199,7 @@ class StockHomeState extends State<StockHome> {
|
|||||||
new IconButton(
|
new IconButton(
|
||||||
icon: new Icon(Icons.search),
|
icon: new Icon(Icons.search),
|
||||||
onPressed: _handleSearchBegin,
|
onPressed: _handleSearchBegin,
|
||||||
tooltip: 'Search'
|
tooltip: 'Search',
|
||||||
),
|
),
|
||||||
new PopupMenuButton<_StockMenuItem>(
|
new PopupMenuButton<_StockMenuItem>(
|
||||||
onSelected: (_StockMenuItem value) { _handleStockMenu(context, value); },
|
onSelected: (_StockMenuItem value) { _handleStockMenu(context, value); },
|
||||||
@ -213,29 +207,29 @@ class StockHomeState extends State<StockHome> {
|
|||||||
new CheckedPopupMenuItem<_StockMenuItem>(
|
new CheckedPopupMenuItem<_StockMenuItem>(
|
||||||
value: _StockMenuItem.autorefresh,
|
value: _StockMenuItem.autorefresh,
|
||||||
checked: _autorefresh,
|
checked: _autorefresh,
|
||||||
child: new Text('Autorefresh')
|
child: new Text('Autorefresh'),
|
||||||
),
|
),
|
||||||
new PopupMenuItem<_StockMenuItem>(
|
new PopupMenuItem<_StockMenuItem>(
|
||||||
value: _StockMenuItem.refresh,
|
value: _StockMenuItem.refresh,
|
||||||
child: new Text('Refresh')
|
child: new Text('Refresh'),
|
||||||
),
|
),
|
||||||
new PopupMenuItem<_StockMenuItem>(
|
new PopupMenuItem<_StockMenuItem>(
|
||||||
value: _StockMenuItem.speedUp,
|
value: _StockMenuItem.speedUp,
|
||||||
child: new Text('Increase animation speed')
|
child: new Text('Increase animation speed'),
|
||||||
),
|
),
|
||||||
new PopupMenuItem<_StockMenuItem>(
|
new PopupMenuItem<_StockMenuItem>(
|
||||||
value: _StockMenuItem.speedDown,
|
value: _StockMenuItem.speedDown,
|
||||||
child: new Text('Decrease animation speed')
|
child: new Text('Decrease animation speed'),
|
||||||
)
|
),
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
bottom: new TabBar(
|
bottom: new TabBar(
|
||||||
tabs: <Widget>[
|
tabs: <Widget>[
|
||||||
new Tab(text: StockStrings.of(context).market()),
|
new Tab(text: StockStrings.of(context).market()),
|
||||||
new Tab(text: StockStrings.of(context).portfolio()),
|
new Tab(text: StockStrings.of(context).portfolio()),
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,8 +256,8 @@ class StockHomeState extends State<StockHome> {
|
|||||||
label: "BUY MORE",
|
label: "BUY MORE",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_buyStock(stock);
|
_buyStock(stock);
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,14 +270,14 @@ class StockHomeState extends State<StockHome> {
|
|||||||
},
|
},
|
||||||
onShow: (Stock stock) {
|
onShow: (Stock stock) {
|
||||||
_scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
|
_scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStockTab(BuildContext context, StockHomeTab tab, List<String> stockSymbols) {
|
Widget _buildStockTab(BuildContext context, StockHomeTab tab, List<String> stockSymbols) {
|
||||||
return new Container(
|
return new Container(
|
||||||
key: new ValueKey<StockHomeTab>(tab),
|
key: new ValueKey<StockHomeTab>(tab),
|
||||||
child: _buildStockList(context, _filterBySearchQuery(_getStockList(stockSymbols)).toList(), tab)
|
child: _buildStockList(context, _filterBySearchQuery(_getStockList(stockSymbols)).toList(), tab),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,21 +290,23 @@ class StockHomeState extends State<StockHome> {
|
|||||||
icon: new Icon(Icons.arrow_back),
|
icon: new Icon(Icons.arrow_back),
|
||||||
color: Theme.of(context).accentColor,
|
color: Theme.of(context).accentColor,
|
||||||
onPressed: _handleSearchEnd,
|
onPressed: _handleSearchEnd,
|
||||||
tooltip: 'Back'
|
tooltip: 'Back',
|
||||||
),
|
),
|
||||||
title: new TextField(
|
title: new TextField(
|
||||||
|
controller: _searchQuery,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
hintText: 'Search stocks',
|
decoration: const InputDecoration(
|
||||||
onChanged: _handleSearchQueryChanged
|
hintText: 'Search stocks',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(context).canvasColor
|
backgroundColor: Theme.of(context).canvasColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleCreateCompany() {
|
void _handleCreateCompany() {
|
||||||
showModalBottomSheet<Null>(
|
showModalBottomSheet<Null>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) => new _CreateCompanySheet()
|
builder: (BuildContext context) => new _CreateCompanySheet(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,7 +315,7 @@ class StockHomeState extends State<StockHome> {
|
|||||||
tooltip: 'Create company',
|
tooltip: 'Create company',
|
||||||
child: new Icon(Icons.add),
|
child: new Icon(Icons.add),
|
||||||
backgroundColor: Colors.redAccent,
|
backgroundColor: Colors.redAccent,
|
||||||
onPressed: _handleCreateCompany
|
onPressed: _handleCreateCompany,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,9 +332,9 @@ class StockHomeState extends State<StockHome> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildStockTab(context, StockHomeTab.market, config.symbols),
|
_buildStockTab(context, StockHomeTab.market, config.symbols),
|
||||||
_buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols),
|
_buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols),
|
||||||
]
|
],
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,9 +347,11 @@ class _CreateCompanySheet extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new TextField(
|
new TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
hintText: 'Company Name',
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Company Name',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ export 'src/material/image_icon.dart';
|
|||||||
export 'src/material/ink_highlight.dart';
|
export 'src/material/ink_highlight.dart';
|
||||||
export 'src/material/ink_splash.dart';
|
export 'src/material/ink_splash.dart';
|
||||||
export 'src/material/ink_well.dart';
|
export 'src/material/ink_well.dart';
|
||||||
export 'src/material/input.dart';
|
export 'src/material/input_decorator.dart';
|
||||||
export 'src/material/list_tile.dart';
|
export 'src/material/list_tile.dart';
|
||||||
export 'src/material/material.dart';
|
export 'src/material/material.dart';
|
||||||
export 'src/material/mergeable_material.dart';
|
export 'src/material/mergeable_material.dart';
|
||||||
@ -72,6 +72,9 @@ export 'src/material/stepper.dart';
|
|||||||
export 'src/material/switch.dart';
|
export 'src/material/switch.dart';
|
||||||
export 'src/material/tab_controller.dart';
|
export 'src/material/tab_controller.dart';
|
||||||
export 'src/material/tabs.dart';
|
export 'src/material/tabs.dart';
|
||||||
|
export 'src/material/text_field.dart';
|
||||||
|
export 'src/material/text_form_field.dart';
|
||||||
|
export 'src/material/text_selection.dart';
|
||||||
export 'src/material/theme.dart';
|
export 'src/material/theme.dart';
|
||||||
export 'src/material/theme_data.dart';
|
export 'src/material/theme_data.dart';
|
||||||
export 'src/material/time_picker.dart';
|
export 'src/material/time_picker.dart';
|
||||||
|
@ -158,6 +158,19 @@ class _MergingListenable extends ChangeNotifier {
|
|||||||
child?.removeListener(notifyListeners);
|
child?.removeListener(notifyListeners);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final StringBuffer buffer = new StringBuffer();
|
||||||
|
buffer.write('_MergingListenable([');
|
||||||
|
for (int i = 0; i < _children.length; ++i) {
|
||||||
|
buffer.write(_children[i].toString());
|
||||||
|
if (i < _children.length - 1)
|
||||||
|
buffer.write(', ');
|
||||||
|
}
|
||||||
|
buffer.write('])');
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [ChangeNotifier] that holds a single value.
|
/// A [ChangeNotifier] that holds a single value.
|
||||||
|
@ -462,7 +462,7 @@ class DropdownButton<T> extends StatefulWidget {
|
|||||||
/// By default this button's height is the same as its menu items' heights.
|
/// By default this button's height is the same as its menu items' heights.
|
||||||
/// If isDense is true, the button's height is reduced by about half. This
|
/// If isDense is true, the button's height is reduced by about half. This
|
||||||
/// can be useful when the button is embedded in a container that adds
|
/// can be useful when the button is embedded in a container that adds
|
||||||
/// its own decorations, like [InputContainer].
|
/// its own decorations, like [InputDecorator].
|
||||||
final bool isDense;
|
final bool isDense;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,754 +0,0 @@
|
|||||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import 'colors.dart';
|
|
||||||
import 'debug.dart';
|
|
||||||
import 'icon.dart';
|
|
||||||
import 'icon_theme.dart';
|
|
||||||
import 'icon_theme_data.dart';
|
|
||||||
import 'text_selection.dart';
|
|
||||||
import 'theme.dart';
|
|
||||||
|
|
||||||
export 'package:flutter/services.dart' show TextInputType;
|
|
||||||
|
|
||||||
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
|
|
||||||
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
|
|
||||||
|
|
||||||
/// A simple undecorated text input field.
|
|
||||||
///
|
|
||||||
/// If you want decorations as specified in the Material spec (most likely),
|
|
||||||
/// use [Input] instead.
|
|
||||||
///
|
|
||||||
/// This widget is comparable to [Text] in that it does not include a margin
|
|
||||||
/// or any decoration outside the text itself. It is useful for applications,
|
|
||||||
/// like a search box, that don't need any additional decoration. It should
|
|
||||||
/// also be useful in custom widgets that support text input.
|
|
||||||
///
|
|
||||||
/// The [value] field must be updated each time the [onChanged] callback is
|
|
||||||
/// invoked. Be sure to include the full [value] provided by the [onChanged]
|
|
||||||
/// callback, or information like the current selection will be lost.
|
|
||||||
///
|
|
||||||
/// Requires one of its ancestors to be a [Material] widget.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [Input], which adds a label, a divider below the text field, and support for
|
|
||||||
/// an error message.
|
|
||||||
/// * [EditableText], a text field that does not require [Material] design.
|
|
||||||
class InputField extends StatefulWidget {
|
|
||||||
InputField({
|
|
||||||
Key key,
|
|
||||||
this.focusNode,
|
|
||||||
this.value,
|
|
||||||
this.keyboardType: TextInputType.text,
|
|
||||||
this.hintText,
|
|
||||||
this.style,
|
|
||||||
this.hintStyle,
|
|
||||||
this.obscureText: false,
|
|
||||||
this.maxLines: 1,
|
|
||||||
this.autofocus: false,
|
|
||||||
this.onChanged,
|
|
||||||
this.onSubmitted,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
/// Controls whether this widget has keyboard focus.
|
|
||||||
///
|
|
||||||
/// If null, this widget will create its own [FocusNode].
|
|
||||||
final FocusNode focusNode;
|
|
||||||
|
|
||||||
/// The current state of text of the input field. This includes the selected
|
|
||||||
/// text, if any, among other things.
|
|
||||||
final InputValue value;
|
|
||||||
|
|
||||||
/// The type of keyboard to use for editing the text.
|
|
||||||
final TextInputType keyboardType;
|
|
||||||
|
|
||||||
/// Text to show inline in the input field when it would otherwise be empty.
|
|
||||||
final String hintText;
|
|
||||||
|
|
||||||
/// The style to use for the text being edited.
|
|
||||||
final TextStyle style;
|
|
||||||
|
|
||||||
/// The style to use for the hint text.
|
|
||||||
///
|
|
||||||
/// Defaults to the specified TextStyle in style with the hintColor from
|
|
||||||
/// the ThemeData
|
|
||||||
final TextStyle hintStyle;
|
|
||||||
|
|
||||||
/// Whether to hide the text being edited (e.g., for passwords).
|
|
||||||
///
|
|
||||||
/// When this is set to true, all the characters in the input are replaced by
|
|
||||||
/// U+2022 BULLET characters (•).
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool obscureText;
|
|
||||||
|
|
||||||
/// The maximum number of lines for the text to span, wrapping if necessary.
|
|
||||||
/// If this is 1 (the default), the text will not wrap, but will scroll
|
|
||||||
/// horizontally instead.
|
|
||||||
final int maxLines;
|
|
||||||
|
|
||||||
/// Whether this input field should focus itself if nothing else is already focused.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool autofocus;
|
|
||||||
|
|
||||||
/// Called when the text being edited changes.
|
|
||||||
///
|
|
||||||
/// The [value] must be updated each time [onChanged] is invoked.
|
|
||||||
final ValueChanged<InputValue> onChanged;
|
|
||||||
|
|
||||||
/// Called when the user indicates that they are done editing the text in the field.
|
|
||||||
final ValueChanged<InputValue> onSubmitted;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_InputFieldState createState() => new _InputFieldState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InputFieldState extends State<InputField> {
|
|
||||||
final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();
|
|
||||||
|
|
||||||
FocusNode _focusNode;
|
|
||||||
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_focusNode?.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void requestKeyboard() {
|
|
||||||
_editableTextKey.currentState?.requestKeyboard();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
assert(debugCheckHasMaterial(context));
|
|
||||||
final InputValue value = config.value ?? InputValue.empty;
|
|
||||||
final ThemeData themeData = Theme.of(context);
|
|
||||||
final TextStyle textStyle = config.style ?? themeData.textTheme.subhead;
|
|
||||||
|
|
||||||
final List<Widget> stackChildren = <Widget>[
|
|
||||||
new GestureDetector(
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onTap: requestKeyboard,
|
|
||||||
child: new EditableText(
|
|
||||||
key: _editableTextKey,
|
|
||||||
value: value,
|
|
||||||
focusNode: _effectiveFocusNode,
|
|
||||||
style: textStyle,
|
|
||||||
obscureText: config.obscureText,
|
|
||||||
maxLines: config.maxLines,
|
|
||||||
autofocus: config.autofocus,
|
|
||||||
cursorColor: themeData.textSelectionColor,
|
|
||||||
selectionColor: themeData.textSelectionColor,
|
|
||||||
selectionControls: materialTextSelectionControls,
|
|
||||||
keyboardType: config.keyboardType,
|
|
||||||
onChanged: config.onChanged,
|
|
||||||
onSubmitted: config.onSubmitted,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (config.hintText != null && value.text.isEmpty) {
|
|
||||||
final TextStyle hintStyle = config.hintStyle ??
|
|
||||||
textStyle.copyWith(color: themeData.hintColor);
|
|
||||||
stackChildren.add(
|
|
||||||
new Positioned(
|
|
||||||
left: 0.0,
|
|
||||||
top: textStyle.fontSize - hintStyle.fontSize,
|
|
||||||
child: new IgnorePointer(
|
|
||||||
child: new Text(config.hintText, style: hintStyle),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RepaintBoundary(child: new Stack(children: stackChildren));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Displays the visual elements of a material design text field around an
|
|
||||||
/// arbitrary child widget.
|
|
||||||
///
|
|
||||||
/// Use InputContainer to create widgets that look and behave like the [Input]
|
|
||||||
/// widget.
|
|
||||||
///
|
|
||||||
/// Requires one of its ancestors to be a [Material] widget.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [Input], which combines an [InputContainer] with an [InputField].
|
|
||||||
class InputContainer extends StatefulWidget {
|
|
||||||
InputContainer({
|
|
||||||
Key key,
|
|
||||||
this.focused: false,
|
|
||||||
this.isEmpty: false,
|
|
||||||
this.icon,
|
|
||||||
this.labelText,
|
|
||||||
this.hintText,
|
|
||||||
this.errorText,
|
|
||||||
this.style,
|
|
||||||
this.isDense: false,
|
|
||||||
this.showDivider: true,
|
|
||||||
this.child,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
/// An icon to show adjacent to the input field.
|
|
||||||
///
|
|
||||||
/// The size and color of the icon is configured automatically using an
|
|
||||||
/// [IconTheme] and therefore does not need to be explicitly given in the
|
|
||||||
/// icon widget.
|
|
||||||
///
|
|
||||||
/// See [Icon], [ImageIcon].
|
|
||||||
final Widget icon;
|
|
||||||
|
|
||||||
/// Text that appears above the child or over it, if isEmpty is true.
|
|
||||||
final String labelText;
|
|
||||||
|
|
||||||
/// Text that appears over the child if isEmpty is true and labelText is null.
|
|
||||||
final String hintText;
|
|
||||||
|
|
||||||
/// Text that appears below the child. If errorText is non-null the divider
|
|
||||||
/// that appears below the child is red.
|
|
||||||
final String errorText;
|
|
||||||
|
|
||||||
/// The style to use for the hint. It's also used for the label when the label
|
|
||||||
/// appears over the child.
|
|
||||||
final TextStyle style;
|
|
||||||
|
|
||||||
/// Whether the input container is part of a dense form (i.e., uses less vertical space).
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool isDense;
|
|
||||||
|
|
||||||
/// True if the hint and label should be displayed as if the child had the focus.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool focused;
|
|
||||||
|
|
||||||
/// Should the hint and label be displayed as if no value had been input
|
|
||||||
/// to the child.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool isEmpty;
|
|
||||||
|
|
||||||
/// Whether to show a divider below the child and above the error text.
|
|
||||||
///
|
|
||||||
/// Defaults to true.
|
|
||||||
final bool showDivider;
|
|
||||||
|
|
||||||
/// The widget below this widget in the tree.
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_InputContainerState createState() => new _InputContainerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InputContainerState extends State<InputContainer> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
assert(debugCheckHasMaterial(context));
|
|
||||||
final ThemeData themeData = Theme.of(context);
|
|
||||||
final String errorText = config.errorText;
|
|
||||||
|
|
||||||
final TextStyle textStyle = config.style ?? themeData.textTheme.subhead;
|
|
||||||
Color activeColor = themeData.hintColor;
|
|
||||||
if (config.focused) {
|
|
||||||
switch (themeData.brightness) {
|
|
||||||
case Brightness.dark:
|
|
||||||
activeColor = themeData.accentColor;
|
|
||||||
break;
|
|
||||||
case Brightness.light:
|
|
||||||
activeColor = themeData.primaryColor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
double topPadding = config.isDense ? 12.0 : 16.0;
|
|
||||||
|
|
||||||
final List<Widget> stackChildren = <Widget>[];
|
|
||||||
|
|
||||||
// If we're not focused, there's not value, and labelText was provided,
|
|
||||||
// then the label appears where the hint would. And we will not show
|
|
||||||
// the hintText.
|
|
||||||
final bool hasInlineLabel = !config.focused && config.labelText != null && config.isEmpty;
|
|
||||||
|
|
||||||
if (config.labelText != null) {
|
|
||||||
final TextStyle labelStyle = hasInlineLabel ?
|
|
||||||
textStyle.copyWith(color: themeData.hintColor) :
|
|
||||||
themeData.textTheme.caption.copyWith(color: activeColor);
|
|
||||||
|
|
||||||
final double topPaddingIncrement = themeData.textTheme.caption.fontSize + (config.isDense ? 4.0 : 8.0);
|
|
||||||
double top = topPadding;
|
|
||||||
if (hasInlineLabel)
|
|
||||||
top += topPaddingIncrement + textStyle.fontSize - labelStyle.fontSize;
|
|
||||||
|
|
||||||
stackChildren.add(
|
|
||||||
new AnimatedPositioned(
|
|
||||||
left: 0.0,
|
|
||||||
top: top,
|
|
||||||
duration: _kTransitionDuration,
|
|
||||||
curve: _kTransitionCurve,
|
|
||||||
child: new _AnimatedLabel(
|
|
||||||
text: config.labelText,
|
|
||||||
style: labelStyle,
|
|
||||||
duration: _kTransitionDuration,
|
|
||||||
curve: _kTransitionCurve,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
topPadding += topPaddingIncrement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.hintText != null) {
|
|
||||||
final TextStyle hintStyle = textStyle.copyWith(color: themeData.hintColor);
|
|
||||||
stackChildren.add(
|
|
||||||
new Positioned(
|
|
||||||
left: 0.0,
|
|
||||||
top: topPadding + textStyle.fontSize - hintStyle.fontSize,
|
|
||||||
child: new AnimatedOpacity(
|
|
||||||
opacity: (config.isEmpty && !hasInlineLabel) ? 1.0 : 0.0,
|
|
||||||
duration: _kTransitionDuration,
|
|
||||||
curve: _kTransitionCurve,
|
|
||||||
child: new IgnorePointer(
|
|
||||||
child: new Text(config.hintText, style: hintStyle),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Color borderColor = errorText == null ? activeColor : themeData.errorColor;
|
|
||||||
final double bottomPadding = config.isDense ? 8.0 : 1.0;
|
|
||||||
final double bottomBorder = 2.0;
|
|
||||||
final double bottomHeight = config.isDense ? 14.0 : 18.0;
|
|
||||||
|
|
||||||
final EdgeInsets padding = new EdgeInsets.only(top: topPadding, bottom: bottomPadding);
|
|
||||||
final Border border = new Border(
|
|
||||||
bottom: new BorderSide(
|
|
||||||
color: borderColor,
|
|
||||||
width: bottomBorder,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
final EdgeInsets margin = new EdgeInsets.only(bottom: bottomHeight - (bottomPadding + bottomBorder));
|
|
||||||
|
|
||||||
Widget divider;
|
|
||||||
if (!config.showDivider) {
|
|
||||||
divider = new Container(
|
|
||||||
margin: margin + new EdgeInsets.only(bottom: bottomBorder),
|
|
||||||
padding: padding,
|
|
||||||
child: config.child,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
divider = new AnimatedContainer(
|
|
||||||
margin: margin,
|
|
||||||
padding: padding,
|
|
||||||
duration: _kTransitionDuration,
|
|
||||||
curve: _kTransitionCurve,
|
|
||||||
decoration: new BoxDecoration(
|
|
||||||
border: border,
|
|
||||||
),
|
|
||||||
child: config.child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
stackChildren.add(divider);
|
|
||||||
|
|
||||||
if (!config.isDense) {
|
|
||||||
final TextStyle errorStyle = themeData.textTheme.caption.copyWith(color: themeData.errorColor);
|
|
||||||
stackChildren.add(new Positioned(
|
|
||||||
left: 0.0,
|
|
||||||
bottom: 0.0,
|
|
||||||
child: new Text(errorText ?? '', style: errorStyle)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget textField = new Stack(children: stackChildren);
|
|
||||||
|
|
||||||
if (config.icon != null) {
|
|
||||||
final double iconSize = config.isDense ? 18.0 : 24.0;
|
|
||||||
final double iconTop = topPadding + (textStyle.fontSize - iconSize) / 2.0;
|
|
||||||
textField = new Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
new Container(
|
|
||||||
margin: new EdgeInsets.only(top: iconTop),
|
|
||||||
width: config.isDense ? 40.0 : 48.0,
|
|
||||||
child: new IconTheme.merge(
|
|
||||||
context: context,
|
|
||||||
data: new IconThemeData(
|
|
||||||
color: config.focused ? activeColor : Colors.black45,
|
|
||||||
size: config.isDense ? 18.0 : 24.0
|
|
||||||
),
|
|
||||||
child: config.icon
|
|
||||||
)
|
|
||||||
),
|
|
||||||
new Expanded(child: textField)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return textField;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A material design text input field.
|
|
||||||
///
|
|
||||||
/// The [value] field must be updated each time the [onChanged] callback is
|
|
||||||
/// invoked. Be sure to include the full [value] provided by the [onChanged]
|
|
||||||
/// callback, or information like the current selection will be lost.
|
|
||||||
///
|
|
||||||
/// Requires one of its ancestors to be a [Material] widget.
|
|
||||||
///
|
|
||||||
/// When using inside a [Form], consider using [TextField] instead.
|
|
||||||
///
|
|
||||||
/// Assuming that the input is already focused, the basic data flow for
|
|
||||||
/// retrieving user input is:
|
|
||||||
/// 1. User taps a character on the keyboard.
|
|
||||||
/// 2. The [onChanged] callback is called with the current [InputValue].
|
|
||||||
/// 3. Perform any necessary logic/validation on the current input value.
|
|
||||||
/// 4. Update the state of the [Input] widget accordingly through [State.setState].
|
|
||||||
///
|
|
||||||
/// For most cases, we recommend that you use the [Input] class within a
|
|
||||||
/// [StatefulWidget] so you can save and operate on the current value of the
|
|
||||||
/// input.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * <https://material.google.com/components/text-fields.html>
|
|
||||||
/// * [TextField], which simplifies steps 2-4 above.
|
|
||||||
class Input extends StatefulWidget {
|
|
||||||
/// Creates a text input field.
|
|
||||||
///
|
|
||||||
/// By default, the input uses a keyboard appropriate for text entry.
|
|
||||||
//
|
|
||||||
// If you change this constructor signature, please also update
|
|
||||||
// InputContainer, TextField, InputField.
|
|
||||||
Input({
|
|
||||||
Key key,
|
|
||||||
this.value,
|
|
||||||
this.focusNode,
|
|
||||||
this.keyboardType: TextInputType.text,
|
|
||||||
this.icon,
|
|
||||||
this.labelText,
|
|
||||||
this.hintText,
|
|
||||||
this.errorText,
|
|
||||||
this.style,
|
|
||||||
this.obscureText: false,
|
|
||||||
this.showDivider: true,
|
|
||||||
this.isDense: false,
|
|
||||||
this.autofocus: false,
|
|
||||||
this.maxLines: 1,
|
|
||||||
this.onChanged,
|
|
||||||
this.onSubmitted,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
/// The current state of text of the input field. This includes the selected
|
|
||||||
/// text, if any, among other things.
|
|
||||||
final InputValue value;
|
|
||||||
|
|
||||||
/// Controls whether this widget has keyboard focus.
|
|
||||||
///
|
|
||||||
/// If null, this widget will create its own [FocusNode].
|
|
||||||
final FocusNode focusNode;
|
|
||||||
|
|
||||||
/// The type of keyboard to use for editing the text.
|
|
||||||
final TextInputType keyboardType;
|
|
||||||
|
|
||||||
/// An icon to show adjacent to the input field.
|
|
||||||
///
|
|
||||||
/// The size and color of the icon is configured automatically using an
|
|
||||||
/// [IconTheme] and therefore does not need to be explicitly given in the
|
|
||||||
/// icon widget.
|
|
||||||
///
|
|
||||||
/// See [Icon], [ImageIcon].
|
|
||||||
final Widget icon;
|
|
||||||
|
|
||||||
/// Text to show above the input field.
|
|
||||||
final String labelText;
|
|
||||||
|
|
||||||
/// Text to show inline in the input field when it would otherwise be empty.
|
|
||||||
final String hintText;
|
|
||||||
|
|
||||||
/// Text to show when the input text is invalid.
|
|
||||||
final String errorText;
|
|
||||||
|
|
||||||
/// The style to use for the text being edited.
|
|
||||||
final TextStyle style;
|
|
||||||
|
|
||||||
/// Whether to hide the text being edited (e.g., for passwords).
|
|
||||||
///
|
|
||||||
/// When this is set to true, all the characters in the input are replaced by
|
|
||||||
/// U+2022 BULLET characters (•).
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool obscureText;
|
|
||||||
|
|
||||||
/// Whether to show a divider below the child and above the error text.
|
|
||||||
///
|
|
||||||
/// Defaults to true.
|
|
||||||
final bool showDivider;
|
|
||||||
|
|
||||||
/// Whether the input field is part of a dense form (i.e., uses less vertical space).
|
|
||||||
/// If true, [errorText] is not shown.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool isDense;
|
|
||||||
|
|
||||||
/// Whether this input field should focus itself if nothing else is already focused.
|
|
||||||
/// If true, the keyboard will open as soon as this input obtains focus. Otherwise,
|
|
||||||
/// the keyboard is only shown after the user taps the text field.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
// See https://github.com/flutter/flutter/issues/7035 for the rationale for this
|
|
||||||
// keyboard behavior.
|
|
||||||
final bool autofocus;
|
|
||||||
|
|
||||||
/// The maximum number of lines for the text to span, wrapping if necessary.
|
|
||||||
/// If this is 1 (the default), the text will not wrap, but will scroll
|
|
||||||
/// horizontally instead.
|
|
||||||
final int maxLines;
|
|
||||||
|
|
||||||
/// Called when the text being edited changes.
|
|
||||||
///
|
|
||||||
/// The [value] must be updated each time [onChanged] is invoked.
|
|
||||||
final ValueChanged<InputValue> onChanged;
|
|
||||||
|
|
||||||
/// Called when the user indicates that they are done editing the text in the field.
|
|
||||||
final ValueChanged<InputValue> onSubmitted;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_InputState createState() => new _InputState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InputState extends State<Input> {
|
|
||||||
final GlobalKey<_InputFieldState> _inputFieldKey = new GlobalKey<_InputFieldState>();
|
|
||||||
|
|
||||||
FocusNode _focusNode;
|
|
||||||
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_focusNode?.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final bool isEmpty = (config.value ?? InputValue.empty).text.isEmpty;
|
|
||||||
final FocusNode focusNode = _effectiveFocusNode;
|
|
||||||
|
|
||||||
return new GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
_inputFieldKey.currentState?.requestKeyboard();
|
|
||||||
},
|
|
||||||
child: new AnimatedBuilder(
|
|
||||||
animation: focusNode,
|
|
||||||
builder: (BuildContext context, Widget child) {
|
|
||||||
return new InputContainer(
|
|
||||||
focused: focusNode.hasFocus,
|
|
||||||
isEmpty: isEmpty,
|
|
||||||
icon: config.icon,
|
|
||||||
labelText: config.labelText,
|
|
||||||
hintText: config.hintText,
|
|
||||||
errorText: config.errorText,
|
|
||||||
style: config.style,
|
|
||||||
isDense: config.isDense,
|
|
||||||
showDivider: config.showDivider,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: new InputField(
|
|
||||||
key: _inputFieldKey,
|
|
||||||
focusNode: focusNode,
|
|
||||||
value: config.value,
|
|
||||||
style: config.style,
|
|
||||||
obscureText: config.obscureText,
|
|
||||||
maxLines: config.maxLines,
|
|
||||||
autofocus: config.autofocus,
|
|
||||||
keyboardType: config.keyboardType,
|
|
||||||
onChanged: config.onChanged,
|
|
||||||
onSubmitted: config.onSubmitted,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A [FormField] that contains an [Input].
|
|
||||||
///
|
|
||||||
/// This is a convenience widget that simply wraps an [Input] widget in a
|
|
||||||
/// [FormField]. The [FormField] maintains the current value of the [Input] so
|
|
||||||
/// that you don't need to manage it yourself.
|
|
||||||
///
|
|
||||||
/// A [Form] ancestor is not required. The [Form] simply makes it easier to
|
|
||||||
/// save, reset, or validate multiple fields at once. To use without a [Form],
|
|
||||||
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
|
|
||||||
/// save or reset the form field.
|
|
||||||
///
|
|
||||||
/// To see the use of [TextField], compare these two ways of a implementing
|
|
||||||
/// a simple two text field form.
|
|
||||||
///
|
|
||||||
/// Using [TextField]:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// String _firstName, _lastName;
|
|
||||||
/// GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
|
|
||||||
/// ...
|
|
||||||
/// new Form(
|
|
||||||
/// key: _formKey,
|
|
||||||
/// child: new Row(
|
|
||||||
/// children: <Widget>[
|
|
||||||
/// new TextField(
|
|
||||||
/// labelText: 'First Name',
|
|
||||||
/// onSaved: (InputValue value) { _firstName = value.text; }
|
|
||||||
/// ),
|
|
||||||
/// new TextField(
|
|
||||||
/// labelText: 'Last Name',
|
|
||||||
/// onSaved: (InputValue value) { _lastName = value.text; }
|
|
||||||
/// ),
|
|
||||||
/// new RaisedButton(
|
|
||||||
/// child: new Text('SUBMIT'),
|
|
||||||
/// // Instead of _formKey.currentState, you could wrap the
|
|
||||||
/// // RaisedButton in a Builder widget to get access to a BuildContext,
|
|
||||||
/// // and use Form.of(context).
|
|
||||||
/// onPressed: () { _formKey.currentState.save(); },
|
|
||||||
/// ),
|
|
||||||
/// )
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Using [Input] directly:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// String _firstName, _lastName;
|
|
||||||
/// InputValue _firstNameValue = const InputValue();
|
|
||||||
/// InputValue _lastNameValue = const InputValue();
|
|
||||||
/// ...
|
|
||||||
/// new Row(
|
|
||||||
/// children: <Widget>[
|
|
||||||
/// new Input(
|
|
||||||
/// value: _firstNameValue,
|
|
||||||
/// labelText: 'First Name',
|
|
||||||
/// onChanged: (InputValue value) { setState( () { _firstNameValue = value; } ); }
|
|
||||||
/// ),
|
|
||||||
/// new Input(
|
|
||||||
/// value: _lastNameValue,
|
|
||||||
/// labelText: 'Last Name',
|
|
||||||
/// onChanged: (InputValue value) { setState( () { _lastNameValue = value; } ); }
|
|
||||||
/// ),
|
|
||||||
/// new RaisedButton(
|
|
||||||
/// child: new Text('SUBMIT'),
|
|
||||||
/// onPressed: () {
|
|
||||||
/// _firstName = _firstNameValue.text;
|
|
||||||
/// _lastName = _lastNameValue.text;
|
|
||||||
/// },
|
|
||||||
/// ),
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
class TextField extends FormField<InputValue> {
|
|
||||||
TextField({
|
|
||||||
Key key,
|
|
||||||
FocusNode focusNode,
|
|
||||||
TextInputType keyboardType: TextInputType.text,
|
|
||||||
Icon icon,
|
|
||||||
String labelText,
|
|
||||||
String hintText,
|
|
||||||
TextStyle style,
|
|
||||||
bool obscureText: false,
|
|
||||||
bool isDense: false,
|
|
||||||
bool autofocus: false,
|
|
||||||
int maxLines: 1,
|
|
||||||
InputValue initialValue: InputValue.empty,
|
|
||||||
FormFieldSetter<InputValue> onSaved,
|
|
||||||
FormFieldValidator<InputValue> validator,
|
|
||||||
ValueChanged<InputValue> onChanged,
|
|
||||||
}) : super(
|
|
||||||
key: key,
|
|
||||||
initialValue: initialValue,
|
|
||||||
onSaved: onSaved,
|
|
||||||
validator: validator,
|
|
||||||
builder: (FormFieldState<InputValue> field) {
|
|
||||||
return new Input(
|
|
||||||
focusNode: focusNode,
|
|
||||||
keyboardType: keyboardType,
|
|
||||||
icon: icon,
|
|
||||||
labelText: labelText,
|
|
||||||
hintText: hintText,
|
|
||||||
style: style,
|
|
||||||
obscureText: obscureText,
|
|
||||||
isDense: isDense,
|
|
||||||
autofocus: autofocus,
|
|
||||||
maxLines: maxLines,
|
|
||||||
value: field.value,
|
|
||||||
onChanged: (InputValue value) {
|
|
||||||
field.onChanged(value);
|
|
||||||
if (onChanged != null)
|
|
||||||
onChanged(value);
|
|
||||||
},
|
|
||||||
errorText: field.errorText,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper widget to smoothly animate the labelText of an Input, as it
|
|
||||||
// transitions between inline and caption.
|
|
||||||
class _AnimatedLabel extends ImplicitlyAnimatedWidget {
|
|
||||||
_AnimatedLabel({
|
|
||||||
Key key,
|
|
||||||
this.text,
|
|
||||||
@required this.style,
|
|
||||||
Curve curve: Curves.linear,
|
|
||||||
Duration duration,
|
|
||||||
}) : super(key: key, curve: curve, duration: duration) {
|
|
||||||
assert(style != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String text;
|
|
||||||
final TextStyle style;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_AnimatedLabelState createState() => new _AnimatedLabelState();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillDescription(List<String> description) {
|
|
||||||
super.debugFillDescription(description);
|
|
||||||
'$style'.split('\n').forEach(description.add);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedLabelState extends AnimatedWidgetBaseState<_AnimatedLabel> {
|
|
||||||
TextStyleTween _style;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void forEachTween(TweenVisitor<dynamic> visitor) {
|
|
||||||
_style = visitor(_style, config.style, (dynamic value) => new TextStyleTween(begin: value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
TextStyle style = _style.evaluate(animation);
|
|
||||||
double scale = 1.0;
|
|
||||||
if (style.fontSize != config.style.fontSize) {
|
|
||||||
// While the fontSize is transitioning, use a scaled Transform as a
|
|
||||||
// fraction of the original fontSize. That way we get a smooth scaling
|
|
||||||
// effect with no snapping between discrete font sizes.
|
|
||||||
scale = style.fontSize / config.style.fontSize;
|
|
||||||
style = style.copyWith(fontSize: config.style.fontSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Transform(
|
|
||||||
transform: new Matrix4.identity()..scale(scale),
|
|
||||||
child: new Text(
|
|
||||||
config.text,
|
|
||||||
style: style,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
498
packages/flutter/lib/src/material/input_decorator.dart
Normal file
498
packages/flutter/lib/src/material/input_decorator.dart
Normal file
@ -0,0 +1,498 @@
|
|||||||
|
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'colors.dart';
|
||||||
|
import 'debug.dart';
|
||||||
|
import 'icon.dart';
|
||||||
|
import 'icon_theme.dart';
|
||||||
|
import 'icon_theme_data.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
|
||||||
|
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
|
||||||
|
|
||||||
|
/// Text and styles used to label an input field.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [TextField], which is a text input widget that uses an
|
||||||
|
/// [InputDecoration].
|
||||||
|
/// * [InputDecorator], which is a widget that draws an [InputDecoration]
|
||||||
|
/// around an arbitrary child widget.
|
||||||
|
class InputDecoration {
|
||||||
|
/// Creates a bundle of text and styles used to label an input field.
|
||||||
|
///
|
||||||
|
/// Sets the [isCollapsed] property to false. To create a decoration that does
|
||||||
|
/// not reserve space for [labelText] or [errorText], use
|
||||||
|
/// [InputDecoration.collapsed].
|
||||||
|
const InputDecoration({
|
||||||
|
this.icon,
|
||||||
|
this.labelText,
|
||||||
|
this.labelStyle,
|
||||||
|
this.hintText,
|
||||||
|
this.hintStyle,
|
||||||
|
this.errorText,
|
||||||
|
this.errorStyle,
|
||||||
|
this.isDense: false,
|
||||||
|
this.hideDivider: false,
|
||||||
|
}) : isCollapsed = false;
|
||||||
|
|
||||||
|
/// Creates a decoration that is the same size as the input field.
|
||||||
|
///
|
||||||
|
/// This type of input decoration does not include a divider or an icon and
|
||||||
|
/// does not reserve space for [labelText] or [errorText].
|
||||||
|
///
|
||||||
|
/// Sets the [isCollapsed] property to true.
|
||||||
|
const InputDecoration.collapsed({
|
||||||
|
@required this.hintText,
|
||||||
|
this.hintStyle,
|
||||||
|
}) : icon = null,
|
||||||
|
labelText = null,
|
||||||
|
labelStyle = null,
|
||||||
|
errorText = null,
|
||||||
|
errorStyle = null,
|
||||||
|
isDense = false,
|
||||||
|
isCollapsed = true,
|
||||||
|
hideDivider = true;
|
||||||
|
|
||||||
|
/// An icon to show before the input field.
|
||||||
|
///
|
||||||
|
/// The size and color of the icon is configured automatically using an
|
||||||
|
/// [IconTheme] and therefore does not need to be explicitly given in the
|
||||||
|
/// icon widget.
|
||||||
|
///
|
||||||
|
/// See [Icon], [ImageIcon].
|
||||||
|
final Widget icon;
|
||||||
|
|
||||||
|
/// Text that describes the input field.
|
||||||
|
///
|
||||||
|
/// When the input field is empty and unfocused, the label is displayed on
|
||||||
|
/// top of the input field (i.e., at the same location on the screen where
|
||||||
|
/// text my be entered in the input field). When the input field receives
|
||||||
|
/// focus (or if the field is non-empty), the label moves above (i.e.,
|
||||||
|
/// vertically adjacent to) the input field.
|
||||||
|
final String labelText;
|
||||||
|
|
||||||
|
/// The style to use for the [labelText] when the label is above (i.e.,
|
||||||
|
/// vertically adjacent to) the input field.
|
||||||
|
///
|
||||||
|
/// When the [labelText] is on top of the input field, the text uses the
|
||||||
|
/// [hintStyle] instead.
|
||||||
|
///
|
||||||
|
/// If null, defaults of a value derived from the base [TextStyle] for the
|
||||||
|
/// input field and the current [Theme].
|
||||||
|
final TextStyle labelStyle;
|
||||||
|
|
||||||
|
/// Text that suggests what sort of input the field accepts.
|
||||||
|
///
|
||||||
|
/// Displayed on top of the input field (i.e., at the same location on the
|
||||||
|
/// screen where text my be entered in the input field) when the input field
|
||||||
|
/// is empty and either (a) [labelText] is null or (b) the input field has
|
||||||
|
/// focus.
|
||||||
|
final String hintText;
|
||||||
|
|
||||||
|
/// The style to use for the [hintText].
|
||||||
|
///
|
||||||
|
/// Also used for the [labelText] when the [labelText] is displayed on
|
||||||
|
/// top of the input field (i.e., at the same location on the screen where
|
||||||
|
/// text my be entered in the input field).
|
||||||
|
///
|
||||||
|
/// If null, defaults of a value derived from the base [TextStyle] for the
|
||||||
|
/// input field and the current [Theme].
|
||||||
|
final TextStyle hintStyle;
|
||||||
|
|
||||||
|
/// Text that appears below the input field.
|
||||||
|
///
|
||||||
|
/// If non-null the divider, that appears below the input field is red.
|
||||||
|
final String errorText;
|
||||||
|
|
||||||
|
/// The style to use for the [errorText.
|
||||||
|
///
|
||||||
|
/// If null, defaults of a value derived from the base [TextStyle] for the
|
||||||
|
/// input field and the current [Theme].
|
||||||
|
final TextStyle errorStyle;
|
||||||
|
|
||||||
|
/// Whether the input field is part of a dense form (i.e., uses less vertical
|
||||||
|
/// space).
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool isDense;
|
||||||
|
|
||||||
|
/// Whether the decoration is the same size as the input field.
|
||||||
|
///
|
||||||
|
/// A collapsed decoration cannot have [labelText], [errorText], an [icon], or
|
||||||
|
/// a divider because those elements require extra space.
|
||||||
|
///
|
||||||
|
/// To create a collapsed input decoration, use [InputDecoration..collapsed].
|
||||||
|
final bool isCollapsed;
|
||||||
|
|
||||||
|
/// Whether to hide the divider below the input field and above the error text.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool hideDivider;
|
||||||
|
|
||||||
|
/// Creates a copy of this input decoration but with the given fields replaced
|
||||||
|
/// with the new values.
|
||||||
|
///
|
||||||
|
/// Always sets [isCollapsed] to false.
|
||||||
|
InputDecoration copyWith({
|
||||||
|
Widget icon,
|
||||||
|
String labelText,
|
||||||
|
TextStyle labelStyle,
|
||||||
|
String hintText,
|
||||||
|
TextStyle hintStyle,
|
||||||
|
String errorText,
|
||||||
|
TextStyle errorStyle,
|
||||||
|
bool isDense,
|
||||||
|
bool hideDivider,
|
||||||
|
}) {
|
||||||
|
return new InputDecoration(
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
labelText: labelText ?? this.labelText,
|
||||||
|
labelStyle: labelStyle ?? this.labelStyle,
|
||||||
|
hintText: hintText ?? this.hintText,
|
||||||
|
hintStyle: hintStyle ?? this.hintStyle,
|
||||||
|
errorText: errorText ?? this.errorText,
|
||||||
|
errorStyle: errorStyle ?? this.errorStyle,
|
||||||
|
isDense: isDense ?? this.isDense,
|
||||||
|
hideDivider: hideDivider ?? this.hideDivider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
if (identical(this, other))
|
||||||
|
return true;
|
||||||
|
if (other.runtimeType != runtimeType)
|
||||||
|
return false;
|
||||||
|
final InputDecoration typedOther = other;
|
||||||
|
return typedOther.icon == icon
|
||||||
|
&& typedOther.labelText == labelText
|
||||||
|
&& typedOther.labelStyle == labelStyle
|
||||||
|
&& typedOther.hintText == hintText
|
||||||
|
&& typedOther.hintStyle == hintStyle
|
||||||
|
&& typedOther.errorText == errorText
|
||||||
|
&& typedOther.errorStyle == errorStyle
|
||||||
|
&& typedOther.isDense == isDense
|
||||||
|
&& typedOther.isCollapsed == isCollapsed
|
||||||
|
&& typedOther.hideDivider == hideDivider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return hashValues(
|
||||||
|
icon,
|
||||||
|
labelText,
|
||||||
|
labelStyle,
|
||||||
|
hintText,
|
||||||
|
hintStyle,
|
||||||
|
errorText,
|
||||||
|
errorStyle,
|
||||||
|
isDense,
|
||||||
|
isCollapsed,
|
||||||
|
hideDivider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final List<String> description = <String>[];
|
||||||
|
if (icon != null)
|
||||||
|
description.add('icon: $icon');
|
||||||
|
if (labelText != null)
|
||||||
|
description.add('labelText: "$labelText"');
|
||||||
|
if (hintText != null)
|
||||||
|
description.add('hintText: "$hintText"');
|
||||||
|
if (errorText != null)
|
||||||
|
description.add('errorText: "$errorText"');
|
||||||
|
if (isDense)
|
||||||
|
description.add('isDense: $isDense');
|
||||||
|
if (isCollapsed)
|
||||||
|
description.add('isCollapsed: $isCollapsed');
|
||||||
|
if (hideDivider)
|
||||||
|
description.add('hideDivider: $hideDivider');
|
||||||
|
return 'InputDecoration(${description.join(', ')})';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays the visual elements of a Material Design text field around an
|
||||||
|
/// arbitrary widget.
|
||||||
|
///
|
||||||
|
/// Use [InputDecorator] to create widgets that look and behave like a
|
||||||
|
/// [TextField] but can be used to input information other than text.
|
||||||
|
///
|
||||||
|
/// Requires one of its ancestors to be a [Material] widget.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [TextField], which uses an [InputDecorator] to draw labels and other
|
||||||
|
/// visual elements around a text entry widget.
|
||||||
|
class InputDecorator extends StatelessWidget {
|
||||||
|
/// Creates a widget that displayes labels and other visual elements similar
|
||||||
|
/// to a [TextField].
|
||||||
|
InputDecorator({
|
||||||
|
Key key,
|
||||||
|
@required this.decoration,
|
||||||
|
this.baseStyle,
|
||||||
|
this.isFocused: false,
|
||||||
|
this.isEmpty: false,
|
||||||
|
this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The text and styles to use when decorating the child.
|
||||||
|
final InputDecoration decoration;
|
||||||
|
|
||||||
|
/// The style on which to base the label, hint, and error styles if the
|
||||||
|
/// [decoration] does not provide explicit styles.
|
||||||
|
///
|
||||||
|
/// If null, defaults to a text style from the current [Theme].
|
||||||
|
final TextStyle baseStyle;
|
||||||
|
|
||||||
|
/// Whether the input field has focus.
|
||||||
|
///
|
||||||
|
/// Determines the position of the label text and the color of the divider.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool isFocused;
|
||||||
|
|
||||||
|
/// Whether the input field is empty.
|
||||||
|
///
|
||||||
|
/// Determines the position of the label text and whether to display the hint
|
||||||
|
/// text.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool isEmpty;
|
||||||
|
|
||||||
|
/// The widget below this widget in the tree.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillDescription(List<String> description) {
|
||||||
|
super.debugFillDescription(description);
|
||||||
|
description.add('decoration: $decoration');
|
||||||
|
description.add('baseStyle: $baseStyle');
|
||||||
|
description.add('isFocused: $isFocused');
|
||||||
|
description.add('isEmpty: $isEmpty');
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getActiveColor(ThemeData themeData) {
|
||||||
|
if (isFocused) {
|
||||||
|
switch (themeData.brightness) {
|
||||||
|
case Brightness.dark:
|
||||||
|
return themeData.accentColor;
|
||||||
|
case Brightness.light:
|
||||||
|
return themeData.primaryColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return themeData.hintColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(Color borderColor, double topPadding, bool isDense) {
|
||||||
|
final double bottomPadding = isDense ? 8.0 : 1.0;
|
||||||
|
const double bottomBorder = 2.0;
|
||||||
|
final double bottomHeight = isDense ? 14.0 : 18.0;
|
||||||
|
|
||||||
|
final EdgeInsets padding = new EdgeInsets.only(top: topPadding, bottom: bottomPadding);
|
||||||
|
final EdgeInsets margin = new EdgeInsets.only(bottom: bottomHeight - (bottomPadding + bottomBorder));
|
||||||
|
|
||||||
|
if (decoration.hideDivider) {
|
||||||
|
return new Container(
|
||||||
|
margin: margin + new EdgeInsets.only(bottom: bottomBorder),
|
||||||
|
padding: padding,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AnimatedContainer(
|
||||||
|
margin: margin,
|
||||||
|
padding: padding,
|
||||||
|
duration: _kTransitionDuration,
|
||||||
|
curve: _kTransitionCurve,
|
||||||
|
decoration: new BoxDecoration(
|
||||||
|
border: new Border(
|
||||||
|
bottom: new BorderSide(
|
||||||
|
color: borderColor,
|
||||||
|
width: bottomBorder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(debugCheckHasMaterial(context));
|
||||||
|
final ThemeData themeData = Theme.of(context);
|
||||||
|
|
||||||
|
final bool isDense = decoration.isDense;
|
||||||
|
final bool isCollapsed = decoration.isCollapsed;
|
||||||
|
assert(!isDense || !isCollapsed);
|
||||||
|
|
||||||
|
final String labelText = decoration.labelText;
|
||||||
|
final String hintText = decoration.hintText;
|
||||||
|
final String errorText = decoration.errorText;
|
||||||
|
|
||||||
|
final TextStyle baseStyle = this.baseStyle ?? themeData.textTheme.subhead;
|
||||||
|
final TextStyle hintStyle = decoration.hintStyle ?? baseStyle.copyWith(color: themeData.hintColor);
|
||||||
|
|
||||||
|
final Color activeColor = _getActiveColor(themeData);
|
||||||
|
|
||||||
|
double topPadding = isDense ? 12.0 : 16.0;
|
||||||
|
|
||||||
|
final List<Widget> stackChildren = <Widget>[];
|
||||||
|
|
||||||
|
// If we're not focused, there's not value, and labelText was provided,
|
||||||
|
// then the label appears where the hint would. And we will not show
|
||||||
|
// the hintText.
|
||||||
|
final bool hasInlineLabel = !isFocused && labelText != null && isEmpty;
|
||||||
|
|
||||||
|
if (labelText != null) {
|
||||||
|
assert(!isCollapsed);
|
||||||
|
final TextStyle labelStyle = hasInlineLabel ?
|
||||||
|
hintStyle : (decoration.labelStyle ?? themeData.textTheme.caption.copyWith(color: activeColor));
|
||||||
|
|
||||||
|
final double topPaddingIncrement = themeData.textTheme.caption.fontSize + (isDense ? 4.0 : 8.0);
|
||||||
|
double top = topPadding;
|
||||||
|
if (hasInlineLabel)
|
||||||
|
top += topPaddingIncrement + baseStyle.fontSize - labelStyle.fontSize;
|
||||||
|
|
||||||
|
stackChildren.add(
|
||||||
|
new AnimatedPositioned(
|
||||||
|
left: 0.0,
|
||||||
|
top: top,
|
||||||
|
duration: _kTransitionDuration,
|
||||||
|
curve: _kTransitionCurve,
|
||||||
|
child: new _AnimatedLabel(
|
||||||
|
text: labelText,
|
||||||
|
style: labelStyle,
|
||||||
|
duration: _kTransitionDuration,
|
||||||
|
curve: _kTransitionCurve,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
topPadding += topPaddingIncrement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hintText != null) {
|
||||||
|
stackChildren.add(
|
||||||
|
new Positioned(
|
||||||
|
left: 0.0,
|
||||||
|
top: topPadding + baseStyle.fontSize - hintStyle.fontSize,
|
||||||
|
child: new AnimatedOpacity(
|
||||||
|
opacity: (isEmpty && !hasInlineLabel) ? 1.0 : 0.0,
|
||||||
|
duration: _kTransitionDuration,
|
||||||
|
curve: _kTransitionCurve,
|
||||||
|
child: new Text(hintText, style: hintStyle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
stackChildren.add(child);
|
||||||
|
} else {
|
||||||
|
final Color borderColor = errorText == null ? activeColor : themeData.errorColor;
|
||||||
|
stackChildren.add(_buildContent(borderColor, topPadding, isDense));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDense && errorText != null) {
|
||||||
|
assert(!isCollapsed);
|
||||||
|
final TextStyle errorStyle = decoration.errorStyle ?? themeData.textTheme.caption.copyWith(color: themeData.errorColor);
|
||||||
|
stackChildren.add(new Positioned(
|
||||||
|
left: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
child: new Text(errorText, style: errorStyle)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget result = new Stack(children: stackChildren);
|
||||||
|
|
||||||
|
if (decoration.icon != null) {
|
||||||
|
assert(!isCollapsed);
|
||||||
|
final double iconSize = isDense ? 18.0 : 24.0;
|
||||||
|
final double iconTop = topPadding + (baseStyle.fontSize - iconSize) / 2.0;
|
||||||
|
result = new Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
new Container(
|
||||||
|
margin: new EdgeInsets.only(top: iconTop),
|
||||||
|
width: isDense ? 40.0 : 48.0,
|
||||||
|
child: new IconTheme.merge(
|
||||||
|
context: context,
|
||||||
|
data: new IconThemeData(
|
||||||
|
color: isFocused ? activeColor : Colors.black45,
|
||||||
|
size: isDense ? 18.0 : 24.0,
|
||||||
|
),
|
||||||
|
child: decoration.icon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
new Expanded(child: result),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smoothly animate the label of an InputDecorator as the label
|
||||||
|
// transitions between inline and caption.
|
||||||
|
class _AnimatedLabel extends ImplicitlyAnimatedWidget {
|
||||||
|
_AnimatedLabel({
|
||||||
|
Key key,
|
||||||
|
this.text,
|
||||||
|
@required this.style,
|
||||||
|
Curve curve: Curves.linear,
|
||||||
|
@required Duration duration,
|
||||||
|
}) : super(key: key, curve: curve, duration: duration) {
|
||||||
|
assert(style != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final TextStyle style;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AnimatedLabelState createState() => new _AnimatedLabelState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillDescription(List<String> description) {
|
||||||
|
super.debugFillDescription(description);
|
||||||
|
'$style'.split('\n').forEach(description.add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedLabelState extends AnimatedWidgetBaseState<_AnimatedLabel> {
|
||||||
|
TextStyleTween _style;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void forEachTween(TweenVisitor<dynamic> visitor) {
|
||||||
|
_style = visitor(_style, config.style, (dynamic value) => new TextStyleTween(begin: value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
TextStyle style = _style.evaluate(animation);
|
||||||
|
double scale = 1.0;
|
||||||
|
if (style.fontSize != config.style.fontSize) {
|
||||||
|
// While the fontSize is transitioning, use a scaled Transform as a
|
||||||
|
// fraction of the original fontSize. That way we get a smooth scaling
|
||||||
|
// effect with no snapping between discrete font sizes.
|
||||||
|
scale = style.fontSize / config.style.fontSize;
|
||||||
|
style = style.copyWith(fontSize: config.style.fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Transform(
|
||||||
|
transform: new Matrix4.identity()..scale(scale),
|
||||||
|
child: new Text(
|
||||||
|
config.text,
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
268
packages/flutter/lib/src/material/text_field.dart
Normal file
268
packages/flutter/lib/src/material/text_field.dart
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'input_decorator.dart';
|
||||||
|
import 'text_selection.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
export 'package:flutter/services.dart' show TextInputType;
|
||||||
|
|
||||||
|
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
|
||||||
|
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
|
||||||
|
|
||||||
|
/// A simple undecorated text input field.
|
||||||
|
///
|
||||||
|
/// If you want decorations as specified in the Material spec (most likely),
|
||||||
|
/// use [Input] instead.
|
||||||
|
///
|
||||||
|
/// This widget is comparable to [Text] in that it does not include a margin
|
||||||
|
/// or any decoration outside the text itself. It is useful for applications,
|
||||||
|
/// like a search box, that don't need any additional decoration. It should
|
||||||
|
/// also be useful in custom widgets that support text input.
|
||||||
|
///
|
||||||
|
/// The [value] field must be updated each time the [onChanged] callback is
|
||||||
|
/// invoked. Be sure to include the full [value] provided by the [onChanged]
|
||||||
|
/// callback, or information like the current selection will be lost.
|
||||||
|
///
|
||||||
|
/// Requires one of its ancestors to be a [Material] widget.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Input], which adds a label, a divider below the text field, and support for
|
||||||
|
/// an error message.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// A Material Design text field.
|
||||||
|
///
|
||||||
|
/// A text field lets the user enter text, either with hardware keyboard or with
|
||||||
|
/// an onscreen keyboard.
|
||||||
|
///
|
||||||
|
/// The text field calls the [onChanged] callback whenever the user changes the
|
||||||
|
/// text in the field. If the user indicates that they are done typing in the
|
||||||
|
/// field (e.g., by pressing a button on the soft keyboard), the text field
|
||||||
|
/// calls the [onSubmitted] callback.
|
||||||
|
///
|
||||||
|
/// To control the text that is displayed in the text field, use the
|
||||||
|
/// [controller]. For example, to set the initial value of the text field, use
|
||||||
|
/// a [controller] that already contains some text. The [controller] can also
|
||||||
|
/// control the selection and composing region (and to observe changes to the
|
||||||
|
/// text, selection, and composing region).
|
||||||
|
///
|
||||||
|
/// By default, a text field has a [decoration] that draws a divider below the
|
||||||
|
/// text field. You can use the [decoration] property to control the decoration,
|
||||||
|
/// for example by adding a label or an icon. If you set the [decoration]
|
||||||
|
/// property to null, the decoration will be removed entirely, including the
|
||||||
|
/// extra padding introduced by the decoration to save space for the labels.
|
||||||
|
///
|
||||||
|
/// If [decoration] is non-null (which is the default), the text field requires
|
||||||
|
/// one of its ancestors to be a [Material] widget.
|
||||||
|
///
|
||||||
|
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
|
||||||
|
/// consider using [TextFormField].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * <https://material.google.com/components/text-fields.html>
|
||||||
|
/// * [TextFormField], which integrates with the [Form] widget.
|
||||||
|
/// * [InputDecorator], which shows the labels and other visual elements that
|
||||||
|
/// surround the actual text editing widget.
|
||||||
|
/// * [EditableText], which is the raw text editing control at the heart of a
|
||||||
|
/// [TextField]. (The [EditableText] widget is rarely used directly unless
|
||||||
|
/// you are implementing an entirely different design language, such as
|
||||||
|
/// Cupertino.)
|
||||||
|
class TextField extends StatefulWidget {
|
||||||
|
/// Creates a Material Design text field.
|
||||||
|
///
|
||||||
|
/// If [decoration] is non-null (which is the default), the text field requires
|
||||||
|
/// one of its ancestors to be a [Material] widget.
|
||||||
|
///
|
||||||
|
/// To remove the decoration entirely (including the extra padding introduced
|
||||||
|
/// by the decoration to save space for the labels), set the [decoration] to
|
||||||
|
/// null.
|
||||||
|
TextField({
|
||||||
|
Key key,
|
||||||
|
this.controller,
|
||||||
|
this.focusNode,
|
||||||
|
this.decoration: const InputDecoration(),
|
||||||
|
this.keyboardType: TextInputType.text,
|
||||||
|
this.style,
|
||||||
|
this.autofocus: false,
|
||||||
|
this.obscureText: false,
|
||||||
|
this.maxLines: 1,
|
||||||
|
this.onChanged,
|
||||||
|
this.onSubmitted,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Controls the text being edited.
|
||||||
|
///
|
||||||
|
/// If null, this widget will creates its own [TextEditingController].
|
||||||
|
final TextEditingController controller;
|
||||||
|
|
||||||
|
/// Controls whether this widget has keyboard focus.
|
||||||
|
///
|
||||||
|
/// If null, this widget will create its own [FocusNode].
|
||||||
|
final FocusNode focusNode;
|
||||||
|
|
||||||
|
/// The decoration to show around the text field.
|
||||||
|
///
|
||||||
|
/// By default, draws a horizontal line under the input field but can be
|
||||||
|
/// configured to show an icon, label, hint text, and error text.
|
||||||
|
///
|
||||||
|
/// Set this field to null to remove the decoration entirely (including the
|
||||||
|
/// extra padding introduced by the decoration to save space for the labels).
|
||||||
|
final InputDecoration decoration;
|
||||||
|
|
||||||
|
/// The type of keyboard to use for editing the text.
|
||||||
|
final TextInputType keyboardType;
|
||||||
|
|
||||||
|
/// The style to use for the text being edited.
|
||||||
|
///
|
||||||
|
/// This text style is also used as the base style for the [decoration].
|
||||||
|
///
|
||||||
|
/// If null, defaults to a text style from the current [Theme].
|
||||||
|
final TextStyle style;
|
||||||
|
|
||||||
|
/// Whether this input field should focus itself if nothing else is already
|
||||||
|
/// focused.
|
||||||
|
///
|
||||||
|
/// If true, the keyboard will open as soon as this input obtains focus.
|
||||||
|
/// Otherwise, the keyboard is only shown after the user taps the text field.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
// See https://github.com/flutter/flutter/issues/7035 for the rationale for this
|
||||||
|
// keyboard behavior.
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// Whether to hide the text being edited (e.g., for passwords).
|
||||||
|
///
|
||||||
|
/// When this is set to true, all the characters in the input are replaced by
|
||||||
|
/// U+2022 BULLET characters (•).
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool obscureText;
|
||||||
|
|
||||||
|
/// The maximum number of lines for the text to span, wrapping if necessary.
|
||||||
|
///
|
||||||
|
/// If this is 1 (the default), the text will not wrap, but will scroll
|
||||||
|
/// horizontally instead.
|
||||||
|
final int maxLines;
|
||||||
|
|
||||||
|
/// Called when the text being edited changes.
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
/// Called when the user indicates that they are done editing the text in the
|
||||||
|
/// field.
|
||||||
|
final ValueChanged<String> onSubmitted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_TextFieldState createState() => new _TextFieldState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillDescription(List<String> description) {
|
||||||
|
super.debugFillDescription(description);
|
||||||
|
if (controller != null)
|
||||||
|
description.add('controller: $controller');
|
||||||
|
if (focusNode != null)
|
||||||
|
description.add('focusNode: $focusNode');
|
||||||
|
description.add('decoration: $decoration');
|
||||||
|
if (keyboardType != TextInputType.text)
|
||||||
|
description.add('keyboardType: $keyboardType');
|
||||||
|
if (style != null)
|
||||||
|
description.add('style: $style');
|
||||||
|
if (autofocus)
|
||||||
|
description.add('autofocus: $autofocus');
|
||||||
|
if (obscureText)
|
||||||
|
description.add('obscureText: $obscureText');
|
||||||
|
if (maxLines != 1)
|
||||||
|
description.add('maxLines: $maxLines');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TextFieldState extends State<TextField> {
|
||||||
|
final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();
|
||||||
|
|
||||||
|
TextEditingController _controller;
|
||||||
|
TextEditingController get _effectiveController => config.controller ?? _controller;
|
||||||
|
|
||||||
|
FocusNode _focusNode;
|
||||||
|
FocusNode get _effectiveFocusNode => config.focusNode ?? (_focusNode ??= new FocusNode());
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (config.controller == null)
|
||||||
|
_controller = new TextEditingController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateConfig(TextField oldConfig) {
|
||||||
|
if (config.controller == null && oldConfig.controller != null)
|
||||||
|
_controller == new TextEditingController.fromValue(oldConfig.controller.value);
|
||||||
|
else if (config.controller != null && oldConfig.controller == null)
|
||||||
|
_controller = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requestKeyboard() {
|
||||||
|
_editableTextKey.currentState?.requestKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData themeData = Theme.of(context);
|
||||||
|
final TextStyle style = config.style ?? themeData.textTheme.subhead;
|
||||||
|
final TextEditingController controller = _effectiveController;
|
||||||
|
final FocusNode focusNode = _effectiveFocusNode;
|
||||||
|
|
||||||
|
Widget child = new RepaintBoundary(
|
||||||
|
child: new EditableText(
|
||||||
|
key: _editableTextKey,
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
keyboardType: config.keyboardType,
|
||||||
|
style: style,
|
||||||
|
autofocus: config.autofocus,
|
||||||
|
obscureText: config.obscureText,
|
||||||
|
maxLines: config.maxLines,
|
||||||
|
cursorColor: themeData.textSelectionColor,
|
||||||
|
selectionColor: themeData.textSelectionColor,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
onChanged: config.onChanged,
|
||||||
|
onSubmitted: config.onSubmitted,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (config.decoration != null) {
|
||||||
|
child = new AnimatedBuilder(
|
||||||
|
animation: new Listenable.merge(<Listenable>[ focusNode, controller ]),
|
||||||
|
builder: (BuildContext context, Widget child) {
|
||||||
|
return new InputDecorator(
|
||||||
|
decoration: config.decoration,
|
||||||
|
baseStyle: config.style,
|
||||||
|
isFocused: focusNode.hasFocus,
|
||||||
|
isEmpty: controller.value.text.isEmpty,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: _requestKeyboard,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
62
packages/flutter/lib/src/material/text_form_field.dart
Normal file
62
packages/flutter/lib/src/material/text_form_field.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'input_decorator.dart';
|
||||||
|
import 'text_field.dart';
|
||||||
|
|
||||||
|
/// A [FormField] that contains a [TextField].
|
||||||
|
///
|
||||||
|
/// This is a convenience widget that simply wraps a [TextField] widget in a
|
||||||
|
/// [FormField].
|
||||||
|
///
|
||||||
|
/// A [Form] ancestor is not required. The [Form] simply makes it easier to
|
||||||
|
/// save, reset, or validate multiple fields at once. To use without a [Form],
|
||||||
|
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
|
||||||
|
/// save or reset the form field.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * <https://material.google.com/components/text-fields.html>
|
||||||
|
/// * [TextField], which is the underlying text field without the [Form]
|
||||||
|
/// integration.
|
||||||
|
/// * [InputDecorator], which shows the labels and other visual elements that
|
||||||
|
/// surround the actual text editing widget.
|
||||||
|
class TextFormField extends FormField<String> {
|
||||||
|
TextFormField({
|
||||||
|
Key key,
|
||||||
|
TextEditingController controller,
|
||||||
|
FocusNode focusNode,
|
||||||
|
InputDecoration decoration: const InputDecoration(),
|
||||||
|
TextInputType keyboardType: TextInputType.text,
|
||||||
|
TextStyle style,
|
||||||
|
bool autofocus: false,
|
||||||
|
bool obscureText: false,
|
||||||
|
int maxLines: 1,
|
||||||
|
FormFieldSetter<String> onSaved,
|
||||||
|
FormFieldValidator<String> validator,
|
||||||
|
}) : super(
|
||||||
|
key: key,
|
||||||
|
initialValue: controller != null ? controller.value.text : '',
|
||||||
|
onSaved: onSaved,
|
||||||
|
validator: validator,
|
||||||
|
builder: (FormFieldState<String> field) {
|
||||||
|
return new TextField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: decoration.copyWith(errorText: field.errorText),
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
style: style,
|
||||||
|
autofocus: autofocus,
|
||||||
|
obscureText: obscureText,
|
||||||
|
maxLines: maxLines,
|
||||||
|
onChanged: (String value) {
|
||||||
|
field.onChanged(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
@ -20,7 +20,7 @@ class _TextSelectionToolbar extends StatelessWidget {
|
|||||||
_TextSelectionToolbar(this.delegate, {Key key}) : super(key: key);
|
_TextSelectionToolbar(this.delegate, {Key key}) : super(key: key);
|
||||||
|
|
||||||
final TextSelectionDelegate delegate;
|
final TextSelectionDelegate delegate;
|
||||||
InputValue get value => delegate.inputValue;
|
TextEditingValue get value => delegate.textEditingValue;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -51,7 +51,7 @@ class _TextSelectionToolbar extends StatelessWidget {
|
|||||||
|
|
||||||
void _handleCut() {
|
void _handleCut() {
|
||||||
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
|
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
|
||||||
delegate.inputValue = new InputValue(
|
delegate.textEditingValue = new TextEditingValue(
|
||||||
text: value.selection.textBefore(value.text) + value.selection.textAfter(value.text),
|
text: value.selection.textBefore(value.text) + value.selection.textAfter(value.text),
|
||||||
selection: new TextSelection.collapsed(offset: value.selection.start)
|
selection: new TextSelection.collapsed(offset: value.selection.start)
|
||||||
);
|
);
|
||||||
@ -60,7 +60,7 @@ class _TextSelectionToolbar extends StatelessWidget {
|
|||||||
|
|
||||||
void _handleCopy() {
|
void _handleCopy() {
|
||||||
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
|
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
|
||||||
delegate.inputValue = new InputValue(
|
delegate.textEditingValue = new TextEditingValue(
|
||||||
text: value.text,
|
text: value.text,
|
||||||
selection: new TextSelection.collapsed(offset: value.selection.end)
|
selection: new TextSelection.collapsed(offset: value.selection.end)
|
||||||
);
|
);
|
||||||
@ -68,10 +68,10 @@ class _TextSelectionToolbar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Null> _handlePaste() async {
|
Future<Null> _handlePaste() async {
|
||||||
final InputValue value = this.value; // Snapshot the input before using `await`.
|
final TextEditingValue value = this.value; // Snapshot the input before using `await`.
|
||||||
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
|
final ClipboardData data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
delegate.inputValue = new InputValue(
|
delegate.textEditingValue = new TextEditingValue(
|
||||||
text: value.selection.textBefore(value.text) + data.text + value.selection.textAfter(value.text),
|
text: value.selection.textBefore(value.text) + data.text + value.selection.textAfter(value.text),
|
||||||
selection: new TextSelection.collapsed(offset: value.selection.start + data.text.length)
|
selection: new TextSelection.collapsed(offset: value.selection.start + data.text.length)
|
||||||
);
|
);
|
||||||
@ -80,7 +80,7 @@ class _TextSelectionToolbar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleSelectAll() {
|
void _handleSelectAll() {
|
||||||
delegate.inputValue = new InputValue(
|
delegate.textEditingValue = new TextEditingValue(
|
||||||
text: value.text,
|
text: value.text,
|
||||||
selection: new TextSelection(baseOffset: 0, extentOffset: value.text.length)
|
selection: new TextSelection(baseOffset: 0, extentOffset: value.text.length)
|
||||||
);
|
);
|
||||||
@ -196,4 +196,4 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final _MaterialTextSelectionControls materialTextSelectionControls = new _MaterialTextSelectionControls();
|
final TextSelectionControls materialTextSelectionControls = new _MaterialTextSelectionControls();
|
||||||
|
@ -44,7 +44,7 @@ class MethodCall {
|
|||||||
final dynamic arguments;
|
final dynamic arguments;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator== (dynamic other) {
|
bool operator == (dynamic other) {
|
||||||
if (identical(this, other))
|
if (identical(this, other))
|
||||||
return true;
|
return true;
|
||||||
if (runtimeType != other.runtimeType)
|
if (runtimeType != other.runtimeType)
|
||||||
|
@ -7,6 +7,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
import 'focus_manager.dart';
|
import 'focus_manager.dart';
|
||||||
@ -18,91 +19,48 @@ import 'scroll_physics.dart';
|
|||||||
import 'scrollable.dart';
|
import 'scrollable.dart';
|
||||||
import 'text_selection.dart';
|
import 'text_selection.dart';
|
||||||
|
|
||||||
export 'package:flutter/services.dart' show TextSelection, TextInputType;
|
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType;
|
||||||
|
|
||||||
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
|
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
|
||||||
|
|
||||||
InputValue _getInputValueFromEditingValue(TextEditingValue value) {
|
class TextEditingController extends ChangeNotifier {
|
||||||
return new InputValue(
|
TextEditingController({ String text })
|
||||||
text: value.text,
|
: _value = text == null ? TextEditingValue.empty : new TextEditingValue(text: text);
|
||||||
selection: value.selection,
|
|
||||||
composing: value.composing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEditingValue _getTextEditingValueFromInputValue(InputValue value) {
|
TextEditingController.fromValue(TextEditingValue value)
|
||||||
return new TextEditingValue(
|
: _value = value ?? TextEditingValue.empty;
|
||||||
text: value.text,
|
|
||||||
selection: value.selection,
|
|
||||||
composing: value.composing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration information for a text input field.
|
TextEditingValue get value => _value;
|
||||||
///
|
TextEditingValue _value;
|
||||||
/// An [InputValue] contains the text for the input field as well as the
|
set value(TextEditingValue newValue) {
|
||||||
/// selection extent and the composing range.
|
assert(newValue != null);
|
||||||
class InputValue {
|
if (_value == newValue)
|
||||||
// TODO(abarth): This class is really the same as TextEditingState.
|
return;
|
||||||
// We should merge them into one object.
|
_value = newValue;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates configuration information for an input field
|
String get text => _value.text;
|
||||||
///
|
set text(String newText) {
|
||||||
/// The selection and composing range must be within the text.
|
value = value.copyWith(text: newText, composing: TextRange.empty);
|
||||||
///
|
}
|
||||||
/// The [text], [selection], and [composing] arguments must not be null but
|
|
||||||
/// each have default values.
|
|
||||||
const InputValue({
|
|
||||||
this.text: '',
|
|
||||||
this.selection: const TextSelection.collapsed(offset: -1),
|
|
||||||
this.composing: TextRange.empty
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The current text being edited.
|
TextSelection get selection => _value.selection;
|
||||||
final String text;
|
set selection(TextSelection newSelection) {
|
||||||
|
value = value.copyWith(selection: newSelection, composing: TextRange.empty);
|
||||||
|
}
|
||||||
|
|
||||||
/// The range of text that is currently selected.
|
void clear() {
|
||||||
final TextSelection selection;
|
value = TextEditingValue.empty;
|
||||||
|
}
|
||||||
|
|
||||||
/// The range of text that is still being composed.
|
void clearComposing() {
|
||||||
final TextRange composing;
|
value = value.copyWith(composing: TextRange.empty);
|
||||||
|
|
||||||
/// An input value that corresponds to the empty string with no selection and no composing range.
|
|
||||||
static const InputValue empty = const InputValue();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => '$runtimeType(text: \u2524$text\u251C, selection: $selection, composing: $composing)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(dynamic other) {
|
|
||||||
if (identical(this, other))
|
|
||||||
return true;
|
|
||||||
if (other is! InputValue)
|
|
||||||
return false;
|
|
||||||
final InputValue typedOther = other;
|
|
||||||
return typedOther.text == text
|
|
||||||
&& typedOther.selection == selection
|
|
||||||
&& typedOther.composing == composing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => hashValues(
|
String toString() {
|
||||||
text.hashCode,
|
return '$runtimeType#$hashCode($value)';
|
||||||
selection.hashCode,
|
|
||||||
composing.hashCode
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Creates a copy of this input value but with the given fields replaced with the new values.
|
|
||||||
InputValue copyWith({
|
|
||||||
String text,
|
|
||||||
TextSelection selection,
|
|
||||||
TextRange composing
|
|
||||||
}) {
|
|
||||||
return new InputValue (
|
|
||||||
text: text ?? this.text,
|
|
||||||
selection: selection ?? this.selection,
|
|
||||||
composing: composing ?? this.composing
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,11 +84,11 @@ class InputValue {
|
|||||||
class EditableText extends StatefulWidget {
|
class EditableText extends StatefulWidget {
|
||||||
/// Creates a basic text input control.
|
/// Creates a basic text input control.
|
||||||
///
|
///
|
||||||
/// The [value], [focusNode], [style], and [cursorColor] arguments must not
|
/// The [controller], [focusNode], [style], and [cursorColor] arguments must
|
||||||
/// be null.
|
/// not be null.
|
||||||
EditableText({
|
EditableText({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.value,
|
@required this.controller,
|
||||||
@required this.focusNode,
|
@required this.focusNode,
|
||||||
this.obscureText: false,
|
this.obscureText: false,
|
||||||
@required this.style,
|
@required this.style,
|
||||||
@ -144,7 +102,7 @@ class EditableText extends StatefulWidget {
|
|||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.onSubmitted,
|
this.onSubmitted,
|
||||||
}) : super(key: key) {
|
}) : super(key: key) {
|
||||||
assert(value != null);
|
assert(controller != null);
|
||||||
assert(focusNode != null);
|
assert(focusNode != null);
|
||||||
assert(obscureText != null);
|
assert(obscureText != null);
|
||||||
assert(style != null);
|
assert(style != null);
|
||||||
@ -153,8 +111,8 @@ class EditableText extends StatefulWidget {
|
|||||||
assert(autofocus != null);
|
assert(autofocus != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The string being displayed in this widget.
|
/// Controls the text being edited.
|
||||||
final InputValue value;
|
final TextEditingController controller;
|
||||||
|
|
||||||
/// Controls whether this widget has keyboard focus.
|
/// Controls whether this widget has keyboard focus.
|
||||||
final FocusNode focusNode;
|
final FocusNode focusNode;
|
||||||
@ -200,10 +158,10 @@ class EditableText extends StatefulWidget {
|
|||||||
final TextInputType keyboardType;
|
final TextInputType keyboardType;
|
||||||
|
|
||||||
/// Called when the text being edited changes.
|
/// Called when the text being edited changes.
|
||||||
final ValueChanged<InputValue> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
/// Called when the user indicates that they are done editing the text in the field.
|
/// Called when the user indicates that they are done editing the text in the field.
|
||||||
final ValueChanged<InputValue> onSubmitted;
|
final ValueChanged<String> onSubmitted;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EditableTextState createState() => new EditableTextState();
|
EditableTextState createState() => new EditableTextState();
|
||||||
@ -214,17 +172,18 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
|||||||
Timer _cursorTimer;
|
Timer _cursorTimer;
|
||||||
bool _showCursor = false;
|
bool _showCursor = false;
|
||||||
|
|
||||||
InputValue _currentValue;
|
|
||||||
TextInputConnection _textInputConnection;
|
TextInputConnection _textInputConnection;
|
||||||
TextSelectionOverlay _selectionOverlay;
|
TextSelectionOverlay _selectionOverlay;
|
||||||
|
|
||||||
final ScrollController _scrollController = new ScrollController();
|
final ScrollController _scrollController = new ScrollController();
|
||||||
bool _didAutoFocus = false;
|
bool _didAutoFocus = false;
|
||||||
|
|
||||||
|
// State lifecycle:
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_currentValue = config.value;
|
config.controller.addListener(_didChangeTextEditingValue);
|
||||||
config.focusNode.addListener(_handleFocusChanged);
|
config.focusNode.addListener(_handleFocusChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,17 +199,63 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateConfig(EditableText oldConfig) {
|
void didUpdateConfig(EditableText oldConfig) {
|
||||||
if (_currentValue != config.value) {
|
if (config.controller != oldConfig.controller) {
|
||||||
_currentValue = config.value;
|
oldConfig.controller.removeListener(_didChangeTextEditingValue);
|
||||||
if (_isAttachedToKeyboard)
|
config.controller.addListener(_didChangeTextEditingValue);
|
||||||
_textInputConnection.setEditingState(_getTextEditingValueFromInputValue(_currentValue));
|
if (_isAttachedToKeyboard && config.controller.value != oldConfig.controller.value)
|
||||||
}
|
_textInputConnection.setEditingState(config.controller.value);
|
||||||
|
}
|
||||||
if (config.focusNode != oldConfig.focusNode) {
|
if (config.focusNode != oldConfig.focusNode) {
|
||||||
oldConfig.focusNode.removeListener(_handleFocusChanged);
|
oldConfig.focusNode.removeListener(_handleFocusChanged);
|
||||||
config.focusNode.addListener(_handleFocusChanged);
|
config.focusNode.addListener(_handleFocusChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
config.controller.removeListener(_didChangeTextEditingValue);
|
||||||
|
if (_isAttachedToKeyboard) {
|
||||||
|
_textInputConnection.close();
|
||||||
|
_textInputConnection = null;
|
||||||
|
}
|
||||||
|
assert(!_isAttachedToKeyboard);
|
||||||
|
if (_cursorTimer != null)
|
||||||
|
_stopCursorTimer();
|
||||||
|
assert(_cursorTimer == null);
|
||||||
|
_selectionOverlay?.dispose();
|
||||||
|
_selectionOverlay = null;
|
||||||
|
config.focusNode.removeListener(_handleFocusChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextInputClient implementation:
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateEditingValue(TextEditingValue value) {
|
||||||
|
if (value.text != _value.text)
|
||||||
|
_hideSelectionOverlayIfNeeded();
|
||||||
|
_value = value;
|
||||||
|
if (config.onChanged != null)
|
||||||
|
config.onChanged(value.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performAction(TextInputAction action) {
|
||||||
|
config.controller.clearComposing();
|
||||||
|
config.focusNode.unfocus();
|
||||||
|
if (config.onSubmitted != null)
|
||||||
|
config.onSubmitted(_value.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEditingValue get _value => config.controller.value;
|
||||||
|
set _value(TextEditingValue value) {
|
||||||
|
config.controller.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _didChangeTextEditingValue() {
|
||||||
|
setState(() { /* We use config.controller.value in build(). */ });
|
||||||
|
}
|
||||||
|
|
||||||
bool get _isAttachedToKeyboard => _textInputConnection != null && _textInputConnection.attached;
|
bool get _isAttachedToKeyboard => _textInputConnection != null && _textInputConnection.attached;
|
||||||
|
|
||||||
bool get _isMultiline => config.maxLines > 1;
|
bool get _isMultiline => config.maxLines > 1;
|
||||||
@ -273,24 +278,18 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
|||||||
void _attachOrDetachKeyboard(bool focused) {
|
void _attachOrDetachKeyboard(bool focused) {
|
||||||
if (focused && !_isAttachedToKeyboard && _didRequestKeyboard) {
|
if (focused && !_isAttachedToKeyboard && _didRequestKeyboard) {
|
||||||
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType))
|
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType))
|
||||||
..setEditingState(_getTextEditingValueFromInputValue(_currentValue))
|
..setEditingState(_value)
|
||||||
..show();
|
..show();
|
||||||
} else if (!focused) {
|
} else if (!focused) {
|
||||||
if (_isAttachedToKeyboard) {
|
if (_isAttachedToKeyboard) {
|
||||||
_textInputConnection.close();
|
_textInputConnection.close();
|
||||||
_textInputConnection = null;
|
_textInputConnection = null;
|
||||||
}
|
}
|
||||||
_clearComposing();
|
config.controller.clearComposing();
|
||||||
}
|
}
|
||||||
_didRequestKeyboard = false;
|
_didRequestKeyboard = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearComposing() {
|
|
||||||
// TODO(abarth): We should call config.onChanged to notify our parent of
|
|
||||||
// this change in our composing range.
|
|
||||||
_currentValue = _currentValue.copyWith(composing: TextRange.empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Express interest in interacting with the keyboard.
|
/// Express interest in interacting with the keyboard.
|
||||||
///
|
///
|
||||||
/// If this control is already attached to the keyboard, this function will
|
/// If this control is already attached to the keyboard, this function will
|
||||||
@ -310,23 +309,9 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _hideSelectionOverlayIfNeeded() {
|
||||||
void updateEditingValue(TextEditingValue value) {
|
_selectionOverlay?.hide();
|
||||||
_currentValue = _getInputValueFromEditingValue(value);
|
_selectionOverlay = null;
|
||||||
if (config.onChanged != null)
|
|
||||||
config.onChanged(_currentValue);
|
|
||||||
if (_currentValue.text != config.value.text) {
|
|
||||||
_selectionOverlay?.hide();
|
|
||||||
_selectionOverlay = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void performAction(TextInputAction action) {
|
|
||||||
_clearComposing();
|
|
||||||
config.focusNode.unfocus();
|
|
||||||
if (config.onSubmitted != null)
|
|
||||||
config.onSubmitted(_currentValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) {
|
void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) {
|
||||||
@ -334,45 +319,40 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
|||||||
// EditableWidget, not just changes triggered by user gestures.
|
// EditableWidget, not just changes triggered by user gestures.
|
||||||
requestKeyboard();
|
requestKeyboard();
|
||||||
|
|
||||||
final InputValue newInput = _currentValue.copyWith(selection: selection, composing: TextRange.empty);
|
_hideSelectionOverlayIfNeeded();
|
||||||
if (config.onChanged != null)
|
config.controller.selection = selection;
|
||||||
config.onChanged(newInput);
|
|
||||||
|
|
||||||
if (_selectionOverlay != null) {
|
|
||||||
_selectionOverlay.hide();
|
|
||||||
_selectionOverlay = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.selectionControls != null) {
|
if (config.selectionControls != null) {
|
||||||
_selectionOverlay = new TextSelectionOverlay(
|
_selectionOverlay = new TextSelectionOverlay(
|
||||||
input: newInput,
|
|
||||||
context: context,
|
context: context,
|
||||||
|
value: _value,
|
||||||
debugRequiredFor: config,
|
debugRequiredFor: config,
|
||||||
renderObject: renderObject,
|
renderObject: renderObject,
|
||||||
onSelectionOverlayChanged: _handleSelectionOverlayChanged,
|
onSelectionOverlayChanged: _handleSelectionOverlayChanged,
|
||||||
selectionControls: config.selectionControls,
|
selectionControls: config.selectionControls,
|
||||||
);
|
);
|
||||||
if (newInput.text.isNotEmpty || longPress)
|
if (_value.text.isNotEmpty || longPress)
|
||||||
_selectionOverlay.showHandles();
|
_selectionOverlay.showHandles();
|
||||||
if (longPress)
|
if (longPress)
|
||||||
_selectionOverlay.showToolbar();
|
_selectionOverlay.showToolbar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSelectionOverlayChanged(InputValue newInput, Rect caretRect) {
|
void _handleSelectionOverlayChanged(TextEditingValue value, Rect caretRect) {
|
||||||
assert(!newInput.composing.isValid); // composing range must be empty while selecting
|
assert(!value.composing.isValid); // composing range must be empty while selecting.
|
||||||
if (config.onChanged != null)
|
_value = value;
|
||||||
config.onChanged(newInput);
|
|
||||||
_scrollController.jumpTo(_getScrollOffsetForCaret(caretRect));
|
_scrollController.jumpTo(_getScrollOffsetForCaret(caretRect));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the blinking cursor is actually visible at this precise moment
|
/// Whether the blinking cursor is actually visible at this precise moment
|
||||||
/// (it's hidden half the time, since it blinks).
|
/// (it's hidden half the time, since it blinks).
|
||||||
|
@visibleForTesting
|
||||||
bool get cursorCurrentlyVisible => _showCursor;
|
bool get cursorCurrentlyVisible => _showCursor;
|
||||||
|
|
||||||
/// The cursor blink interval (the amount of time the cursor is in the "on"
|
/// The cursor blink interval (the amount of time the cursor is in the "on"
|
||||||
/// state or the "off" state). A complete cursor blink period is twice this
|
/// state or the "off" state). A complete cursor blink period is twice this
|
||||||
/// value (half on, half off).
|
/// value (half on, half off).
|
||||||
|
@visibleForTesting
|
||||||
Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
|
Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
|
||||||
|
|
||||||
void _cursorTick(Timer timer) {
|
void _cursorTick(Timer timer) {
|
||||||
@ -390,37 +370,21 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
|||||||
final bool focused = config.focusNode.hasFocus;
|
final bool focused = config.focusNode.hasFocus;
|
||||||
_attachOrDetachKeyboard(focused);
|
_attachOrDetachKeyboard(focused);
|
||||||
|
|
||||||
if (_cursorTimer == null && focused && config.value.selection.isCollapsed)
|
if (_cursorTimer == null && focused && _value.selection.isCollapsed)
|
||||||
_startCursorTimer();
|
_startCursorTimer();
|
||||||
else if (_cursorTimer != null && (!focused || !config.value.selection.isCollapsed))
|
else if (_cursorTimer != null && (!focused || !_value.selection.isCollapsed))
|
||||||
_stopCursorTimer();
|
_stopCursorTimer();
|
||||||
|
|
||||||
if (_selectionOverlay != null) {
|
if (_selectionOverlay != null) {
|
||||||
if (focused) {
|
if (focused) {
|
||||||
_selectionOverlay.update(config.value);
|
_selectionOverlay.update(_value);
|
||||||
} else {
|
} else {
|
||||||
_selectionOverlay?.dispose();
|
_selectionOverlay.dispose();
|
||||||
_selectionOverlay = null;
|
_selectionOverlay = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
if (_isAttachedToKeyboard) {
|
|
||||||
_textInputConnection.close();
|
|
||||||
_textInputConnection = null;
|
|
||||||
}
|
|
||||||
assert(!_isAttachedToKeyboard);
|
|
||||||
if (_cursorTimer != null)
|
|
||||||
_stopCursorTimer();
|
|
||||||
assert(_cursorTimer == null);
|
|
||||||
_selectionOverlay?.dispose();
|
|
||||||
_selectionOverlay = null;
|
|
||||||
config.focusNode.removeListener(_handleFocusChanged);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _stopCursorTimer() {
|
void _stopCursorTimer() {
|
||||||
_cursorTimer.cancel();
|
_cursorTimer.cancel();
|
||||||
_cursorTimer = null;
|
_cursorTimer = null;
|
||||||
@ -436,7 +400,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
|||||||
physics: const ClampingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||||
return new _Editable(
|
return new _Editable(
|
||||||
value: _currentValue,
|
value: _value,
|
||||||
style: config.style,
|
style: config.style,
|
||||||
cursorColor: config.cursorColor,
|
cursorColor: config.cursorColor,
|
||||||
showCursor: _showCursor,
|
showCursor: _showCursor,
|
||||||
@ -467,7 +431,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
this.onSelectionChanged,
|
this.onSelectionChanged,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final InputValue value;
|
final TextEditingValue value;
|
||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
final Color cursorColor;
|
final Color cursorColor;
|
||||||
final bool showCursor;
|
final bool showCursor;
|
||||||
|
@ -43,22 +43,22 @@ enum TextSelectionHandleType {
|
|||||||
/// [start] handle always moves the [start]/[baseOffset] of the selection.
|
/// [start] handle always moves the [start]/[baseOffset] of the selection.
|
||||||
enum _TextSelectionHandlePosition { start, end }
|
enum _TextSelectionHandlePosition { start, end }
|
||||||
|
|
||||||
/// Signature for reporting changes to the selection component of an
|
/// Signature for reporting changes to the selection component of a
|
||||||
/// [InputValue] for the purposes of a [TextSelectionOverlay]. The [caretRect]
|
/// [TextEditingValue] for the purposes of a [TextSelectionOverlay]. The
|
||||||
/// argument gives the location of the caret in the coordinate space of the
|
/// [caretRect] argument gives the location of the caret in the coordinate space
|
||||||
/// [RenderBox] given by the [TextSelectionOverlay.renderObject].
|
/// of the [RenderBox] given by the [TextSelectionOverlay.renderObject].
|
||||||
///
|
///
|
||||||
/// Used by [TextSelectionOverlay.onSelectionOverlayChanged].
|
/// Used by [TextSelectionOverlay.onSelectionOverlayChanged].
|
||||||
typedef void TextSelectionOverlayChanged(InputValue value, Rect caretRect);
|
typedef void TextSelectionOverlayChanged(TextEditingValue value, Rect caretRect);
|
||||||
|
|
||||||
/// An interface for manipulating the selection, to be used by the implementor
|
/// An interface for manipulating the selection, to be used by the implementor
|
||||||
/// of the toolbar widget.
|
/// of the toolbar widget.
|
||||||
abstract class TextSelectionDelegate {
|
abstract class TextSelectionDelegate {
|
||||||
/// Gets the current text input.
|
/// Gets the current text input.
|
||||||
InputValue get inputValue;
|
TextEditingValue get textEditingValue;
|
||||||
|
|
||||||
/// Sets the current text input (replaces the whole line).
|
/// Sets the current text input (replaces the whole line).
|
||||||
set inputValue(InputValue value);
|
set textEditingValue(TextEditingValue value);
|
||||||
|
|
||||||
/// Hides the text selection toolbar.
|
/// Hides the text selection toolbar.
|
||||||
void hideToolbar();
|
void hideToolbar();
|
||||||
@ -89,13 +89,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
|||||||
///
|
///
|
||||||
/// The [context] must not be null and must have an [Overlay] as an ancestor.
|
/// The [context] must not be null and must have an [Overlay] as an ancestor.
|
||||||
TextSelectionOverlay({
|
TextSelectionOverlay({
|
||||||
InputValue input,
|
@required TextEditingValue value,
|
||||||
@required this.context,
|
@required this.context,
|
||||||
this.debugRequiredFor,
|
this.debugRequiredFor,
|
||||||
this.renderObject,
|
this.renderObject,
|
||||||
this.onSelectionOverlayChanged,
|
this.onSelectionOverlayChanged,
|
||||||
this.selectionControls,
|
this.selectionControls,
|
||||||
}): _input = input {
|
}): _value = value {
|
||||||
|
assert(value != null);
|
||||||
assert(context != null);
|
assert(context != null);
|
||||||
final OverlayState overlay = Overlay.of(context);
|
final OverlayState overlay = Overlay.of(context);
|
||||||
assert(overlay != null);
|
assert(overlay != null);
|
||||||
@ -133,7 +134,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
|||||||
Animation<double> get _handleOpacity => _handleController.view;
|
Animation<double> get _handleOpacity => _handleController.view;
|
||||||
Animation<double> get _toolbarOpacity => _toolbarController.view;
|
Animation<double> get _toolbarOpacity => _toolbarController.view;
|
||||||
|
|
||||||
InputValue _input;
|
TextEditingValue _value;
|
||||||
|
|
||||||
/// A pair of handles. If this is non-null, there are always 2, though the
|
/// A pair of handles. If this is non-null, there are always 2, though the
|
||||||
/// second is hidden when the selection is collapsed.
|
/// second is hidden when the selection is collapsed.
|
||||||
@ -142,7 +143,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
|||||||
/// A copy/paste toolbar.
|
/// A copy/paste toolbar.
|
||||||
OverlayEntry _toolbar;
|
OverlayEntry _toolbar;
|
||||||
|
|
||||||
TextSelection get _selection => _input.selection;
|
TextSelection get _selection => _value.selection;
|
||||||
|
|
||||||
/// Shows the handles by inserting them into the [context]'s overlay.
|
/// Shows the handles by inserting them into the [context]'s overlay.
|
||||||
void showHandles() {
|
void showHandles() {
|
||||||
@ -172,10 +173,10 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
|||||||
/// synchronously. This means that it is safe to call during builds, but also
|
/// synchronously. This means that it is safe to call during builds, but also
|
||||||
/// that if you do call this during a build, the UI will not update until the
|
/// that if you do call this during a build, the UI will not update until the
|
||||||
/// next frame (i.e. many milliseconds later).
|
/// next frame (i.e. many milliseconds later).
|
||||||
void update(InputValue newInput) {
|
void update(TextEditingValue newValue) {
|
||||||
if (_input == newInput)
|
if (_value == newValue)
|
||||||
return;
|
return;
|
||||||
_input = newInput;
|
_value = newValue;
|
||||||
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
|
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
|
||||||
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
|
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
|
||||||
} else {
|
} else {
|
||||||
@ -259,13 +260,13 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
|||||||
caretRect = renderObject.getLocalRectForCaret(newSelection.extent);
|
caretRect = renderObject.getLocalRectForCaret(newSelection.extent);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
update(_input.copyWith(selection: newSelection, composing: TextRange.empty));
|
update(_value.copyWith(selection: newSelection, composing: TextRange.empty));
|
||||||
if (onSelectionOverlayChanged != null)
|
if (onSelectionOverlayChanged != null)
|
||||||
onSelectionOverlayChanged(_input, caretRect);
|
onSelectionOverlayChanged(_value, caretRect);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSelectionHandleTapped() {
|
void _handleSelectionHandleTapped() {
|
||||||
if (inputValue.selection.isCollapsed) {
|
if (_value.selection.isCollapsed) {
|
||||||
if (_toolbar != null) {
|
if (_toolbar != null) {
|
||||||
_toolbar?.remove();
|
_toolbar?.remove();
|
||||||
_toolbar = null;
|
_toolbar = null;
|
||||||
@ -276,14 +277,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InputValue get inputValue => _input;
|
TextEditingValue get textEditingValue => _value;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
set inputValue(InputValue value) {
|
set textEditingValue(TextEditingValue newValue) {
|
||||||
update(value);
|
update(newValue);
|
||||||
if (onSelectionOverlayChanged != null) {
|
if (onSelectionOverlayChanged != null) {
|
||||||
final Rect caretRect = renderObject.getLocalRectForCaret(value.selection.extent);
|
final Rect caretRect = renderObject.getLocalRectForCaret(newValue.selection.extent);
|
||||||
onSelectionOverlayChanged(value, caretRect);
|
onSelectionOverlayChanged(newValue, caretRect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,9 +43,9 @@ void main() {
|
|||||||
await tester.pumpWidget(new MaterialApp(
|
await tester.pumpWidget(new MaterialApp(
|
||||||
home: new Material(
|
home: new Material(
|
||||||
child: new Center(
|
child: new Center(
|
||||||
child: new Input(focusNode: focusNode, autofocus: true)
|
child: new TextField(focusNode: focusNode, autofocus: true),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(focusNode.hasFocus, isTrue);
|
expect(focusNode.hasFocus, isTrue);
|
||||||
|
@ -12,7 +12,7 @@ import 'package:flutter/services.dart';
|
|||||||
|
|
||||||
class MockClipboard {
|
class MockClipboard {
|
||||||
Object _clipboardData = <String, dynamic>{
|
Object _clipboardData = <String, dynamic>{
|
||||||
'text': null
|
'text': null,
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
|
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
|
||||||
@ -30,9 +30,9 @@ Widget overlay(Widget child) {
|
|||||||
return new Overlay(
|
return new Overlay(
|
||||||
initialEntries: <OverlayEntry>[
|
initialEntries: <OverlayEntry>[
|
||||||
new OverlayEntry(
|
new OverlayEntry(
|
||||||
builder: (BuildContext context) => child
|
builder: (BuildContext context) => child,
|
||||||
)
|
),
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,28 +74,31 @@ void main() {
|
|||||||
return endpoints[0].point + const Offset(0.0, -2.0);
|
return endpoints[0].point + const Offset(0.0, -2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('Editable text has consistent size', (WidgetTester tester) async {
|
testWidgets('TextField has consistent size', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
final Key textFieldKey = new UniqueKey();
|
||||||
InputValue inputValue = InputValue.empty;
|
String textFieldValue;
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Input(
|
child: new TextField(
|
||||||
value: inputValue,
|
key: textFieldKey,
|
||||||
key: inputKey,
|
decoration: new InputDecoration(
|
||||||
hintText: 'Placeholder',
|
hintText: 'Placeholder',
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
),
|
||||||
)
|
onChanged: (String value) {
|
||||||
)
|
textFieldValue = value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));
|
RenderBox findTextFieldBox() => tester.renderObject(find.byKey(textFieldKey));
|
||||||
|
|
||||||
final RenderBox inputBox = findInputBox();
|
final RenderBox inputBox = findTextFieldBox();
|
||||||
final Size emptyInputSize = inputBox.size;
|
final Size emptyInputSize = inputBox.size;
|
||||||
|
|
||||||
Future<Null> checkText(String testValue) async {
|
Future<Null> checkText(String testValue) async {
|
||||||
@ -103,31 +106,31 @@ void main() {
|
|||||||
await tester.idle();
|
await tester.idle();
|
||||||
|
|
||||||
// Check that the onChanged event handler fired.
|
// Check that the onChanged event handler fired.
|
||||||
expect(inputValue.text, equals(testValue));
|
expect(textFieldValue, equals(testValue));
|
||||||
|
|
||||||
return await tester.pumpWidget(builder());
|
return await tester.pumpWidget(builder());
|
||||||
}
|
}
|
||||||
|
|
||||||
await checkText(' ');
|
await checkText(' ');
|
||||||
expect(findInputBox(), equals(inputBox));
|
expect(findTextFieldBox(), equals(inputBox));
|
||||||
expect(inputBox.size, equals(emptyInputSize));
|
expect(inputBox.size, equals(emptyInputSize));
|
||||||
|
|
||||||
await checkText('Test');
|
await checkText('Test');
|
||||||
expect(findInputBox(), equals(inputBox));
|
expect(findTextFieldBox(), equals(inputBox));
|
||||||
expect(inputBox.size, equals(emptyInputSize));
|
expect(inputBox.size, equals(emptyInputSize));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Cursor blinks', (WidgetTester tester) async {
|
testWidgets('Cursor blinks', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Input(
|
child: new TextField(
|
||||||
key: inputKey,
|
decoration: new InputDecoration(
|
||||||
hintText: 'Placeholder'
|
hintText: 'Placeholder',
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,17 +166,16 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('obscureText control test', (WidgetTester tester) async {
|
testWidgets('obscureText control test', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Input(
|
child: new TextField(
|
||||||
key: inputKey,
|
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
hintText: 'Placeholder'
|
decoration: new InputDecoration(
|
||||||
)
|
hintText: 'Placeholder',
|
||||||
)
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,8 +192,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Can long press to select', (WidgetTester tester) async {
|
testWidgets('Can long press to select', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
final TextEditingController controller = new TextEditingController();
|
||||||
InputValue inputValue = InputValue.empty;
|
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Overlay(
|
return new Overlay(
|
||||||
@ -200,16 +201,14 @@ void main() {
|
|||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Input(
|
child: new TextField(
|
||||||
value: inputValue,
|
controller: controller,
|
||||||
key: inputKey,
|
),
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
),
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
]
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,11 +217,11 @@ void main() {
|
|||||||
final String testValue = 'abc def ghi';
|
final String testValue = 'abc def ghi';
|
||||||
await tester.enterText(find.byType(EditableText), testValue);
|
await tester.enterText(find.byType(EditableText), testValue);
|
||||||
await tester.idle();
|
await tester.idle();
|
||||||
expect(inputValue.text, testValue);
|
expect(controller.value.text, testValue);
|
||||||
|
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
expect(inputValue.selection.isCollapsed, true);
|
expect(controller.selection.isCollapsed, true);
|
||||||
|
|
||||||
// Long press the 'e' to select 'def'.
|
// Long press the 'e' to select 'def'.
|
||||||
final Point ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
final Point ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||||||
@ -232,32 +231,21 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// 'def' is selected.
|
// 'def' is selected.
|
||||||
expect(inputValue.selection.baseOffset, testValue.indexOf('d'));
|
expect(controller.selection.baseOffset, testValue.indexOf('d'));
|
||||||
expect(inputValue.selection.extentOffset, testValue.indexOf('f')+1);
|
expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
|
testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
final TextEditingController controller = new TextEditingController();
|
||||||
InputValue inputValue = InputValue.empty;
|
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Overlay(
|
return overlay(new Center(
|
||||||
initialEntries: <OverlayEntry>[
|
child: new Material(
|
||||||
new OverlayEntry(
|
child: new TextField(
|
||||||
builder: (BuildContext context) {
|
controller: controller,
|
||||||
return new Center(
|
),
|
||||||
child: new Material(
|
),
|
||||||
child: new Input(
|
));
|
||||||
value: inputValue,
|
|
||||||
key: inputKey,
|
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
@ -275,7 +263,7 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
final TextSelection selection = inputValue.selection;
|
final TextSelection selection = controller.selection;
|
||||||
|
|
||||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||||
@ -294,8 +282,8 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
expect(inputValue.selection.baseOffset, selection.baseOffset);
|
expect(controller.selection.baseOffset, selection.baseOffset);
|
||||||
expect(inputValue.selection.extentOffset, selection.extentOffset+2);
|
expect(controller.selection.extentOffset, selection.extentOffset+2);
|
||||||
|
|
||||||
// Drag the left handle 2 letters to the left.
|
// Drag the left handle 2 letters to the left.
|
||||||
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||||||
@ -307,32 +295,21 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
expect(inputValue.selection.baseOffset, selection.baseOffset-2);
|
expect(controller.selection.baseOffset, selection.baseOffset-2);
|
||||||
expect(inputValue.selection.extentOffset, selection.extentOffset+2);
|
expect(controller.selection.extentOffset, selection.extentOffset+2);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Can use selection toolbar', (WidgetTester tester) async {
|
testWidgets('Can use selection toolbar', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
final TextEditingController controller = new TextEditingController();
|
||||||
InputValue inputValue = InputValue.empty;
|
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Overlay(
|
return overlay(new Center(
|
||||||
initialEntries: <OverlayEntry>[
|
child: new Material(
|
||||||
new OverlayEntry(
|
child: new TextField(
|
||||||
builder: (BuildContext context) {
|
controller: controller,
|
||||||
return new Center(
|
),
|
||||||
child: new Material(
|
),
|
||||||
child: new Input(
|
));
|
||||||
value: inputValue,
|
|
||||||
key: inputKey,
|
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
@ -347,57 +324,46 @@ void main() {
|
|||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
RenderEditable renderEditable = findRenderEditable(tester);
|
RenderEditable renderEditable = findRenderEditable(tester);
|
||||||
List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||||
inputValue.selection);
|
controller.selection);
|
||||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
// SELECT ALL should select all the text.
|
// SELECT ALL should select all the text.
|
||||||
await tester.tap(find.text('SELECT ALL'));
|
await tester.tap(find.text('SELECT ALL'));
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
expect(inputValue.selection.baseOffset, 0);
|
expect(controller.selection.baseOffset, 0);
|
||||||
expect(inputValue.selection.extentOffset, testValue.length);
|
expect(controller.selection.extentOffset, testValue.length);
|
||||||
|
|
||||||
// COPY should reset the selection.
|
// COPY should reset the selection.
|
||||||
await tester.tap(find.text('COPY'));
|
await tester.tap(find.text('COPY'));
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
expect(inputValue.selection.isCollapsed, true);
|
expect(controller.selection.isCollapsed, true);
|
||||||
|
|
||||||
// Tap again to bring back the menu.
|
// Tap again to bring back the menu.
|
||||||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
renderEditable = findRenderEditable(tester);
|
renderEditable = findRenderEditable(tester);
|
||||||
endpoints = renderEditable.getEndpointsForSelection(inputValue.selection);
|
endpoints = renderEditable.getEndpointsForSelection(controller.selection);
|
||||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
// PASTE right before the 'e'.
|
// PASTE right before the 'e'.
|
||||||
await tester.tap(find.text('PASTE'));
|
await tester.tap(find.text('PASTE'));
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
expect(inputValue.text, 'abc d${testValue}ef ghi');
|
expect(controller.text, 'abc d${testValue}ef ghi');
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Selection toolbar fades in', (WidgetTester tester) async {
|
testWidgets('Selection toolbar fades in', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
final TextEditingController controller = new TextEditingController();
|
||||||
InputValue inputValue = InputValue.empty;
|
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Overlay(
|
return overlay(new Center(
|
||||||
initialEntries: <OverlayEntry>[
|
child: new Material(
|
||||||
new OverlayEntry(
|
child: new TextField(
|
||||||
builder: (BuildContext context) {
|
controller: controller,
|
||||||
return new Center(
|
),
|
||||||
child: new Material(
|
),
|
||||||
child: new Input(
|
));
|
||||||
value: inputValue,
|
|
||||||
key: inputKey,
|
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
@ -412,7 +378,7 @@ void main() {
|
|||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||||
inputValue.selection);
|
controller.selection);
|
||||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
@ -432,27 +398,26 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async {
|
testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
final Key textFieldKey = new UniqueKey();
|
||||||
InputValue inputValue = InputValue.empty;
|
|
||||||
|
|
||||||
Widget builder(int maxLines) {
|
Widget builder(int maxLines) {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Input(
|
child: new TextField(
|
||||||
value: inputValue,
|
key: textFieldKey,
|
||||||
key: inputKey,
|
|
||||||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
hintText: 'Placeholder',
|
decoration: new InputDecoration(
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
hintText: 'Placeholder',
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.pumpWidget(builder(3));
|
await tester.pumpWidget(builder(3));
|
||||||
|
|
||||||
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));
|
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||||||
|
|
||||||
final RenderBox inputBox = findInputBox();
|
final RenderBox inputBox = findInputBox();
|
||||||
final Size emptyInputSize = inputBox.size;
|
final Size emptyInputSize = inputBox.size;
|
||||||
@ -487,29 +452,18 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
|
testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
final TextEditingController controller = new TextEditingController();
|
||||||
InputValue inputValue = InputValue.empty;
|
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Overlay(
|
return overlay(new Center(
|
||||||
initialEntries: <OverlayEntry>[
|
child: new Material(
|
||||||
new OverlayEntry(
|
child: new TextField(
|
||||||
builder: (BuildContext context) {
|
controller: controller,
|
||||||
return new Center(
|
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||||
child: new Material(
|
maxLines: 3,
|
||||||
child: new Input(
|
),
|
||||||
value: inputValue,
|
),
|
||||||
key: inputKey,
|
));
|
||||||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
|
||||||
maxLines: 3,
|
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
@ -537,12 +491,12 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(inputValue.selection.baseOffset, 76);
|
expect(controller.selection.baseOffset, 76);
|
||||||
expect(inputValue.selection.extentOffset, 81);
|
expect(controller.selection.extentOffset, 81);
|
||||||
|
|
||||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||||
inputValue.selection);
|
controller.selection);
|
||||||
expect(endpoints.length, 2);
|
expect(endpoints.length, 2);
|
||||||
|
|
||||||
// Drag the right handle to the third line, just after 'Third'.
|
// Drag the right handle to the third line, just after 'Third'.
|
||||||
@ -555,8 +509,8 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
expect(inputValue.selection.baseOffset, 76);
|
expect(controller.selection.baseOffset, 76);
|
||||||
expect(inputValue.selection.extentOffset, 108);
|
expect(controller.selection.extentOffset, 108);
|
||||||
|
|
||||||
// Drag the left handle to the first line, just after 'First'.
|
// Drag the left handle to the first line, just after 'First'.
|
||||||
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||||||
@ -568,39 +522,30 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
expect(inputValue.selection.baseOffset, 5);
|
expect(controller.selection.baseOffset, 5);
|
||||||
expect(inputValue.selection.extentOffset, 108);
|
expect(controller.selection.extentOffset, 108);
|
||||||
|
|
||||||
await tester.tap(find.text('CUT'));
|
await tester.tap(find.text('CUT'));
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
expect(inputValue.selection.isCollapsed, true);
|
expect(controller.selection.isCollapsed, true);
|
||||||
expect(inputValue.text, cutValue);
|
expect(controller.text, cutValue);
|
||||||
}, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961
|
}, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961
|
||||||
|
|
||||||
testWidgets('Can scroll multiline input', (WidgetTester tester) async {
|
testWidgets('Can scroll multiline input', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
final Key textFieldKey = new UniqueKey();
|
||||||
InputValue inputValue = InputValue.empty;
|
final TextEditingController controller = new TextEditingController();
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Overlay(
|
return overlay(new Center(
|
||||||
initialEntries: <OverlayEntry>[
|
child: new Material(
|
||||||
new OverlayEntry(
|
child: new TextField(
|
||||||
builder: (BuildContext context) {
|
key: textFieldKey,
|
||||||
return new Center(
|
controller: controller,
|
||||||
child: new Material(
|
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||||
child: new Input(
|
maxLines: 2,
|
||||||
value: inputValue,
|
),
|
||||||
key: inputKey,
|
),
|
||||||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
));
|
||||||
maxLines: 2,
|
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
@ -610,7 +555,7 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(builder());
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));
|
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||||||
final RenderBox inputBox = findInputBox();
|
final RenderBox inputBox = findInputBox();
|
||||||
|
|
||||||
// Check that the last line of text is not displayed.
|
// Check that the last line of text is not displayed.
|
||||||
@ -652,7 +597,7 @@ void main() {
|
|||||||
|
|
||||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||||
inputValue.selection);
|
controller.selection);
|
||||||
expect(endpoints.length, 2);
|
expect(endpoints.length, 2);
|
||||||
|
|
||||||
// Drag the left handle to the first line, just after 'First'.
|
// Drag the left handle to the first line, just after 'First'.
|
||||||
@ -675,17 +620,18 @@ void main() {
|
|||||||
}, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961
|
}, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961
|
||||||
|
|
||||||
testWidgets('InputField smoke test', (WidgetTester tester) async {
|
testWidgets('InputField smoke test', (WidgetTester tester) async {
|
||||||
InputValue inputValue = InputValue.empty;
|
String textFieldValue;
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new InputField(
|
child: new TextField(
|
||||||
value: inputValue,
|
decoration: null,
|
||||||
hintText: 'Placeholder',
|
onChanged: (String value) {
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
textFieldValue = value;
|
||||||
)
|
},
|
||||||
)
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,7 +642,7 @@ void main() {
|
|||||||
await tester.idle();
|
await tester.idle();
|
||||||
|
|
||||||
// Check that the onChanged event handler fired.
|
// Check that the onChanged event handler fired.
|
||||||
expect(inputValue.text, equals(testValue));
|
expect(textFieldValue, equals(testValue));
|
||||||
|
|
||||||
return await tester.pumpWidget(builder());
|
return await tester.pumpWidget(builder());
|
||||||
}
|
}
|
||||||
@ -705,19 +651,20 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('InputField with global key', (WidgetTester tester) async {
|
testWidgets('InputField with global key', (WidgetTester tester) async {
|
||||||
final GlobalKey inputFieldKey = new GlobalKey(debugLabel: 'inputFieldKey');
|
final GlobalKey textFieldKey = new GlobalKey(debugLabel: 'textFieldKey');
|
||||||
InputValue inputValue = InputValue.empty;
|
String textFieldValue;
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new InputField(
|
child: new TextField(
|
||||||
key: inputFieldKey,
|
key: textFieldKey,
|
||||||
value: inputValue,
|
decoration: new InputDecoration(
|
||||||
hintText: 'Placeholder',
|
hintText: 'Placeholder',
|
||||||
onChanged: (InputValue value) { inputValue = value; }
|
),
|
||||||
)
|
onChanged: (String value) { textFieldValue = value; },
|
||||||
)
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -728,7 +675,7 @@ void main() {
|
|||||||
await tester.idle();
|
await tester.idle();
|
||||||
|
|
||||||
// Check that the onChanged event handler fired.
|
// Check that the onChanged event handler fired.
|
||||||
expect(inputValue.text, equals(testValue));
|
expect(textFieldValue, equals(testValue));
|
||||||
|
|
||||||
return await tester.pumpWidget(builder());
|
return await tester.pumpWidget(builder());
|
||||||
}
|
}
|
||||||
@ -736,9 +683,8 @@ void main() {
|
|||||||
checkText('Hello World');
|
checkText('Hello World');
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('InputField with default hintStyle', (WidgetTester tester) async {
|
testWidgets('TextField with default hintStyle', (WidgetTester tester) async {
|
||||||
final InputValue inputValue = InputValue.empty;
|
final TextStyle style = new TextStyle(
|
||||||
final TextStyle textStyle = new TextStyle(
|
|
||||||
color: Colors.pink[500],
|
color: Colors.pink[500],
|
||||||
fontSize: 10.0,
|
fontSize: 10.0,
|
||||||
);
|
);
|
||||||
@ -751,10 +697,11 @@ void main() {
|
|||||||
child: new Theme(
|
child: new Theme(
|
||||||
data: themeData,
|
data: themeData,
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new InputField(
|
child: new TextField(
|
||||||
value: inputValue,
|
decoration: new InputDecoration(
|
||||||
hintText: 'Placeholder',
|
hintText: 'Placeholder',
|
||||||
style: textStyle,
|
),
|
||||||
|
style: style,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -765,11 +712,10 @@ void main() {
|
|||||||
|
|
||||||
final Text hintText = tester.widget(find.text('Placeholder'));
|
final Text hintText = tester.widget(find.text('Placeholder'));
|
||||||
expect(hintText.style.color, themeData.hintColor);
|
expect(hintText.style.color, themeData.hintColor);
|
||||||
expect(hintText.style.fontSize, textStyle.fontSize);
|
expect(hintText.style.fontSize, style.fontSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('InputField with specified hintStyle', (WidgetTester tester) async {
|
testWidgets('TextField with specified hintStyle', (WidgetTester tester) async {
|
||||||
final InputValue inputValue = InputValue.empty;
|
|
||||||
final TextStyle hintStyle = new TextStyle(
|
final TextStyle hintStyle = new TextStyle(
|
||||||
color: Colors.pink[500],
|
color: Colors.pink[500],
|
||||||
fontSize: 10.0,
|
fontSize: 10.0,
|
||||||
@ -778,10 +724,11 @@ void main() {
|
|||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new InputField(
|
child: new TextField(
|
||||||
value: inputValue,
|
decoration: new InputDecoration(
|
||||||
hintText: 'Placeholder',
|
hintText: 'Placeholder',
|
||||||
hintStyle: hintStyle,
|
hintStyle: hintStyle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -793,20 +740,24 @@ void main() {
|
|||||||
expect(hintText.style, hintStyle);
|
expect(hintText.style, hintStyle);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Input label text animates', (WidgetTester tester) async {
|
testWidgets('TextField label text animates', (WidgetTester tester) async {
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
final Key secondKey = new UniqueKey();
|
||||||
|
|
||||||
Widget innerBuilder() {
|
Widget innerBuilder() {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Column(
|
child: new Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new Input(
|
new TextField(
|
||||||
labelText: 'First',
|
decoration: new InputDecoration(
|
||||||
|
labelText: 'First',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
new Input(
|
new TextField(
|
||||||
key: inputKey,
|
key: secondKey,
|
||||||
labelText: 'Second',
|
decoration: new InputDecoration(
|
||||||
|
labelText: 'Second',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -820,7 +771,7 @@ void main() {
|
|||||||
Point pos = tester.getTopLeft(find.text('Second'));
|
Point pos = tester.getTopLeft(find.text('Second'));
|
||||||
|
|
||||||
// Focus the Input. The label should start animating upwards.
|
// Focus the Input. The label should start animating upwards.
|
||||||
await tester.tap(find.byKey(inputKey));
|
await tester.tap(find.byKey(secondKey));
|
||||||
await tester.idle();
|
await tester.idle();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
@ -839,10 +790,11 @@ void main() {
|
|||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
new Center(
|
new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Input(
|
child: new TextField(
|
||||||
icon: new Icon(Icons.phone),
|
decoration: new InputDecoration(
|
||||||
labelText: 'label',
|
icon: new Icon(Icons.phone),
|
||||||
value: InputValue.empty,
|
labelText: 'label',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -850,6 +802,6 @@ void main() {
|
|||||||
|
|
||||||
final double iconRight = tester.getTopRight(find.byType(Icon)).x;
|
final double iconRight = tester.getTopRight(find.byType(Icon)).x;
|
||||||
expect(iconRight, equals(tester.getTopLeft(find.text('label')).x));
|
expect(iconRight, equals(tester.getTopLeft(find.text('label')).x));
|
||||||
expect(iconRight, equals(tester.getTopLeft(find.byType(InputField)).x));
|
expect(iconRight, equals(tester.getTopLeft(find.byType(EditableText)).x));
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -15,11 +15,11 @@ void main() {
|
|||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Form(
|
child: new Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: new TextField(
|
child: new TextFormField(
|
||||||
onSaved: (InputValue value) { fieldValue = value.text; },
|
onSaved: (String value) { fieldValue = value; },
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,10 +47,10 @@ void main() {
|
|||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Form(
|
child: new Form(
|
||||||
child: new TextField(
|
child: new TextField(
|
||||||
onChanged: (InputValue value) { fieldValue = value.text; },
|
onChanged: (String value) { fieldValue = value; },
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,8 +71,7 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('Validator sets the error text only when validate is called', (WidgetTester tester) async {
|
testWidgets('Validator sets the error text only when validate is called', (WidgetTester tester) async {
|
||||||
final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
|
final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
|
||||||
final GlobalKey inputKey = new GlobalKey();
|
String errorText(String value) => value + '/error';
|
||||||
String errorText(InputValue input) => input.text + '/error';
|
|
||||||
|
|
||||||
Widget builder(bool autovalidate) {
|
Widget builder(bool autovalidate) {
|
||||||
return new Center(
|
return new Center(
|
||||||
@ -80,12 +79,11 @@ void main() {
|
|||||||
child: new Form(
|
child: new Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
autovalidate: autovalidate,
|
autovalidate: autovalidate,
|
||||||
child: new TextField(
|
child: new TextFormField(
|
||||||
key: inputKey,
|
|
||||||
validator: errorText,
|
validator: errorText,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,10 +97,10 @@ void main() {
|
|||||||
await tester.pumpWidget(builder(false));
|
await tester.pumpWidget(builder(false));
|
||||||
|
|
||||||
// We have to manually validate if we're not autovalidating.
|
// We have to manually validate if we're not autovalidating.
|
||||||
expect(find.text(errorText(new InputValue(text: testValue))), findsNothing);
|
expect(find.text(errorText(testValue)), findsNothing);
|
||||||
formKey.currentState.validate();
|
formKey.currentState.validate();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text(errorText(new InputValue(text: testValue))), findsOneWidget);
|
expect(find.text(errorText(testValue)), findsOneWidget);
|
||||||
|
|
||||||
// Try again with autovalidation. Should validate immediately.
|
// Try again with autovalidation. Should validate immediately.
|
||||||
formKey.currentState.reset();
|
formKey.currentState.reset();
|
||||||
@ -110,18 +108,18 @@ void main() {
|
|||||||
await tester.idle();
|
await tester.idle();
|
||||||
await tester.pumpWidget(builder(true));
|
await tester.pumpWidget(builder(true));
|
||||||
|
|
||||||
expect(find.text(errorText(new InputValue(text: testValue))), findsOneWidget);
|
expect(find.text(errorText(testValue)), findsOneWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
await checkErrorText('Test');
|
await checkErrorText('Test');
|
||||||
await checkErrorText('');
|
await checkErrorText('');
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Multiple Inputs communicate', (WidgetTester tester) async {
|
testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async {
|
||||||
final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
|
final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
|
||||||
final GlobalKey<FormFieldState<InputValue>> fieldKey = new GlobalKey<FormFieldState<InputValue>>();
|
final GlobalKey<FormFieldState<String>> fieldKey = new GlobalKey<FormFieldState<String>>();
|
||||||
// Input 2's validator depends on a input 1's value.
|
// Input 2's validator depends on a input 1's value.
|
||||||
String errorText(InputValue input) => fieldKey.currentState.value?.text.toString() + '/error';
|
String errorText(String input) => fieldKey.currentState.value?.toString() + '/error';
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Center(
|
return new Center(
|
||||||
@ -131,10 +129,10 @@ void main() {
|
|||||||
autovalidate: true,
|
autovalidate: true,
|
||||||
child: new ListView(
|
child: new ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new TextField(
|
new TextFormField(
|
||||||
key: fieldKey,
|
key: fieldKey,
|
||||||
),
|
),
|
||||||
new TextField(
|
new TextFormField(
|
||||||
validator: errorText,
|
validator: errorText,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -162,18 +160,19 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('Provide initial value to input', (WidgetTester tester) async {
|
testWidgets('Provide initial value to input', (WidgetTester tester) async {
|
||||||
final String initialValue = 'hello';
|
final String initialValue = 'hello';
|
||||||
final GlobalKey<FormFieldState<InputValue>> inputKey = new GlobalKey<FormFieldState<InputValue>>();
|
final TextEditingController controller = new TextEditingController(text: initialValue);
|
||||||
|
final GlobalKey<FormFieldState<String>> inputKey = new GlobalKey<FormFieldState<String>>();
|
||||||
|
|
||||||
Widget builder() {
|
Widget builder() {
|
||||||
return new Center(
|
return new Center(
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Form(
|
child: new Form(
|
||||||
child: new TextField(
|
child: new TextFormField(
|
||||||
key: inputKey,
|
key: inputKey,
|
||||||
initialValue: new InputValue(text: initialValue),
|
controller: controller,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,20 +185,19 @@ void main() {
|
|||||||
|
|
||||||
// initial value should also be visible in the raw input line
|
// initial value should also be visible in the raw input line
|
||||||
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
final EditableTextState editableText = tester.state(find.byType(EditableText));
|
||||||
expect(editableText.config.value.text, equals(initialValue));
|
expect(editableText.config.controller.text, equals(initialValue));
|
||||||
|
|
||||||
// sanity check, make sure we can still edit the text and everything updates
|
// sanity check, make sure we can still edit the text and everything updates
|
||||||
expect(inputKey.currentState.value.text, equals(initialValue));
|
expect(inputKey.currentState.value, equals(initialValue));
|
||||||
await tester.enterText(find.byType(EditableText), 'world');
|
await tester.enterText(find.byType(EditableText), 'world');
|
||||||
await tester.idle();
|
await tester.idle();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(inputKey.currentState.value.text, equals('world'));
|
expect(inputKey.currentState.value, equals('world'));
|
||||||
expect(editableText.config.value.text, equals('world'));
|
expect(editableText.config.controller.text, equals('world'));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('No crash when a FormField is removed from the tree', (WidgetTester tester) async {
|
testWidgets('No crash when a TextFormField is removed from the tree', (WidgetTester tester) async {
|
||||||
final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
|
final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
|
||||||
final GlobalKey fieldKey = new GlobalKey();
|
|
||||||
String fieldValue;
|
String fieldValue;
|
||||||
|
|
||||||
Widget builder(bool remove) {
|
Widget builder(bool remove) {
|
||||||
@ -207,14 +205,13 @@ void main() {
|
|||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Form(
|
child: new Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: remove ? new Container() : new TextField(
|
child: remove ? new Container() : new TextFormField(
|
||||||
key: fieldKey,
|
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
onSaved: (InputValue value) { fieldValue = value.text; },
|
onSaved: (String value) { fieldValue = value; },
|
||||||
validator: (InputValue value) { return value.text.isEmpty ? null : 'yes'; }
|
validator: (String value) { return value.isEmpty ? null : 'yes'; }
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,7 +321,7 @@ class FlutterDriver {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the text value of the `Input` widget located by [finder].
|
/// Sets the text value of the [TextField] widget located by [finder].
|
||||||
///
|
///
|
||||||
/// This command invokes the `onChanged` handler of the `Input` widget with
|
/// This command invokes the `onChanged` handler of the `Input` widget with
|
||||||
/// the provided [text].
|
/// the provided [text].
|
||||||
@ -330,10 +330,10 @@ class FlutterDriver {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Submits the current text value of the `Input` widget located by [finder].
|
/// Submits the current text value of the [TextField] widget located by [finder].
|
||||||
///
|
///
|
||||||
/// This command invokes the `onSubmitted` handler of the `Input` widget and
|
/// This command invokes the `onSubmitted` handler of the [TextField] widget
|
||||||
/// the returns the submitted text value.
|
/// and the returns the submitted text value.
|
||||||
Future<String> submitInputText(SerializableFinder finder, {Duration timeout}) async {
|
Future<String> submitInputText(SerializableFinder finder, {Duration timeout}) async {
|
||||||
final Map<String, dynamic> json = await _sendCommand(new SubmitInputText(finder, timeout: timeout));
|
final Map<String, dynamic> json = await _sendCommand(new SubmitInputText(finder, timeout: timeout));
|
||||||
return json['text'];
|
return json['text'];
|
||||||
|
@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart' show RendererBinding;
|
import 'package:flutter/rendering.dart' show RendererBinding;
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
@ -262,20 +263,29 @@ class FlutterDriverExtension {
|
|||||||
return new ScrollResult();
|
return new ScrollResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Finder _findEditableText(SerializableFinder finder) {
|
||||||
|
return find.descendant(of: _createFinder(finder), matching: find.byType(EditableText));
|
||||||
|
}
|
||||||
|
|
||||||
|
EditableTextState _getEditableTextState(Finder finder) {
|
||||||
|
final StatefulElement element = finder.evaluate().single;
|
||||||
|
return element.state;
|
||||||
|
}
|
||||||
|
|
||||||
Future<SetInputTextResult> _setInputText(Command command) async {
|
Future<SetInputTextResult> _setInputText(Command command) async {
|
||||||
final SetInputText setInputTextCommand = command;
|
final SetInputText setInputTextCommand = command;
|
||||||
final Finder target = await _waitForElement(_createFinder(setInputTextCommand.finder));
|
final Finder target = await _waitForElement(_findEditableText(setInputTextCommand.finder));
|
||||||
final Input input = target.evaluate().single.widget;
|
final EditableTextState editable = _getEditableTextState(target);
|
||||||
input.onChanged(new InputValue(text: setInputTextCommand.text));
|
editable.updateEditingValue(new TextEditingValue(text: setInputTextCommand.text));
|
||||||
return new SetInputTextResult();
|
return new SetInputTextResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SubmitInputTextResult> _submitInputText(Command command) async {
|
Future<SubmitInputTextResult> _submitInputText(Command command) async {
|
||||||
final SubmitInputText submitInputTextCommand = command;
|
final SubmitInputText submitInputTextCommand = command;
|
||||||
final Finder target = await _waitForElement(_createFinder(submitInputTextCommand.finder));
|
final Finder target = await _waitForElement(_findEditableText(submitInputTextCommand.finder));
|
||||||
final Input input = target.evaluate().single.widget;
|
final EditableTextState editable = _getEditableTextState(target);
|
||||||
input.onSubmitted(input.value);
|
editable.performAction(TextInputAction.done);
|
||||||
return new SubmitInputTextResult(input.value.text);
|
return new SubmitInputTextResult(editable.config.controller.value.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<GetTextResult> _getText(Command command) async {
|
Future<GetTextResult> _getText(Command command) async {
|
||||||
|
Loading…
Reference in New Issue
Block a user