Commit 74dd0a3a authored by Hans Muller's avatar Hans Muller Committed by GitHub

Input refactoring: added an InputField widget (#6733)

parent fb3bf7a9
...@@ -16,6 +16,125 @@ import 'theme.dart'; ...@@ -16,6 +16,125 @@ import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType; export 'package:flutter/services.dart' show TextInputType;
class InputField extends StatefulWidget {
InputField({
Key key,
this.focusKey,
this.value,
this.keyboardType: TextInputType.text,
this.hintText,
this.style,
this.hideText: false,
this.maxLines: 1,
this.onChanged,
this.onSubmitted,
}) : super(key: key);
final GlobalKey focusKey;
/// 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;
/// 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 (•).
final bool hideText;
/// 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
_InputFieldState createState() => new _InputFieldState();
}
class _InputFieldState extends State<InputField> {
GlobalKey<RawInputState> _rawInputKey = new GlobalKey<RawInputState>();
GlobalKey<RawInputState> _focusKey = new GlobalKey(debugLabel: "_InputFieldState _focusKey");
GlobalKey get focusKey => config.focusKey ?? (config.key is GlobalKey ? config.key : _focusKey);
void requestKeyboard() {
_rawInputKey.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(
key: focusKey == _focusKey ? _focusKey : null,
behavior: HitTestBehavior.opaque,
onTap: () {
requestKeyboard();
},
// Since the focusKey may have been created here, defer building the
// RawInput until the focusKey's context has been set. This is necessary
// because the RawInput will check the focus, like Focus.at(focusContext),
// when it builds.
child: new Builder(
builder: (BuildContext context) {
return new RawInput(
key: _rawInputKey,
value: value,
focusKey: focusKey,
style: textStyle,
hideText: config.hideText,
maxLines: config.maxLines,
cursorColor: themeData.textSelectionColor,
selectionColor: themeData.textSelectionColor,
selectionControls: materialTextSelectionControls,
platform: Theme.of(context).platform,
keyboardType: config.keyboardType,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
);
}
),
),
];
if (config.hintText != null && value.text.isEmpty) {
TextStyle hintStyle = themeData.textTheme.subhead.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));
}
}
/// A material design text input field. /// A material design text input field.
/// ///
/// Requires one of its ancestors to be a [Material] widget. /// Requires one of its ancestors to be a [Material] widget.
...@@ -116,9 +235,10 @@ const Duration _kTransitionDuration = const Duration(milliseconds: 200); ...@@ -116,9 +235,10 @@ const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn; const Curve _kTransitionCurve = Curves.fastOutSlowIn;
class _InputState extends State<Input> { class _InputState extends State<Input> {
GlobalKey<RawInputState> _rawInputKey = new GlobalKey<RawInputState>(); GlobalKey<_InputFieldState> _inputFieldKey = new GlobalKey<_InputFieldState>(debugLabel: '_InputState _inputFieldKey');
GlobalKey<_InputFieldState> _focusKey = new GlobalKey(debugLabel: '_InputState _focusKey');
GlobalKey get focusKey => config.key is GlobalKey ? config.key : _rawInputKey; GlobalKey get focusKey => config.key is GlobalKey ? config.key : _focusKey;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -145,38 +265,35 @@ class _InputState extends State<Input> { ...@@ -145,38 +265,35 @@ class _InputState extends State<Input> {
List<Widget> stackChildren = <Widget>[]; List<Widget> stackChildren = <Widget>[];
bool hasInlineLabel = config.labelText != null && !focused && !value.text.isNotEmpty; final bool hasInlineLabel = config.labelText != null && !focused && !value.text.isNotEmpty;
if (config.labelText != null) { if (config.labelText != null) {
TextStyle labelStyle = hasInlineLabel ? final TextStyle labelStyle = hasInlineLabel ?
themeData.textTheme.subhead.copyWith(color: themeData.hintColor) : themeData.textTheme.subhead.copyWith(color: themeData.hintColor) :
themeData.textTheme.caption.copyWith(color: activeColor); themeData.textTheme.caption.copyWith(color: activeColor);
double topPaddingIncrement = themeData.textTheme.caption.fontSize + (config.isDense ? 4.0 : 8.0); final double topPaddingIncrement = themeData.textTheme.caption.fontSize + (config.isDense ? 4.0 : 8.0);
double top = topPadding; double top = topPadding;
if (hasInlineLabel) if (hasInlineLabel)
top += topPaddingIncrement + textStyle.fontSize - labelStyle.fontSize; top += topPaddingIncrement + textStyle.fontSize - labelStyle.fontSize;
stackChildren.add(new AnimatedPositioned( stackChildren.add(
left: 0.0, new AnimatedPositioned(
top: top, left: 0.0,
duration: _kTransitionDuration, top: top,
curve: _kTransitionCurve, duration: _kTransitionDuration,
child: new Text(config.labelText, style: labelStyle) curve: _kTransitionCurve,
)); child: new AnimatedOpacity(
opacity: focused ? 1.0 : 0.0,
curve: _kTransitionCurve,
duration: _kTransitionDuration,
child: new Text(config.labelText, style: labelStyle)
),
),
);
topPadding += topPaddingIncrement; topPadding += topPaddingIncrement;
} }
if (config.hintText != null && value.text.isEmpty && !hasInlineLabel) {
TextStyle hintStyle = themeData.textTheme.subhead.copyWith(color: themeData.hintColor);
stackChildren.add(new Positioned(
left: 0.0,
top: topPadding + textStyle.fontSize - hintStyle.fontSize,
child: new Text(config.hintText, style: hintStyle)
));
}
Color borderColor = activeColor; Color borderColor = activeColor;
double bottomPadding = 8.0; double bottomPadding = 8.0;
double bottomBorder = focused ? 2.0 : 1.0; double bottomBorder = focused ? 2.0 : 1.0;
...@@ -206,21 +323,26 @@ class _InputState extends State<Input> { ...@@ -206,21 +323,26 @@ class _InputState extends State<Input> {
decoration: new BoxDecoration( decoration: new BoxDecoration(
border: border, border: border,
), ),
child: new RawInput( // Since the focusKey may have been created here, defer building the
key: _rawInputKey, // InputField until the focusKey's context has been set. This is necessary
value: value, // because our descendants may check the focus, like Focus.at(focusContext),
focusKey: focusKey, // when they build.
style: textStyle, child: new Builder(
hideText: config.hideText, builder: (BuildContext context) {
maxLines: config.maxLines, return new InputField(
cursorColor: themeData.textSelectionColor, key: _inputFieldKey,
selectionColor: themeData.textSelectionColor, focusKey: focusKey,
selectionControls: materialTextSelectionControls, value: value,
platform: Theme.of(context).platform, style: textStyle,
keyboardType: config.keyboardType, hideText: config.hideText,
onChanged: config.onChanged, maxLines: config.maxLines,
onSubmitted: config.onSubmitted, keyboardType: config.keyboardType,
) hintText: config.hintText,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
);
}
),
)); ));
if (errorText != null && !config.isDense) { if (errorText != null && !config.isDense) {
...@@ -257,12 +379,13 @@ class _InputState extends State<Input> { ...@@ -257,12 +379,13 @@ class _InputState extends State<Input> {
); );
} }
return new RepaintBoundary( return new GestureDetector(
child: new GestureDetector( key: config.key is GlobalKey ? null : focusKey,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => _rawInputKey.currentState?.requestKeyboard(), onTap: () {
child: child _inputFieldKey.currentState?.requestKeyboard();
) },
child: child,
); );
} }
} }
......
...@@ -24,8 +24,8 @@ void main() { ...@@ -24,8 +24,8 @@ void main() {
child: new Form( child: new Form(
key: formKey, key: formKey,
child: new InputFormField( child: new InputFormField(
onSaved: (InputValue value) { fieldValue = value.text; } onSaved: (InputValue value) { fieldValue = value.text; },
) ),
) )
) )
); );
...@@ -57,8 +57,8 @@ void main() { ...@@ -57,8 +57,8 @@ void main() {
child: new Form( child: new Form(
child: new InputFormField( child: new InputFormField(
key: inputKey, key: inputKey,
validator: errorText validator: errorText,
) ),
) )
) )
); );
...@@ -101,11 +101,11 @@ void main() { ...@@ -101,11 +101,11 @@ void main() {
focusKey: inputFocusKey, focusKey: inputFocusKey,
), ),
new InputFormField( new InputFormField(
validator: errorText validator: errorText,
) ),
] ]
) )
) ),
) )
) )
); );
...@@ -140,7 +140,7 @@ void main() { ...@@ -140,7 +140,7 @@ void main() {
child: new InputFormField( child: new InputFormField(
key: inputKey, key: inputKey,
initialValue: new InputValue(text: initialValue), initialValue: new InputValue(text: initialValue),
) ),
) )
) )
); );
...@@ -175,14 +175,12 @@ void main() { ...@@ -175,14 +175,12 @@ void main() {
child: new Material( child: new Material(
child: new Form( child: new Form(
key: formKey, key: formKey,
child: remove ? child: remove ? new Container() : new InputFormField(
new Container() : key: fieldKey,
new InputFormField( autofocus: true,
key: fieldKey, onSaved: (InputValue value) { fieldValue = value.text; },
autofocus: true, validator: (InputValue value) { return value.text.isEmpty ? null : 'yes'; }
onSaved: (InputValue value) { fieldValue = value.text; }, ),
validator: (InputValue value) { return value.text.isEmpty ? null : 'yes'; }
)
) )
) )
); );
......
...@@ -673,4 +673,66 @@ void main() { ...@@ -673,4 +673,66 @@ void main() {
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFirstPos)), isTrue);
expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); expect(inputBox.hitTest(new HitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse);
}); });
testWidgets('InputField smoke test', (WidgetTester tester) async {
InputValue inputValue = InputValue.empty;
Widget builder() {
return new Center(
child: new Material(
child: new InputField(
value: inputValue,
hintText: 'Placeholder',
onChanged: (InputValue value) { inputValue = value; }
)
)
);
}
await tester.pumpWidget(builder());
Future<Null> checkText(String testValue) async {
enterText(testValue);
await tester.idle();
// Check that the onChanged event handler fired.
expect(inputValue.text, equals(testValue));
return await tester.pumpWidget(builder());
}
checkText('Hello World');
});
testWidgets('InputField with global key', (WidgetTester tester) async {
GlobalKey inputFieldKey = new GlobalKey(debugLabel: 'inputFieldKey');
InputValue inputValue = InputValue.empty;
Widget builder() {
return new Center(
child: new Material(
child: new InputField(
key: inputFieldKey,
value: inputValue,
hintText: 'Placeholder',
onChanged: (InputValue value) { inputValue = value; }
)
)
);
}
await tester.pumpWidget(builder());
Future<Null> checkText(String testValue) async {
enterText(testValue);
await tester.idle();
// Check that the onChanged event handler fired.
expect(inputValue.text, equals(testValue));
return await tester.pumpWidget(builder());
}
checkText('Hello World');
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment