Commit a7360d24 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Factor out Input's layout: add InputContainer etc (#6881)

parent 7c56f00f
...@@ -10,12 +10,31 @@ import 'debug.dart'; ...@@ -10,12 +10,31 @@ import 'debug.dart';
import 'icon.dart'; import 'icon.dart';
import 'icon_theme.dart'; import 'icon_theme.dart';
import 'icon_theme_data.dart'; import 'icon_theme_data.dart';
import 'material.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'theme.dart'; import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType; export 'package:flutter/services.dart' show TextInputType;
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
/// A simple text input field.
///
/// 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.
class InputField extends StatefulWidget { class InputField extends StatefulWidget {
InputField({ InputField({
Key key, Key key,
...@@ -119,7 +138,8 @@ class _InputFieldState extends State<InputField> { ...@@ -119,7 +138,8 @@ class _InputFieldState extends State<InputField> {
]; ];
if (config.hintText != null && value.text.isEmpty) { if (config.hintText != null && value.text.isEmpty) {
TextStyle hintStyle = themeData.textTheme.subhead.copyWith(color: themeData.hintColor);
TextStyle hintStyle = textStyle.copyWith(color: themeData.hintColor);
stackChildren.add( stackChildren.add(
new Positioned( new Positioned(
left: 0.0, left: 0.0,
...@@ -135,52 +155,31 @@ class _InputFieldState extends State<InputField> { ...@@ -135,52 +155,31 @@ class _InputFieldState extends State<InputField> {
} }
} }
/// A material design text input field. /// Displays the visual elements of a material design text field around an
/// arbitrary child widget.
/// ///
/// Requires one of its ancestors to be a [Material] widget. /// Use InputContainer to create widgets that look and behave like the [Input]
/// widget.
/// ///
/// The [value] field must be updated each time the [onChanged] callback is /// Requires one of its ancestors to be a [Material] widget.
/// invoked. Be sure to include the full [value] provided by the [onChanged]
/// callback, or information like the current selection will be lost.
/// ///
/// See also: /// See also:
/// ///
/// * <https://material.google.com/components/text-fields.html> /// * [Input], which combines an [InputContainer] with an [InputField].
/// class InputContainer extends StatefulWidget {
/// For a detailed guide on using the input widget, see: InputContainer({
///
/// * <https://flutter.io/text-input/>
class Input extends StatefulWidget {
/// Creates a text input field.
///
/// By default, the input uses a keyboard appropriate for text entry.
//
// Note: If you change this constructor signature, please also update
// InputFormField below.
Input({
Key key, Key key,
this.value, this.focused: false,
this.keyboardType: TextInputType.text, this.isEmpty: false,
this.icon, this.icon,
this.labelText, this.labelText,
this.hintText, this.hintText,
this.errorText, this.errorText,
this.style, this.style,
this.hideText: false,
this.isDense: false, this.isDense: false,
this.autofocus: false, this.child,
this.maxLines: 1,
this.onChanged,
this.onSubmitted,
}) : super(key: key); }) : 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;
/// The type of keyboard to use for editing the text.
final TextInputType keyboardType;
/// An icon to show adjacent to the input field. /// An icon to show adjacent to the input field.
/// ///
/// The size and color of the icon is configured automatically using an /// The size and color of the icon is configured automatically using an
...@@ -190,68 +189,46 @@ class Input extends StatefulWidget { ...@@ -190,68 +189,46 @@ class Input extends StatefulWidget {
/// See [Icon], [ImageIcon]. /// See [Icon], [ImageIcon].
final Widget icon; final Widget icon;
/// Text to show above the input field. /// Text that appears above the child or over it, if isEmpty is true.
final String labelText; final String labelText;
/// Text to show inline in the input field when it would otherwise be empty. /// Text that appears over the child if isEmpty is true and labelText is null.
final String hintText; final String hintText;
/// Text to show when the input text is invalid. /// Text that appears below the child. If errorText is non-null the divider
/// that appears below the child is red.
final String errorText; final String errorText;
/// The style to use for the text being edited. /// The style to use for the hint. It's also used for the label when the label
/// appears over the child.
final TextStyle style; final TextStyle style;
/// Whether to hide the text being edited (e.g., for passwords). /// Whether the input container is part of a dense form (i.e., uses less vertical space).
///
/// When this is set to true, all the characters in the input are replaced by
/// U+2022 BULLET characters (•).
final bool hideText;
/// Whether the input field is part of a dense form (i.e., uses less vertical space).
final bool isDense; final bool isDense;
/// Whether this input field should focus itself if nothing else is already focused. /// True if the hint and label should be displayed as if the child had the focus.
final bool autofocus; final bool focused;
/// The maximum number of lines for the text to span, wrapping if necessary. /// Should the hint and label be displayed as if no value had been input
/// If this is 1 (the default), the text will not wrap, but will scroll /// to the child.
/// horizontally instead. final bool isEmpty;
final int maxLines;
/// Called when the text being edited changes. final Widget child;
///
/// 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 @override
_InputState createState() => new _InputState(); _InputContainerState createState() => new _InputContainerState();
} }
const Duration _kTransitionDuration = const Duration(milliseconds: 200); class _InputContainerState extends State<InputContainer> {
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
class _InputState extends State<Input> {
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 : _focusKey;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context); ThemeData themeData = Theme.of(context);
BuildContext focusContext = focusKey.currentContext;
bool focused = focusContext != null && Focus.at(focusContext, autofocus: config.autofocus);
InputValue value = config.value ?? InputValue.empty;
String errorText = config.errorText; String errorText = config.errorText;
TextStyle textStyle = config.style ?? themeData.textTheme.subhead; final TextStyle textStyle = config.style ?? themeData.textTheme.subhead;
Color activeColor = themeData.hintColor; Color activeColor = themeData.hintColor;
if (focused) { if (config.focused) {
switch (themeData.brightness) { switch (themeData.brightness) {
case Brightness.dark: case Brightness.dark:
activeColor = themeData.accentColor; activeColor = themeData.accentColor;
...@@ -265,10 +242,14 @@ class _InputState extends State<Input> { ...@@ -265,10 +242,14 @@ class _InputState extends State<Input> {
List<Widget> stackChildren = <Widget>[]; List<Widget> stackChildren = <Widget>[];
final bool hasInlineLabel = config.labelText != null && !focused && !value.text.isNotEmpty; // 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) { if (config.labelText != null) {
final TextStyle labelStyle = hasInlineLabel ? final TextStyle labelStyle = hasInlineLabel ?
themeData.textTheme.subhead.copyWith(color: themeData.hintColor) : textStyle.copyWith(color: themeData.hintColor) :
themeData.textTheme.caption.copyWith(color: activeColor); themeData.textTheme.caption.copyWith(color: activeColor);
final 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);
...@@ -282,21 +263,29 @@ class _InputState extends State<Input> { ...@@ -282,21 +263,29 @@ class _InputState extends State<Input> {
top: top, top: top,
duration: _kTransitionDuration, duration: _kTransitionDuration,
curve: _kTransitionCurve, curve: _kTransitionCurve,
child: new AnimatedOpacity( child: new Text(config.labelText, style: labelStyle),
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 && config.isEmpty && !hasInlineLabel) {
TextStyle hintStyle = textStyle.copyWith(color: themeData.hintColor);
stackChildren.add(
new Positioned(
left: 0.0,
top: topPadding + textStyle.fontSize - hintStyle.fontSize,
child: new IgnorePointer(
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 = config.focused ? 2.0 : 1.0;
double bottomHeight = config.isDense ? 14.0 : 18.0; double bottomHeight = config.isDense ? 14.0 : 18.0;
if (errorText != null) { if (errorText != null) {
...@@ -323,26 +312,7 @@ class _InputState extends State<Input> { ...@@ -323,26 +312,7 @@ class _InputState extends State<Input> {
decoration: new BoxDecoration( decoration: new BoxDecoration(
border: border, border: border,
), ),
// Since the focusKey may have been created here, defer building the child: config.child,
// InputField until the focusKey's context has been set. This is necessary
// because our descendants may check the focus, like Focus.at(focusContext),
// when they build.
child: new Builder(
builder: (BuildContext context) {
return new InputField(
key: _inputFieldKey,
focusKey: focusKey,
value: value,
style: textStyle,
hideText: config.hideText,
maxLines: config.maxLines,
keyboardType: config.keyboardType,
hintText: config.hintText,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
);
}
),
)); ));
if (errorText != null && !config.isDense) { if (errorText != null && !config.isDense) {
...@@ -354,12 +324,12 @@ class _InputState extends State<Input> { ...@@ -354,12 +324,12 @@ class _InputState extends State<Input> {
)); ));
} }
Widget child = new Stack(children: stackChildren); Widget textField = new Stack(children: stackChildren);
if (config.icon != null) { if (config.icon != null) {
double iconSize = config.isDense ? 18.0 : 24.0; double iconSize = config.isDense ? 18.0 : 24.0;
double iconTop = topPadding + (textStyle.fontSize - iconSize) / 2.0; double iconTop = topPadding + (textStyle.fontSize - iconSize) / 2.0;
child = new Row( textField = new Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
new Container( new Container(
...@@ -368,24 +338,160 @@ class _InputState extends State<Input> { ...@@ -368,24 +338,160 @@ class _InputState extends State<Input> {
child: new IconTheme.merge( child: new IconTheme.merge(
context: context, context: context,
data: new IconThemeData( data: new IconThemeData(
color: focused ? activeColor : Colors.black45, color: config.focused ? activeColor : Colors.black45,
size: config.isDense ? 18.0 : 24.0 size: config.isDense ? 18.0 : 24.0
), ),
child: config.icon child: config.icon
) )
), ),
new Flexible(child: child) new Flexible(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.
///
/// See also:
///
/// * <https://material.google.com/components/text-fields.html>
///
/// For a detailed guide on using the input widget, see:
///
/// * <https://flutter.io/text-input/>
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, InputFormField, InputField.
Input({
Key key,
this.value,
this.keyboardType: TextInputType.text,
this.icon,
this.labelText,
this.hintText,
this.errorText,
this.style,
this.hideText: false,
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;
/// 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 (•).
final bool hideText;
/// Whether the input field is part of a dense form (i.e., uses less vertical space).
final bool isDense;
/// Whether this input field should focus itself if nothing else is already focused.
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>();
final GlobalKey _focusKey = new GlobalKey();
GlobalKey get focusKey => config.key is GlobalKey ? config.key : _focusKey;
@override
Widget build(BuildContext context) {
return new GestureDetector( return new GestureDetector(
key: config.key is GlobalKey ? null : focusKey, key: focusKey == _focusKey ? _focusKey : null,
behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
_inputFieldKey.currentState?.requestKeyboard(); _inputFieldKey.currentState?.requestKeyboard();
}, },
child: child, // Since the focusKey may have been created here, defer building the
// InputContainer until the focusKey's context has been set. This is
// necessary because we're passing the value of Focus.at() along.
child: new Builder(
builder: (BuildContext context) {
final bool focused = Focus.at(focusKey.currentContext, autofocus: config.autofocus);
final bool isEmpty = (config.value ?? InputValue.empty).text.isEmpty;
return new InputContainer(
focused: focused,
isEmpty: isEmpty,
icon: config.icon,
labelText: config.labelText,
hintText: config.hintText,
errorText: config.errorText,
style: config.style,
isDense: config.isDense,
child: new InputField(
key: _inputFieldKey,
focusKey: focusKey,
value: config.value,
style: config.style,
hideText: config.hideText,
maxLines: config.maxLines,
keyboardType: config.keyboardType,
onChanged: config.onChanged,
onSubmitted: config.onSubmitted,
),
);
},
),
); );
} }
} }
......
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