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 {
|
||||
CardModel(this.value, this.height) {
|
||||
inputValue = new InputValue(text: 'Item $value');
|
||||
textController = new TextEditingController(text: 'Item $value');
|
||||
}
|
||||
int value;
|
||||
double height;
|
||||
int get color => ((value % 9) + 1) * 100;
|
||||
InputValue inputValue;
|
||||
TextEditingController textController;
|
||||
Key get key => new ObjectKey(this);
|
||||
}
|
||||
|
||||
@ -245,11 +245,7 @@ class CardCollectionState extends State<CardCollection> {
|
||||
new Center(
|
||||
child: new TextField(
|
||||
key: new GlobalObjectKey(cardModel),
|
||||
onChanged: (InputValue value) {
|
||||
setState(() {
|
||||
cardModel.inputValue = value;
|
||||
});
|
||||
},
|
||||
controller: cardModel.textController,
|
||||
),
|
||||
)
|
||||
: new DefaultTextStyle.merge(
|
||||
@ -261,7 +257,7 @@ class CardCollectionState extends State<CardCollection> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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) {
|
||||
return new InkWell(
|
||||
onTap: onPressed,
|
||||
child: new InputContainer(
|
||||
labelText: labelText,
|
||||
style: valueStyle,
|
||||
child: new InputDecorator(
|
||||
decoration: new InputDecoration(
|
||||
labelText: labelText,
|
||||
),
|
||||
baseStyle: valueStyle,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -133,11 +135,15 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: <Widget>[
|
||||
new TextField(
|
||||
labelText: 'Event name',
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Event name',
|
||||
),
|
||||
style: Theme.of(context).textTheme.display1,
|
||||
),
|
||||
new TextField(
|
||||
labelText: 'Location',
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Location',
|
||||
),
|
||||
style: Theme.of(context).textTheme.display1.copyWith(fontSize: 20.0),
|
||||
),
|
||||
new _DateTimePicker(
|
||||
@ -170,9 +176,11 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
|
||||
});
|
||||
},
|
||||
),
|
||||
new InputContainer(
|
||||
labelText: 'Activity',
|
||||
hintText: 'Choose an activity',
|
||||
new InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Activity',
|
||||
hintText: 'Choose an activity',
|
||||
),
|
||||
isEmpty: _activity == null,
|
||||
child: new DropdownButton<String>(
|
||||
value: _activity,
|
||||
|
@ -148,10 +148,11 @@ class DemoItem<T> {
|
||||
this.hint,
|
||||
this.builder,
|
||||
this.valueToString
|
||||
});
|
||||
}) : textController = new TextEditingController(text: valueToString(value));
|
||||
|
||||
final String name;
|
||||
final String hint;
|
||||
final TextEditingController textController;
|
||||
final DemoItemBodyBuilder<T> builder;
|
||||
final ValueToString<T> valueToString;
|
||||
T value;
|
||||
@ -205,18 +206,20 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
|
||||
onCancel: () { Form.of(context).reset(); close(); },
|
||||
child: new Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: new TextField(
|
||||
hintText: item.hint,
|
||||
labelText: item.name,
|
||||
initialValue: new InputValue(text: item.value),
|
||||
onSaved: (InputValue val) { item.value = val.text; },
|
||||
child: new TextFormField(
|
||||
controller: item.textController,
|
||||
decoration: new InputDecoration(
|
||||
hintText: item.hint,
|
||||
labelText: item.name,
|
||||
),
|
||||
onSaved: (String value) { item.value = value; },
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
new DemoItem<_Location>(
|
||||
name: 'Location',
|
||||
@ -229,8 +232,6 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
|
||||
item.isExpanded = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return new Form(
|
||||
child: new Builder(
|
||||
builder: (BuildContext context) {
|
||||
|
@ -27,6 +27,6 @@ export 'slider_demo.dart';
|
||||
export 'snack_bar_demo.dart';
|
||||
export 'tabs_demo.dart';
|
||||
export 'tabs_fab_demo.dart';
|
||||
export 'text_field_demo.dart';
|
||||
export 'text_form_field_demo.dart';
|
||||
export 'tooltip_demo.dart';
|
||||
export 'two_level_list_demo.dart';
|
||||
|
@ -6,13 +6,13 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TextFieldDemo extends StatefulWidget {
|
||||
TextFieldDemo({ Key key }) : super(key: key);
|
||||
class TextFormFieldDemo extends StatefulWidget {
|
||||
TextFormFieldDemo({ Key key }) : super(key: key);
|
||||
|
||||
static const String routeName = '/material/text-field';
|
||||
static const String routeName = '/material/text-form-field';
|
||||
|
||||
@override
|
||||
TextFieldDemoState createState() => new TextFieldDemoState();
|
||||
TextFormFieldDemoState createState() => new TextFormFieldDemoState();
|
||||
}
|
||||
|
||||
class PersonData {
|
||||
@ -21,7 +21,7 @@ class PersonData {
|
||||
String password = '';
|
||||
}
|
||||
|
||||
class TextFieldDemoState extends State<TextFieldDemo> {
|
||||
class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
|
||||
PersonData person = new PersonData();
|
||||
@ -35,7 +35,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
||||
bool _autovalidate = false;
|
||||
bool _formWasEdited = false;
|
||||
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() {
|
||||
final FormState form = _formKey.currentState;
|
||||
if (!form.validate()) {
|
||||
@ -47,30 +47,30 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
||||
}
|
||||
}
|
||||
|
||||
String _validateName(InputValue value) {
|
||||
String _validateName(String value) {
|
||||
_formWasEdited = true;
|
||||
if (value.text.isEmpty)
|
||||
if (value.isEmpty)
|
||||
return 'Name is required.';
|
||||
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 null;
|
||||
}
|
||||
|
||||
String _validatePhoneNumber(InputValue value) {
|
||||
String _validatePhoneNumber(String value) {
|
||||
_formWasEdited = true;
|
||||
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 null;
|
||||
}
|
||||
|
||||
String _validatePassword(InputValue value) {
|
||||
String _validatePassword(String value) {
|
||||
_formWasEdited = true;
|
||||
final FormFieldState<InputValue> passwordField = _passwordFieldKey.currentState;
|
||||
if (passwordField.value == null || passwordField.value.text.isEmpty)
|
||||
final FormFieldState<String> passwordField = _passwordFieldKey.currentState;
|
||||
if (passwordField.value == null || passwordField.value.isEmpty)
|
||||
return 'Please choose a password.';
|
||||
if (passwordField.value.text != value.text)
|
||||
if (passwordField.value != value)
|
||||
return 'Passwords don\'t match';
|
||||
return null;
|
||||
}
|
||||
@ -104,7 +104,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
||||
return new Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: new AppBar(
|
||||
title: new Text('Text fields')
|
||||
title: new Text('Text fields'),
|
||||
),
|
||||
body: new Form(
|
||||
key: _formKey,
|
||||
@ -113,48 +113,58 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
||||
child: new ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
children: <Widget>[
|
||||
new TextField(
|
||||
icon: new Icon(Icons.person),
|
||||
hintText: 'What do people call you?',
|
||||
labelText: 'Name *',
|
||||
onSaved: (InputValue val) { person.name = val.text; },
|
||||
new TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
icon: const Icon(Icons.person),
|
||||
hintText: 'What do people call you?',
|
||||
labelText: 'Name *',
|
||||
),
|
||||
onSaved: (String value) { person.name = value; },
|
||||
validator: _validateName,
|
||||
),
|
||||
new TextField(
|
||||
icon: new Icon(Icons.phone),
|
||||
hintText: 'Where can we reach you?',
|
||||
labelText: 'Phone Number *',
|
||||
new TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
icon: const Icon(Icons.phone),
|
||||
hintText: 'Where can we reach you?',
|
||||
labelText: 'Phone Number *',
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
onSaved: (InputValue val) { person.phoneNumber = val.text; },
|
||||
onSaved: (String value) { person.phoneNumber = value; },
|
||||
validator: _validatePhoneNumber,
|
||||
),
|
||||
new TextField(
|
||||
hintText: 'Tell us about yourself',
|
||||
labelText: 'Life story',
|
||||
new TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Tell us about yourself',
|
||||
labelText: 'Life story',
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
new Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
new Expanded(
|
||||
child: new TextField(
|
||||
child: new TextFormField(
|
||||
key: _passwordFieldKey,
|
||||
hintText: 'How do you log in?',
|
||||
labelText: 'New Password *',
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'How do you log in?',
|
||||
labelText: 'New Password *',
|
||||
),
|
||||
obscureText: true,
|
||||
onSaved: (InputValue val) { person.password = val.text; }
|
||||
)
|
||||
onSaved: (String value) { person.password = value; },
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
new Expanded(
|
||||
child: new TextField(
|
||||
hintText: 'How do you log in?',
|
||||
labelText: 'Re-type Password *',
|
||||
child: new TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'How do you log in?',
|
||||
labelText: 'Re-type Password *',
|
||||
),
|
||||
obscureText: true,
|
||||
validator: _validatePassword,
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
new Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
@ -168,9 +178,9 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: new Text('* indicates required field', style: Theme.of(context).textTheme.caption),
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -261,8 +261,8 @@ final List<GalleryItem> kAllGalleryItems = <GalleryItem>[
|
||||
title: 'Text fields',
|
||||
subtitle: 'Single line of editable text and numbers',
|
||||
category: 'Material Components',
|
||||
routeName: TextFieldDemo.routeName,
|
||||
buildRoute: (BuildContext context) => new TextFieldDemo(),
|
||||
routeName: TextFormFieldDemo.routeName,
|
||||
buildRoute: (BuildContext context) => new TextFormFieldDemo(),
|
||||
),
|
||||
new GalleryItem(
|
||||
title: 'Tooltips',
|
||||
|
@ -29,22 +29,22 @@ class _NotImplementedDialog extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
new Icon(
|
||||
Icons.dvr,
|
||||
size: 18.0
|
||||
size: 18.0,
|
||||
),
|
||||
new Container(
|
||||
width: 8.0
|
||||
width: 8.0,
|
||||
),
|
||||
new Text('DUMP APP TO CONSOLE'),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
new FlatButton(
|
||||
onPressed: () {
|
||||
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>();
|
||||
bool _isSearching = false;
|
||||
InputValue _searchQuery = InputValue.empty;
|
||||
final TextEditingController _searchQuery = new TextEditingController();
|
||||
bool _autorefresh = false;
|
||||
|
||||
void _handleSearchBegin() {
|
||||
@ -73,9 +73,9 @@ class StockHomeState extends State<StockHome> {
|
||||
onRemove: () {
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
_searchQuery = InputValue.empty;
|
||||
_searchQuery.clear();
|
||||
});
|
||||
}
|
||||
},
|
||||
));
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
@ -86,12 +86,6 @@ class StockHomeState extends State<StockHome> {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _handleSearchQueryChanged(InputValue query) {
|
||||
setState(() {
|
||||
_searchQuery = query;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleStockModeChange(StockMode value) {
|
||||
if (config.updater != null)
|
||||
config.updater(config.configuration.copyWith(stockMode: value));
|
||||
@ -155,7 +149,7 @@ class StockHomeState extends State<StockHome> {
|
||||
trailing: new Radio<StockMode>(
|
||||
value: StockMode.optimistic,
|
||||
groupValue: config.configuration.stockMode,
|
||||
onChanged: _handleStockModeChange
|
||||
onChanged: _handleStockModeChange,
|
||||
),
|
||||
onTap: () {
|
||||
_handleStockModeChange(StockMode.optimistic);
|
||||
@ -167,7 +161,7 @@ class StockHomeState extends State<StockHome> {
|
||||
trailing: new Radio<StockMode>(
|
||||
value: StockMode.pessimistic,
|
||||
groupValue: config.configuration.stockMode,
|
||||
onChanged: _handleStockModeChange
|
||||
onChanged: _handleStockModeChange,
|
||||
),
|
||||
onTap: () {
|
||||
_handleStockModeChange(StockMode.pessimistic);
|
||||
@ -184,8 +178,8 @@ class StockHomeState extends State<StockHome> {
|
||||
title: new Text('About'),
|
||||
onTap: _handleShowAbout,
|
||||
),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -205,7 +199,7 @@ class StockHomeState extends State<StockHome> {
|
||||
new IconButton(
|
||||
icon: new Icon(Icons.search),
|
||||
onPressed: _handleSearchBegin,
|
||||
tooltip: 'Search'
|
||||
tooltip: 'Search',
|
||||
),
|
||||
new PopupMenuButton<_StockMenuItem>(
|
||||
onSelected: (_StockMenuItem value) { _handleStockMenu(context, value); },
|
||||
@ -213,29 +207,29 @@ class StockHomeState extends State<StockHome> {
|
||||
new CheckedPopupMenuItem<_StockMenuItem>(
|
||||
value: _StockMenuItem.autorefresh,
|
||||
checked: _autorefresh,
|
||||
child: new Text('Autorefresh')
|
||||
child: new Text('Autorefresh'),
|
||||
),
|
||||
new PopupMenuItem<_StockMenuItem>(
|
||||
value: _StockMenuItem.refresh,
|
||||
child: new Text('Refresh')
|
||||
child: new Text('Refresh'),
|
||||
),
|
||||
new PopupMenuItem<_StockMenuItem>(
|
||||
value: _StockMenuItem.speedUp,
|
||||
child: new Text('Increase animation speed')
|
||||
child: new Text('Increase animation speed'),
|
||||
),
|
||||
new PopupMenuItem<_StockMenuItem>(
|
||||
value: _StockMenuItem.speedDown,
|
||||
child: new Text('Decrease animation speed')
|
||||
)
|
||||
]
|
||||
)
|
||||
child: new Text('Decrease animation speed'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
bottom: new TabBar(
|
||||
tabs: <Widget>[
|
||||
new Tab(text: StockStrings.of(context).market()),
|
||||
new Tab(text: StockStrings.of(context).portfolio()),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -262,8 +256,8 @@ class StockHomeState extends State<StockHome> {
|
||||
label: "BUY MORE",
|
||||
onPressed: () {
|
||||
_buyStock(stock);
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@ -276,14 +270,14 @@ class StockHomeState extends State<StockHome> {
|
||||
},
|
||||
onShow: (Stock stock) {
|
||||
_scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) => new StockSymbolBottomSheet(stock: stock));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStockTab(BuildContext context, StockHomeTab tab, List<String> stockSymbols) {
|
||||
return new Container(
|
||||
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),
|
||||
color: Theme.of(context).accentColor,
|
||||
onPressed: _handleSearchEnd,
|
||||
tooltip: 'Back'
|
||||
tooltip: 'Back',
|
||||
),
|
||||
title: new TextField(
|
||||
controller: _searchQuery,
|
||||
autofocus: true,
|
||||
hintText: 'Search stocks',
|
||||
onChanged: _handleSearchQueryChanged
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search stocks',
|
||||
),
|
||||
),
|
||||
backgroundColor: Theme.of(context).canvasColor
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleCreateCompany() {
|
||||
showModalBottomSheet<Null>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => new _CreateCompanySheet()
|
||||
builder: (BuildContext context) => new _CreateCompanySheet(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -319,7 +315,7 @@ class StockHomeState extends State<StockHome> {
|
||||
tooltip: 'Create company',
|
||||
child: new Icon(Icons.add),
|
||||
backgroundColor: Colors.redAccent,
|
||||
onPressed: _handleCreateCompany
|
||||
onPressed: _handleCreateCompany,
|
||||
);
|
||||
}
|
||||
|
||||
@ -336,9 +332,9 @@ class StockHomeState extends State<StockHome> {
|
||||
children: <Widget>[
|
||||
_buildStockTab(context, StockHomeTab.market, config.symbols),
|
||||
_buildStockTab(context, StockHomeTab.portfolio, portfolioSymbols),
|
||||
]
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -351,9 +347,11 @@ class _CreateCompanySheet extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
new TextField(
|
||||
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_splash.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/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/tab_controller.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_data.dart';
|
||||
export 'src/material/time_picker.dart';
|
||||
|
@ -158,6 +158,19 @@ class _MergingListenable extends ChangeNotifier {
|
||||
child?.removeListener(notifyListeners);
|
||||
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.
|
||||
|
@ -462,7 +462,7 @@ class DropdownButton<T> extends StatefulWidget {
|
||||
/// 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
|
||||
/// 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;
|
||||
|
||||
@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);
|
||||
|
||||
final TextSelectionDelegate delegate;
|
||||
InputValue get value => delegate.inputValue;
|
||||
TextEditingValue get value => delegate.textEditingValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -51,7 +51,7 @@ class _TextSelectionToolbar extends StatelessWidget {
|
||||
|
||||
void _handleCut() {
|
||||
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),
|
||||
selection: new TextSelection.collapsed(offset: value.selection.start)
|
||||
);
|
||||
@ -60,7 +60,7 @@ class _TextSelectionToolbar extends StatelessWidget {
|
||||
|
||||
void _handleCopy() {
|
||||
Clipboard.setData(new ClipboardData(text: value.selection.textInside(value.text)));
|
||||
delegate.inputValue = new InputValue(
|
||||
delegate.textEditingValue = new TextEditingValue(
|
||||
text: value.text,
|
||||
selection: new TextSelection.collapsed(offset: value.selection.end)
|
||||
);
|
||||
@ -68,10 +68,10 @@ class _TextSelectionToolbar extends StatelessWidget {
|
||||
}
|
||||
|
||||
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);
|
||||
if (data != null) {
|
||||
delegate.inputValue = new InputValue(
|
||||
delegate.textEditingValue = new TextEditingValue(
|
||||
text: value.selection.textBefore(value.text) + data.text + value.selection.textAfter(value.text),
|
||||
selection: new TextSelection.collapsed(offset: value.selection.start + data.text.length)
|
||||
);
|
||||
@ -80,7 +80,7 @@ class _TextSelectionToolbar extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _handleSelectAll() {
|
||||
delegate.inputValue = new InputValue(
|
||||
delegate.textEditingValue = new TextEditingValue(
|
||||
text: value.text,
|
||||
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;
|
||||
|
||||
@override
|
||||
bool operator== (dynamic other) {
|
||||
bool operator == (dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (runtimeType != other.runtimeType)
|
||||
|
@ -7,6 +7,7 @@ import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'focus_manager.dart';
|
||||
@ -18,91 +19,48 @@ import 'scroll_physics.dart';
|
||||
import 'scrollable.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);
|
||||
|
||||
InputValue _getInputValueFromEditingValue(TextEditingValue value) {
|
||||
return new InputValue(
|
||||
text: value.text,
|
||||
selection: value.selection,
|
||||
composing: value.composing,
|
||||
);
|
||||
}
|
||||
class TextEditingController extends ChangeNotifier {
|
||||
TextEditingController({ String text })
|
||||
: _value = text == null ? TextEditingValue.empty : new TextEditingValue(text: text);
|
||||
|
||||
TextEditingValue _getTextEditingValueFromInputValue(InputValue value) {
|
||||
return new TextEditingValue(
|
||||
text: value.text,
|
||||
selection: value.selection,
|
||||
composing: value.composing,
|
||||
);
|
||||
}
|
||||
TextEditingController.fromValue(TextEditingValue value)
|
||||
: _value = value ?? TextEditingValue.empty;
|
||||
|
||||
/// Configuration information for a text input field.
|
||||
///
|
||||
/// An [InputValue] contains the text for the input field as well as the
|
||||
/// selection extent and the composing range.
|
||||
class InputValue {
|
||||
// TODO(abarth): This class is really the same as TextEditingState.
|
||||
// We should merge them into one object.
|
||||
TextEditingValue get value => _value;
|
||||
TextEditingValue _value;
|
||||
set value(TextEditingValue newValue) {
|
||||
assert(newValue != null);
|
||||
if (_value == newValue)
|
||||
return;
|
||||
_value = newValue;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Creates configuration information for an input field
|
||||
///
|
||||
/// The selection and composing range must be within the text.
|
||||
///
|
||||
/// 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
|
||||
});
|
||||
String get text => _value.text;
|
||||
set text(String newText) {
|
||||
value = value.copyWith(text: newText, composing: TextRange.empty);
|
||||
}
|
||||
|
||||
/// The current text being edited.
|
||||
final String text;
|
||||
TextSelection get selection => _value.selection;
|
||||
set selection(TextSelection newSelection) {
|
||||
value = value.copyWith(selection: newSelection, composing: TextRange.empty);
|
||||
}
|
||||
|
||||
/// The range of text that is currently selected.
|
||||
final TextSelection selection;
|
||||
void clear() {
|
||||
value = TextEditingValue.empty;
|
||||
}
|
||||
|
||||
/// The range of text that is still being composed.
|
||||
final TextRange composing;
|
||||
|
||||
/// 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;
|
||||
void clearComposing() {
|
||||
value = value.copyWith(composing: TextRange.empty);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(
|
||||
text.hashCode,
|
||||
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
|
||||
);
|
||||
String toString() {
|
||||
return '$runtimeType#$hashCode($value)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,11 +84,11 @@ class InputValue {
|
||||
class EditableText extends StatefulWidget {
|
||||
/// Creates a basic text input control.
|
||||
///
|
||||
/// The [value], [focusNode], [style], and [cursorColor] arguments must not
|
||||
/// be null.
|
||||
/// The [controller], [focusNode], [style], and [cursorColor] arguments must
|
||||
/// not be null.
|
||||
EditableText({
|
||||
Key key,
|
||||
@required this.value,
|
||||
@required this.controller,
|
||||
@required this.focusNode,
|
||||
this.obscureText: false,
|
||||
@required this.style,
|
||||
@ -144,7 +102,7 @@ class EditableText extends StatefulWidget {
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
}) : super(key: key) {
|
||||
assert(value != null);
|
||||
assert(controller != null);
|
||||
assert(focusNode != null);
|
||||
assert(obscureText != null);
|
||||
assert(style != null);
|
||||
@ -153,8 +111,8 @@ class EditableText extends StatefulWidget {
|
||||
assert(autofocus != null);
|
||||
}
|
||||
|
||||
/// The string being displayed in this widget.
|
||||
final InputValue value;
|
||||
/// Controls the text being edited.
|
||||
final TextEditingController controller;
|
||||
|
||||
/// Controls whether this widget has keyboard focus.
|
||||
final FocusNode focusNode;
|
||||
@ -200,10 +158,10 @@ class EditableText extends StatefulWidget {
|
||||
final TextInputType keyboardType;
|
||||
|
||||
/// 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.
|
||||
final ValueChanged<InputValue> onSubmitted;
|
||||
final ValueChanged<String> onSubmitted;
|
||||
|
||||
@override
|
||||
EditableTextState createState() => new EditableTextState();
|
||||
@ -214,17 +172,18 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
Timer _cursorTimer;
|
||||
bool _showCursor = false;
|
||||
|
||||
InputValue _currentValue;
|
||||
TextInputConnection _textInputConnection;
|
||||
TextSelectionOverlay _selectionOverlay;
|
||||
|
||||
final ScrollController _scrollController = new ScrollController();
|
||||
bool _didAutoFocus = false;
|
||||
|
||||
// State lifecycle:
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentValue = config.value;
|
||||
config.controller.addListener(_didChangeTextEditingValue);
|
||||
config.focusNode.addListener(_handleFocusChanged);
|
||||
}
|
||||
|
||||
@ -240,17 +199,63 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
|
||||
@override
|
||||
void didUpdateConfig(EditableText oldConfig) {
|
||||
if (_currentValue != config.value) {
|
||||
_currentValue = config.value;
|
||||
if (_isAttachedToKeyboard)
|
||||
_textInputConnection.setEditingState(_getTextEditingValueFromInputValue(_currentValue));
|
||||
}
|
||||
if (config.controller != oldConfig.controller) {
|
||||
oldConfig.controller.removeListener(_didChangeTextEditingValue);
|
||||
config.controller.addListener(_didChangeTextEditingValue);
|
||||
if (_isAttachedToKeyboard && config.controller.value != oldConfig.controller.value)
|
||||
_textInputConnection.setEditingState(config.controller.value);
|
||||
}
|
||||
if (config.focusNode != oldConfig.focusNode) {
|
||||
oldConfig.focusNode.removeListener(_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 _isMultiline => config.maxLines > 1;
|
||||
@ -273,24 +278,18 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
void _attachOrDetachKeyboard(bool focused) {
|
||||
if (focused && !_isAttachedToKeyboard && _didRequestKeyboard) {
|
||||
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType))
|
||||
..setEditingState(_getTextEditingValueFromInputValue(_currentValue))
|
||||
..setEditingState(_value)
|
||||
..show();
|
||||
} else if (!focused) {
|
||||
if (_isAttachedToKeyboard) {
|
||||
_textInputConnection.close();
|
||||
_textInputConnection = null;
|
||||
}
|
||||
_clearComposing();
|
||||
config.controller.clearComposing();
|
||||
}
|
||||
_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.
|
||||
///
|
||||
/// 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 updateEditingValue(TextEditingValue value) {
|
||||
_currentValue = _getInputValueFromEditingValue(value);
|
||||
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 _hideSelectionOverlayIfNeeded() {
|
||||
_selectionOverlay?.hide();
|
||||
_selectionOverlay = null;
|
||||
}
|
||||
|
||||
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.
|
||||
requestKeyboard();
|
||||
|
||||
final InputValue newInput = _currentValue.copyWith(selection: selection, composing: TextRange.empty);
|
||||
if (config.onChanged != null)
|
||||
config.onChanged(newInput);
|
||||
|
||||
if (_selectionOverlay != null) {
|
||||
_selectionOverlay.hide();
|
||||
_selectionOverlay = null;
|
||||
}
|
||||
_hideSelectionOverlayIfNeeded();
|
||||
config.controller.selection = selection;
|
||||
|
||||
if (config.selectionControls != null) {
|
||||
_selectionOverlay = new TextSelectionOverlay(
|
||||
input: newInput,
|
||||
context: context,
|
||||
value: _value,
|
||||
debugRequiredFor: config,
|
||||
renderObject: renderObject,
|
||||
onSelectionOverlayChanged: _handleSelectionOverlayChanged,
|
||||
selectionControls: config.selectionControls,
|
||||
);
|
||||
if (newInput.text.isNotEmpty || longPress)
|
||||
if (_value.text.isNotEmpty || longPress)
|
||||
_selectionOverlay.showHandles();
|
||||
if (longPress)
|
||||
_selectionOverlay.showToolbar();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSelectionOverlayChanged(InputValue newInput, Rect caretRect) {
|
||||
assert(!newInput.composing.isValid); // composing range must be empty while selecting
|
||||
if (config.onChanged != null)
|
||||
config.onChanged(newInput);
|
||||
void _handleSelectionOverlayChanged(TextEditingValue value, Rect caretRect) {
|
||||
assert(!value.composing.isValid); // composing range must be empty while selecting.
|
||||
_value = value;
|
||||
_scrollController.jumpTo(_getScrollOffsetForCaret(caretRect));
|
||||
}
|
||||
|
||||
/// Whether the blinking cursor is actually visible at this precise moment
|
||||
/// (it's hidden half the time, since it blinks).
|
||||
@visibleForTesting
|
||||
bool get cursorCurrentlyVisible => _showCursor;
|
||||
|
||||
/// 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
|
||||
/// value (half on, half off).
|
||||
@visibleForTesting
|
||||
Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
|
||||
|
||||
void _cursorTick(Timer timer) {
|
||||
@ -390,37 +370,21 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
final bool focused = config.focusNode.hasFocus;
|
||||
_attachOrDetachKeyboard(focused);
|
||||
|
||||
if (_cursorTimer == null && focused && config.value.selection.isCollapsed)
|
||||
if (_cursorTimer == null && focused && _value.selection.isCollapsed)
|
||||
_startCursorTimer();
|
||||
else if (_cursorTimer != null && (!focused || !config.value.selection.isCollapsed))
|
||||
else if (_cursorTimer != null && (!focused || !_value.selection.isCollapsed))
|
||||
_stopCursorTimer();
|
||||
|
||||
if (_selectionOverlay != null) {
|
||||
if (focused) {
|
||||
_selectionOverlay.update(config.value);
|
||||
_selectionOverlay.update(_value);
|
||||
} else {
|
||||
_selectionOverlay?.dispose();
|
||||
_selectionOverlay.dispose();
|
||||
_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() {
|
||||
_cursorTimer.cancel();
|
||||
_cursorTimer = null;
|
||||
@ -436,7 +400,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
physics: const ClampingScrollPhysics(),
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
return new _Editable(
|
||||
value: _currentValue,
|
||||
value: _value,
|
||||
style: config.style,
|
||||
cursorColor: config.cursorColor,
|
||||
showCursor: _showCursor,
|
||||
@ -467,7 +431,7 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
this.onSelectionChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
final InputValue value;
|
||||
final TextEditingValue value;
|
||||
final TextStyle style;
|
||||
final Color cursorColor;
|
||||
final bool showCursor;
|
||||
|
@ -43,22 +43,22 @@ enum TextSelectionHandleType {
|
||||
/// [start] handle always moves the [start]/[baseOffset] of the selection.
|
||||
enum _TextSelectionHandlePosition { start, end }
|
||||
|
||||
/// Signature for reporting changes to the selection component of an
|
||||
/// [InputValue] for the purposes of a [TextSelectionOverlay]. The [caretRect]
|
||||
/// argument gives the location of the caret in the coordinate space of the
|
||||
/// [RenderBox] given by the [TextSelectionOverlay.renderObject].
|
||||
/// Signature for reporting changes to the selection component of a
|
||||
/// [TextEditingValue] for the purposes of a [TextSelectionOverlay]. The
|
||||
/// [caretRect] argument gives the location of the caret in the coordinate space
|
||||
/// of the [RenderBox] given by the [TextSelectionOverlay.renderObject].
|
||||
///
|
||||
/// 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
|
||||
/// of the toolbar widget.
|
||||
abstract class TextSelectionDelegate {
|
||||
/// Gets the current text input.
|
||||
InputValue get inputValue;
|
||||
TextEditingValue get textEditingValue;
|
||||
|
||||
/// Sets the current text input (replaces the whole line).
|
||||
set inputValue(InputValue value);
|
||||
set textEditingValue(TextEditingValue value);
|
||||
|
||||
/// Hides the text selection toolbar.
|
||||
void hideToolbar();
|
||||
@ -89,13 +89,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
///
|
||||
/// The [context] must not be null and must have an [Overlay] as an ancestor.
|
||||
TextSelectionOverlay({
|
||||
InputValue input,
|
||||
@required TextEditingValue value,
|
||||
@required this.context,
|
||||
this.debugRequiredFor,
|
||||
this.renderObject,
|
||||
this.onSelectionOverlayChanged,
|
||||
this.selectionControls,
|
||||
}): _input = input {
|
||||
}): _value = value {
|
||||
assert(value != null);
|
||||
assert(context != null);
|
||||
final OverlayState overlay = Overlay.of(context);
|
||||
assert(overlay != null);
|
||||
@ -133,7 +134,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
Animation<double> get _handleOpacity => _handleController.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
|
||||
/// second is hidden when the selection is collapsed.
|
||||
@ -142,7 +143,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
/// A copy/paste toolbar.
|
||||
OverlayEntry _toolbar;
|
||||
|
||||
TextSelection get _selection => _input.selection;
|
||||
TextSelection get _selection => _value.selection;
|
||||
|
||||
/// Shows the handles by inserting them into the [context]'s overlay.
|
||||
void showHandles() {
|
||||
@ -172,10 +173,10 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
/// 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
|
||||
/// next frame (i.e. many milliseconds later).
|
||||
void update(InputValue newInput) {
|
||||
if (_input == newInput)
|
||||
void update(TextEditingValue newValue) {
|
||||
if (_value == newValue)
|
||||
return;
|
||||
_input = newInput;
|
||||
_value = newValue;
|
||||
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
|
||||
SchedulerBinding.instance.addPostFrameCallback(_markNeedsBuild);
|
||||
} else {
|
||||
@ -259,13 +260,13 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
caretRect = renderObject.getLocalRectForCaret(newSelection.extent);
|
||||
break;
|
||||
}
|
||||
update(_input.copyWith(selection: newSelection, composing: TextRange.empty));
|
||||
update(_value.copyWith(selection: newSelection, composing: TextRange.empty));
|
||||
if (onSelectionOverlayChanged != null)
|
||||
onSelectionOverlayChanged(_input, caretRect);
|
||||
onSelectionOverlayChanged(_value, caretRect);
|
||||
}
|
||||
|
||||
void _handleSelectionHandleTapped() {
|
||||
if (inputValue.selection.isCollapsed) {
|
||||
if (_value.selection.isCollapsed) {
|
||||
if (_toolbar != null) {
|
||||
_toolbar?.remove();
|
||||
_toolbar = null;
|
||||
@ -276,14 +277,14 @@ class TextSelectionOverlay implements TextSelectionDelegate {
|
||||
}
|
||||
|
||||
@override
|
||||
InputValue get inputValue => _input;
|
||||
TextEditingValue get textEditingValue => _value;
|
||||
|
||||
@override
|
||||
set inputValue(InputValue value) {
|
||||
update(value);
|
||||
set textEditingValue(TextEditingValue newValue) {
|
||||
update(newValue);
|
||||
if (onSelectionOverlayChanged != null) {
|
||||
final Rect caretRect = renderObject.getLocalRectForCaret(value.selection.extent);
|
||||
onSelectionOverlayChanged(value, caretRect);
|
||||
final Rect caretRect = renderObject.getLocalRectForCaret(newValue.selection.extent);
|
||||
onSelectionOverlayChanged(newValue, caretRect);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,9 +43,9 @@ void main() {
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
home: new Material(
|
||||
child: new Center(
|
||||
child: new Input(focusNode: focusNode, autofocus: true)
|
||||
)
|
||||
)
|
||||
child: new TextField(focusNode: focusNode, autofocus: true),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(focusNode.hasFocus, isTrue);
|
||||
|
@ -12,7 +12,7 @@ import 'package:flutter/services.dart';
|
||||
|
||||
class MockClipboard {
|
||||
Object _clipboardData = <String, dynamic>{
|
||||
'text': null
|
||||
'text': null,
|
||||
};
|
||||
|
||||
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
|
||||
@ -30,9 +30,9 @@ Widget overlay(Widget child) {
|
||||
return new Overlay(
|
||||
initialEntries: <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);
|
||||
}
|
||||
|
||||
testWidgets('Editable text has consistent size', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
InputValue inputValue = InputValue.empty;
|
||||
testWidgets('TextField has consistent size', (WidgetTester tester) async {
|
||||
final Key textFieldKey = new UniqueKey();
|
||||
String textFieldValue;
|
||||
|
||||
Widget builder() {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
value: inputValue,
|
||||
key: inputKey,
|
||||
hintText: 'Placeholder',
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
child: new TextField(
|
||||
key: textFieldKey,
|
||||
decoration: new InputDecoration(
|
||||
hintText: 'Placeholder',
|
||||
),
|
||||
onChanged: (String value) {
|
||||
textFieldValue = value;
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Future<Null> checkText(String testValue) async {
|
||||
@ -103,31 +106,31 @@ void main() {
|
||||
await tester.idle();
|
||||
|
||||
// Check that the onChanged event handler fired.
|
||||
expect(inputValue.text, equals(testValue));
|
||||
expect(textFieldValue, equals(testValue));
|
||||
|
||||
return await tester.pumpWidget(builder());
|
||||
}
|
||||
|
||||
await checkText(' ');
|
||||
expect(findInputBox(), equals(inputBox));
|
||||
expect(findTextFieldBox(), equals(inputBox));
|
||||
expect(inputBox.size, equals(emptyInputSize));
|
||||
|
||||
await checkText('Test');
|
||||
expect(findInputBox(), equals(inputBox));
|
||||
expect(findTextFieldBox(), equals(inputBox));
|
||||
expect(inputBox.size, equals(emptyInputSize));
|
||||
});
|
||||
|
||||
testWidgets('Cursor blinks', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
|
||||
Widget builder() {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
key: inputKey,
|
||||
hintText: 'Placeholder'
|
||||
)
|
||||
)
|
||||
child: new TextField(
|
||||
decoration: new InputDecoration(
|
||||
hintText: 'Placeholder',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -163,17 +166,16 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('obscureText control test', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
|
||||
Widget builder() {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
key: inputKey,
|
||||
child: new TextField(
|
||||
obscureText: true,
|
||||
hintText: 'Placeholder'
|
||||
)
|
||||
)
|
||||
decoration: new InputDecoration(
|
||||
hintText: 'Placeholder',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -190,8 +192,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Can long press to select', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
InputValue inputValue = InputValue.empty;
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
|
||||
Widget builder() {
|
||||
return new Overlay(
|
||||
@ -200,16 +201,14 @@ void main() {
|
||||
builder: (BuildContext context) {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
value: inputValue,
|
||||
key: inputKey,
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
child: new TextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
]
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -218,11 +217,11 @@ void main() {
|
||||
final String testValue = 'abc def ghi';
|
||||
await tester.enterText(find.byType(EditableText), testValue);
|
||||
await tester.idle();
|
||||
expect(inputValue.text, testValue);
|
||||
expect(controller.value.text, testValue);
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
expect(inputValue.selection.isCollapsed, true);
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
|
||||
// Long press the 'e' to select 'def'.
|
||||
final Point ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||||
@ -232,32 +231,21 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
// 'def' is selected.
|
||||
expect(inputValue.selection.baseOffset, testValue.indexOf('d'));
|
||||
expect(inputValue.selection.extentOffset, testValue.indexOf('f')+1);
|
||||
expect(controller.selection.baseOffset, testValue.indexOf('d'));
|
||||
expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
|
||||
});
|
||||
|
||||
testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
InputValue inputValue = InputValue.empty;
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
|
||||
Widget builder() {
|
||||
return new Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
new OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
value: inputValue,
|
||||
key: inputKey,
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
return overlay(new Center(
|
||||
child: new Material(
|
||||
child: new TextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
@ -275,7 +263,7 @@ void main() {
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
final TextSelection selection = inputValue.selection;
|
||||
final TextSelection selection = controller.selection;
|
||||
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||
@ -294,8 +282,8 @@ void main() {
|
||||
await gesture.up();
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
expect(inputValue.selection.baseOffset, selection.baseOffset);
|
||||
expect(inputValue.selection.extentOffset, selection.extentOffset+2);
|
||||
expect(controller.selection.baseOffset, selection.baseOffset);
|
||||
expect(controller.selection.extentOffset, selection.extentOffset+2);
|
||||
|
||||
// Drag the left handle 2 letters to the left.
|
||||
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||||
@ -307,32 +295,21 @@ void main() {
|
||||
await gesture.up();
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
expect(inputValue.selection.baseOffset, selection.baseOffset-2);
|
||||
expect(inputValue.selection.extentOffset, selection.extentOffset+2);
|
||||
expect(controller.selection.baseOffset, selection.baseOffset-2);
|
||||
expect(controller.selection.extentOffset, selection.extentOffset+2);
|
||||
});
|
||||
|
||||
testWidgets('Can use selection toolbar', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
InputValue inputValue = InputValue.empty;
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
|
||||
Widget builder() {
|
||||
return new Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
new OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
value: inputValue,
|
||||
key: inputKey,
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
return overlay(new Center(
|
||||
child: new Material(
|
||||
child: new TextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
@ -347,57 +324,46 @@ void main() {
|
||||
await tester.pumpWidget(builder());
|
||||
RenderEditable renderEditable = findRenderEditable(tester);
|
||||
List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||
inputValue.selection);
|
||||
controller.selection);
|
||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
// SELECT ALL should select all the text.
|
||||
await tester.tap(find.text('SELECT ALL'));
|
||||
await tester.pumpWidget(builder());
|
||||
expect(inputValue.selection.baseOffset, 0);
|
||||
expect(inputValue.selection.extentOffset, testValue.length);
|
||||
expect(controller.selection.baseOffset, 0);
|
||||
expect(controller.selection.extentOffset, testValue.length);
|
||||
|
||||
// COPY should reset the selection.
|
||||
await tester.tap(find.text('COPY'));
|
||||
await tester.pumpWidget(builder());
|
||||
expect(inputValue.selection.isCollapsed, true);
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
|
||||
// Tap again to bring back the menu.
|
||||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||||
await tester.pumpWidget(builder());
|
||||
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.pumpWidget(builder());
|
||||
|
||||
// PASTE right before the 'e'.
|
||||
await tester.tap(find.text('PASTE'));
|
||||
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 {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
InputValue inputValue = InputValue.empty;
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
|
||||
Widget builder() {
|
||||
return new Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
new OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
value: inputValue,
|
||||
key: inputKey,
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
return overlay(new Center(
|
||||
child: new Material(
|
||||
child: new TextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
@ -412,7 +378,7 @@ void main() {
|
||||
await tester.pumpWidget(builder());
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||
inputValue.selection);
|
||||
controller.selection);
|
||||
await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0));
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
@ -432,27 +398,26 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
InputValue inputValue = InputValue.empty;
|
||||
final Key textFieldKey = new UniqueKey();
|
||||
|
||||
Widget builder(int maxLines) {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
value: inputValue,
|
||||
key: inputKey,
|
||||
child: new TextField(
|
||||
key: textFieldKey,
|
||||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||
maxLines: maxLines,
|
||||
hintText: 'Placeholder',
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
decoration: new InputDecoration(
|
||||
hintText: 'Placeholder',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder(3));
|
||||
|
||||
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));
|
||||
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||||
|
||||
final RenderBox inputBox = findInputBox();
|
||||
final Size emptyInputSize = inputBox.size;
|
||||
@ -487,29 +452,18 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
InputValue inputValue = InputValue.empty;
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
|
||||
Widget builder() {
|
||||
return new Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
new OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
value: inputValue,
|
||||
key: inputKey,
|
||||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||
maxLines: 3,
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
return overlay(new Center(
|
||||
child: new Material(
|
||||
child: new TextField(
|
||||
controller: controller,
|
||||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
@ -537,12 +491,12 @@ void main() {
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
expect(inputValue.selection.baseOffset, 76);
|
||||
expect(inputValue.selection.extentOffset, 81);
|
||||
expect(controller.selection.baseOffset, 76);
|
||||
expect(controller.selection.extentOffset, 81);
|
||||
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||
inputValue.selection);
|
||||
controller.selection);
|
||||
expect(endpoints.length, 2);
|
||||
|
||||
// Drag the right handle to the third line, just after 'Third'.
|
||||
@ -555,8 +509,8 @@ void main() {
|
||||
await gesture.up();
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
expect(inputValue.selection.baseOffset, 76);
|
||||
expect(inputValue.selection.extentOffset, 108);
|
||||
expect(controller.selection.baseOffset, 76);
|
||||
expect(controller.selection.extentOffset, 108);
|
||||
|
||||
// Drag the left handle to the first line, just after 'First'.
|
||||
handlePos = endpoints[0].point + const Offset(-1.0, 1.0);
|
||||
@ -568,39 +522,30 @@ void main() {
|
||||
await gesture.up();
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
expect(inputValue.selection.baseOffset, 5);
|
||||
expect(inputValue.selection.extentOffset, 108);
|
||||
expect(controller.selection.baseOffset, 5);
|
||||
expect(controller.selection.extentOffset, 108);
|
||||
|
||||
await tester.tap(find.text('CUT'));
|
||||
await tester.pumpWidget(builder());
|
||||
expect(inputValue.selection.isCollapsed, true);
|
||||
expect(inputValue.text, cutValue);
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
expect(controller.text, cutValue);
|
||||
}, skip: Platform.isMacOS); // Skip due to https://github.com/flutter/flutter/issues/6961
|
||||
|
||||
testWidgets('Can scroll multiline input', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
InputValue inputValue = InputValue.empty;
|
||||
final Key textFieldKey = new UniqueKey();
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
|
||||
Widget builder() {
|
||||
return new Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
new OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
value: inputValue,
|
||||
key: inputKey,
|
||||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||
maxLines: 2,
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
return overlay(new Center(
|
||||
child: new Material(
|
||||
child: new TextField(
|
||||
key: textFieldKey,
|
||||
controller: controller,
|
||||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
@ -610,7 +555,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
RenderBox findInputBox() => tester.renderObject(find.byKey(inputKey));
|
||||
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||||
final RenderBox inputBox = findInputBox();
|
||||
|
||||
// Check that the last line of text is not displayed.
|
||||
@ -652,7 +597,7 @@ void main() {
|
||||
|
||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||
final List<TextSelectionPoint> endpoints = renderEditable.getEndpointsForSelection(
|
||||
inputValue.selection);
|
||||
controller.selection);
|
||||
expect(endpoints.length, 2);
|
||||
|
||||
// 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
|
||||
|
||||
testWidgets('InputField smoke test', (WidgetTester tester) async {
|
||||
InputValue inputValue = InputValue.empty;
|
||||
String textFieldValue;
|
||||
|
||||
Widget builder() {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new InputField(
|
||||
value: inputValue,
|
||||
hintText: 'Placeholder',
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
child: new TextField(
|
||||
decoration: null,
|
||||
onChanged: (String value) {
|
||||
textFieldValue = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -696,7 +642,7 @@ void main() {
|
||||
await tester.idle();
|
||||
|
||||
// Check that the onChanged event handler fired.
|
||||
expect(inputValue.text, equals(testValue));
|
||||
expect(textFieldValue, equals(testValue));
|
||||
|
||||
return await tester.pumpWidget(builder());
|
||||
}
|
||||
@ -705,19 +651,20 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('InputField with global key', (WidgetTester tester) async {
|
||||
final GlobalKey inputFieldKey = new GlobalKey(debugLabel: 'inputFieldKey');
|
||||
InputValue inputValue = InputValue.empty;
|
||||
final GlobalKey textFieldKey = new GlobalKey(debugLabel: 'textFieldKey');
|
||||
String textFieldValue;
|
||||
|
||||
Widget builder() {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new InputField(
|
||||
key: inputFieldKey,
|
||||
value: inputValue,
|
||||
hintText: 'Placeholder',
|
||||
onChanged: (InputValue value) { inputValue = value; }
|
||||
)
|
||||
)
|
||||
child: new TextField(
|
||||
key: textFieldKey,
|
||||
decoration: new InputDecoration(
|
||||
hintText: 'Placeholder',
|
||||
),
|
||||
onChanged: (String value) { textFieldValue = value; },
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -728,7 +675,7 @@ void main() {
|
||||
await tester.idle();
|
||||
|
||||
// Check that the onChanged event handler fired.
|
||||
expect(inputValue.text, equals(testValue));
|
||||
expect(textFieldValue, equals(testValue));
|
||||
|
||||
return await tester.pumpWidget(builder());
|
||||
}
|
||||
@ -736,9 +683,8 @@ void main() {
|
||||
checkText('Hello World');
|
||||
});
|
||||
|
||||
testWidgets('InputField with default hintStyle', (WidgetTester tester) async {
|
||||
final InputValue inputValue = InputValue.empty;
|
||||
final TextStyle textStyle = new TextStyle(
|
||||
testWidgets('TextField with default hintStyle', (WidgetTester tester) async {
|
||||
final TextStyle style = new TextStyle(
|
||||
color: Colors.pink[500],
|
||||
fontSize: 10.0,
|
||||
);
|
||||
@ -751,10 +697,11 @@ void main() {
|
||||
child: new Theme(
|
||||
data: themeData,
|
||||
child: new Material(
|
||||
child: new InputField(
|
||||
value: inputValue,
|
||||
hintText: 'Placeholder',
|
||||
style: textStyle,
|
||||
child: new TextField(
|
||||
decoration: new InputDecoration(
|
||||
hintText: 'Placeholder',
|
||||
),
|
||||
style: style,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -765,11 +712,10 @@ void main() {
|
||||
|
||||
final Text hintText = tester.widget(find.text('Placeholder'));
|
||||
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 {
|
||||
final InputValue inputValue = InputValue.empty;
|
||||
testWidgets('TextField with specified hintStyle', (WidgetTester tester) async {
|
||||
final TextStyle hintStyle = new TextStyle(
|
||||
color: Colors.pink[500],
|
||||
fontSize: 10.0,
|
||||
@ -778,10 +724,11 @@ void main() {
|
||||
Widget builder() {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new InputField(
|
||||
value: inputValue,
|
||||
hintText: 'Placeholder',
|
||||
hintStyle: hintStyle,
|
||||
child: new TextField(
|
||||
decoration: new InputDecoration(
|
||||
hintText: 'Placeholder',
|
||||
hintStyle: hintStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -793,20 +740,24 @@ void main() {
|
||||
expect(hintText.style, hintStyle);
|
||||
});
|
||||
|
||||
testWidgets('Input label text animates', (WidgetTester tester) async {
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
testWidgets('TextField label text animates', (WidgetTester tester) async {
|
||||
final Key secondKey = new UniqueKey();
|
||||
|
||||
Widget innerBuilder() {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Column(
|
||||
children: <Widget>[
|
||||
new Input(
|
||||
labelText: 'First',
|
||||
new TextField(
|
||||
decoration: new InputDecoration(
|
||||
labelText: 'First',
|
||||
),
|
||||
),
|
||||
new Input(
|
||||
key: inputKey,
|
||||
labelText: 'Second',
|
||||
new TextField(
|
||||
key: secondKey,
|
||||
decoration: new InputDecoration(
|
||||
labelText: 'Second',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -820,7 +771,7 @@ void main() {
|
||||
Point pos = tester.getTopLeft(find.text('Second'));
|
||||
|
||||
// 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.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
@ -839,10 +790,11 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
new Center(
|
||||
child: new Material(
|
||||
child: new Input(
|
||||
icon: new Icon(Icons.phone),
|
||||
labelText: 'label',
|
||||
value: InputValue.empty,
|
||||
child: new TextField(
|
||||
decoration: new InputDecoration(
|
||||
icon: new Icon(Icons.phone),
|
||||
labelText: 'label',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -850,6 +802,6 @@ void main() {
|
||||
|
||||
final double iconRight = tester.getTopRight(find.byType(Icon)).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 Form(
|
||||
key: formKey,
|
||||
child: new TextField(
|
||||
onSaved: (InputValue value) { fieldValue = value.text; },
|
||||
child: new TextFormField(
|
||||
onSaved: (String value) { fieldValue = value; },
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -47,10 +47,10 @@ void main() {
|
||||
child: new Material(
|
||||
child: new Form(
|
||||
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 {
|
||||
final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
|
||||
final GlobalKey inputKey = new GlobalKey();
|
||||
String errorText(InputValue input) => input.text + '/error';
|
||||
String errorText(String value) => value + '/error';
|
||||
|
||||
Widget builder(bool autovalidate) {
|
||||
return new Center(
|
||||
@ -80,12 +79,11 @@ void main() {
|
||||
child: new Form(
|
||||
key: formKey,
|
||||
autovalidate: autovalidate,
|
||||
child: new TextField(
|
||||
key: inputKey,
|
||||
child: new TextFormField(
|
||||
validator: errorText,
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -99,10 +97,10 @@ void main() {
|
||||
await tester.pumpWidget(builder(false));
|
||||
|
||||
// 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();
|
||||
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.
|
||||
formKey.currentState.reset();
|
||||
@ -110,18 +108,18 @@ void main() {
|
||||
await tester.idle();
|
||||
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('');
|
||||
});
|
||||
|
||||
testWidgets('Multiple Inputs communicate', (WidgetTester tester) async {
|
||||
testWidgets('Multiple TextFormFields communicate', (WidgetTester tester) async {
|
||||
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.
|
||||
String errorText(InputValue input) => fieldKey.currentState.value?.text.toString() + '/error';
|
||||
String errorText(String input) => fieldKey.currentState.value?.toString() + '/error';
|
||||
|
||||
Widget builder() {
|
||||
return new Center(
|
||||
@ -131,10 +129,10 @@ void main() {
|
||||
autovalidate: true,
|
||||
child: new ListView(
|
||||
children: <Widget>[
|
||||
new TextField(
|
||||
new TextFormField(
|
||||
key: fieldKey,
|
||||
),
|
||||
new TextField(
|
||||
new TextFormField(
|
||||
validator: errorText,
|
||||
),
|
||||
],
|
||||
@ -162,18 +160,19 @@ void main() {
|
||||
|
||||
testWidgets('Provide initial value to input', (WidgetTester tester) async {
|
||||
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() {
|
||||
return new Center(
|
||||
child: new Material(
|
||||
child: new Form(
|
||||
child: new TextField(
|
||||
child: new TextFormField(
|
||||
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
|
||||
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
|
||||
expect(inputKey.currentState.value.text, equals(initialValue));
|
||||
expect(inputKey.currentState.value, equals(initialValue));
|
||||
await tester.enterText(find.byType(EditableText), 'world');
|
||||
await tester.idle();
|
||||
await tester.pump();
|
||||
expect(inputKey.currentState.value.text, equals('world'));
|
||||
expect(editableText.config.value.text, equals('world'));
|
||||
expect(inputKey.currentState.value, 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 fieldKey = new GlobalKey();
|
||||
String fieldValue;
|
||||
|
||||
Widget builder(bool remove) {
|
||||
@ -207,14 +205,13 @@ void main() {
|
||||
child: new Material(
|
||||
child: new Form(
|
||||
key: formKey,
|
||||
child: remove ? new Container() : new TextField(
|
||||
key: fieldKey,
|
||||
child: remove ? new Container() : new TextFormField(
|
||||
autofocus: true,
|
||||
onSaved: (InputValue value) { fieldValue = value.text; },
|
||||
validator: (InputValue value) { return value.text.isEmpty ? null : 'yes'; }
|
||||
onSaved: (String value) { fieldValue = value; },
|
||||
validator: (String value) { return value.isEmpty ? null : 'yes'; }
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -321,7 +321,7 @@ class FlutterDriver {
|
||||
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
|
||||
/// the provided [text].
|
||||
@ -330,10 +330,10 @@ class FlutterDriver {
|
||||
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
|
||||
/// the returns the submitted text value.
|
||||
/// This command invokes the `onSubmitted` handler of the [TextField] widget
|
||||
/// and the returns the submitted text value.
|
||||
Future<String> submitInputText(SerializableFinder finder, {Duration timeout}) async {
|
||||
final Map<String, dynamic> json = await _sendCommand(new SubmitInputText(finder, timeout: timeout));
|
||||
return json['text'];
|
||||
|
@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart' show RendererBinding;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -262,20 +263,29 @@ class FlutterDriverExtension {
|
||||
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 {
|
||||
final SetInputText setInputTextCommand = command;
|
||||
final Finder target = await _waitForElement(_createFinder(setInputTextCommand.finder));
|
||||
final Input input = target.evaluate().single.widget;
|
||||
input.onChanged(new InputValue(text: setInputTextCommand.text));
|
||||
final Finder target = await _waitForElement(_findEditableText(setInputTextCommand.finder));
|
||||
final EditableTextState editable = _getEditableTextState(target);
|
||||
editable.updateEditingValue(new TextEditingValue(text: setInputTextCommand.text));
|
||||
return new SetInputTextResult();
|
||||
}
|
||||
|
||||
Future<SubmitInputTextResult> _submitInputText(Command command) async {
|
||||
final SubmitInputText submitInputTextCommand = command;
|
||||
final Finder target = await _waitForElement(_createFinder(submitInputTextCommand.finder));
|
||||
final Input input = target.evaluate().single.widget;
|
||||
input.onSubmitted(input.value);
|
||||
return new SubmitInputTextResult(input.value.text);
|
||||
final Finder target = await _waitForElement(_findEditableText(submitInputTextCommand.finder));
|
||||
final EditableTextState editable = _getEditableTextState(target);
|
||||
editable.performAction(TextInputAction.done);
|
||||
return new SubmitInputTextResult(editable.config.controller.value.text);
|
||||
}
|
||||
|
||||
Future<GetTextResult> _getText(Command command) async {
|
||||
|
Loading…
Reference in New Issue
Block a user