mirror of
https://github.com/flutter/flutter
synced 2024-10-14 04:02:56 +00:00
Add material design features to Input
Properly support labels, hints, icons, and custom typography.
This commit is contained in:
parent
b240cda819
commit
432bfb4729
|
@ -4,79 +4,41 @@
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class Field extends StatelessComponent {
|
final GlobalKey _kNameKey = new GlobalKey(debugLabel: 'name field');
|
||||||
Field({
|
final GlobalKey _kPhoneKey = new GlobalKey(debugLabel: 'phone field');
|
||||||
Key key,
|
final GlobalKey _kEmailKey = new GlobalKey(debugLabel: 'email field');
|
||||||
this.inputKey,
|
final GlobalKey _kAddressKey = new GlobalKey(debugLabel: 'address field');
|
||||||
this.icon,
|
final GlobalKey _kRingtoneKey = new GlobalKey(debugLabel: 'ringtone field');
|
||||||
this.placeholder
|
final GlobalKey _kNoteKey = new GlobalKey(debugLabel: 'note field');
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final GlobalKey inputKey;
|
|
||||||
final String icon;
|
|
||||||
final String placeholder;
|
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return new Row(
|
|
||||||
children: <Widget>[
|
|
||||||
new Padding(
|
|
||||||
padding: const EdgeDims.symmetric(horizontal: 16.0),
|
|
||||||
child: new Icon(icon: icon)
|
|
||||||
),
|
|
||||||
new Flexible(
|
|
||||||
child: new Input(
|
|
||||||
key: inputKey,
|
|
||||||
placeholder: placeholder
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AddressBookHome extends StatelessComponent {
|
class AddressBookHome extends StatelessComponent {
|
||||||
Widget buildToolBar(BuildContext context) {
|
|
||||||
return new ToolBar(
|
|
||||||
right: <Widget>[new IconButton(icon: "navigation/check")]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildFloatingActionButton(BuildContext context) {
|
|
||||||
return new FloatingActionButton(
|
|
||||||
child: new Icon(icon: 'image/photo_camera'),
|
|
||||||
backgroundColor: Theme.of(context).accentColor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static final GlobalKey nameKey = new GlobalKey(debugLabel: 'name field');
|
|
||||||
static final GlobalKey phoneKey = new GlobalKey(debugLabel: 'phone field');
|
|
||||||
static final GlobalKey emailKey = new GlobalKey(debugLabel: 'email field');
|
|
||||||
static final GlobalKey addressKey = new GlobalKey(debugLabel: 'address field');
|
|
||||||
static final GlobalKey ringtoneKey = new GlobalKey(debugLabel: 'ringtone field');
|
|
||||||
static final GlobalKey noteKey = new GlobalKey(debugLabel: 'note field');
|
|
||||||
|
|
||||||
Widget buildBody(BuildContext context) {
|
|
||||||
return new Block(children: <Widget>[
|
|
||||||
new AspectRatio(
|
|
||||||
aspectRatio: 16.0 / 9.0,
|
|
||||||
child: new Container(
|
|
||||||
decoration: new BoxDecoration(backgroundColor: Colors.purple[300])
|
|
||||||
)
|
|
||||||
),
|
|
||||||
new Field(inputKey: nameKey, icon: "social/person", placeholder: "Name"),
|
|
||||||
new Field(inputKey: phoneKey, icon: "communication/phone", placeholder: "Phone"),
|
|
||||||
new Field(inputKey: emailKey, icon: "communication/email", placeholder: "Email"),
|
|
||||||
new Field(inputKey: addressKey, icon: "maps/place", placeholder: "Address"),
|
|
||||||
new Field(inputKey: ringtoneKey, icon: "av/volume_up", placeholder: "Ringtone"),
|
|
||||||
new Field(inputKey: noteKey, icon: "content/add", placeholder: "Add note"),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
toolBar: buildToolBar(context),
|
toolBar: new ToolBar(
|
||||||
body: buildBody(context),
|
center: new Text('Edit contact'),
|
||||||
floatingActionButton: buildFloatingActionButton(context)
|
right: <Widget>[
|
||||||
|
new IconButton(icon: 'navigation/check')
|
||||||
|
]
|
||||||
|
),
|
||||||
|
body: new Block(
|
||||||
|
children: <Widget>[
|
||||||
|
new AspectRatio(
|
||||||
|
aspectRatio: 16.0 / 9.0,
|
||||||
|
child: new Container(
|
||||||
|
decoration: new BoxDecoration(backgroundColor: Colors.purple[300])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new Input(key: _kNameKey, icon: 'social/person', labelText: 'Name', style: Typography.black.display1),
|
||||||
|
new Input(key: _kPhoneKey, icon: 'communication/phone', hintText: 'Phone'),
|
||||||
|
new Input(key: _kEmailKey, icon: 'communication/email', hintText: 'Email'),
|
||||||
|
new Input(key: _kAddressKey, icon: 'maps/place', hintText: 'Address'),
|
||||||
|
new Input(key: _kRingtoneKey, icon: 'av/volume_up', hintText: 'Ringtone'),
|
||||||
|
new Input(key: _kNoteKey, icon: 'content/add', hintText: 'Add note'),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
floatingActionButton: new FloatingActionButton(
|
||||||
|
child: new Icon(icon: 'image/photo_camera')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ class MealFragmentState extends State<MealFragment> {
|
||||||
new Input(
|
new Input(
|
||||||
key: descriptionKey,
|
key: descriptionKey,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
placeholder: 'Describe meal',
|
hintText: 'Describe meal',
|
||||||
onChanged: _handleDescriptionChanged
|
onChanged: _handleDescriptionChanged
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -142,7 +142,7 @@ class MeasurementFragmentState extends State<MeasurementFragment> {
|
||||||
new Input(
|
new Input(
|
||||||
key: weightKey,
|
key: weightKey,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
placeholder: 'Enter weight',
|
hintText: 'Enter weight',
|
||||||
keyboardType: KeyboardType.number,
|
keyboardType: KeyboardType.number,
|
||||||
onChanged: _handleWeightChanged
|
onChanged: _handleWeightChanged
|
||||||
),
|
),
|
||||||
|
|
|
@ -63,7 +63,7 @@ class SettingsFragmentState extends State<SettingsFragment> {
|
||||||
content: new Input(
|
content: new Input(
|
||||||
key: weightGoalKey,
|
key: weightGoalKey,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
placeholder: 'Goal weight in lbs',
|
hintText: 'Goal weight in lbs',
|
||||||
keyboardType: KeyboardType.number,
|
keyboardType: KeyboardType.number,
|
||||||
onChanged: _handleGoalWeightChanged
|
onChanged: _handleGoalWeightChanged
|
||||||
),
|
),
|
||||||
|
|
|
@ -237,7 +237,7 @@ class StockHomeState extends State<StockHome> {
|
||||||
center: new Input(
|
center: new Input(
|
||||||
key: searchFieldKey,
|
key: searchFieldKey,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
placeholder: 'Search stocks',
|
hintText: 'Search stocks',
|
||||||
onChanged: _handleSearchQueryChanged
|
onChanged: _handleSearchQueryChanged
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(context).canvasColor
|
backgroundColor: Theme.of(context).canvasColor
|
||||||
|
@ -254,7 +254,7 @@ class StockHomeState extends State<StockHome> {
|
||||||
new Input(
|
new Input(
|
||||||
key: companyNameKey,
|
key: companyNameKey,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
placeholder: 'Company Name'
|
hintText: 'Company Name'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,23 +6,29 @@ import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'colors.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
|
import 'icon.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
export 'package:flutter/rendering.dart' show ValueChanged;
|
export 'package:flutter/rendering.dart' show ValueChanged;
|
||||||
export 'package:flutter/services.dart' show KeyboardType;
|
export 'package:flutter/services.dart' show KeyboardType;
|
||||||
|
|
||||||
/// A material design text input widget.
|
/// A material design text input field.
|
||||||
class Input extends Scrollable {
|
class Input extends Scrollable {
|
||||||
Input({
|
Input({
|
||||||
GlobalKey key,
|
GlobalKey key,
|
||||||
this.initialValue: '',
|
this.initialValue: '',
|
||||||
this.placeholder,
|
this.keyboardType: KeyboardType.text,
|
||||||
|
this.icon,
|
||||||
|
this.labelText,
|
||||||
|
this.hintText,
|
||||||
|
this.errorText,
|
||||||
|
this.style,
|
||||||
this.hideText: false,
|
this.hideText: false,
|
||||||
this.isDense: false,
|
this.isDense: false,
|
||||||
this.autofocus: false,
|
this.autofocus: false,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.keyboardType: KeyboardType.text,
|
|
||||||
this.onSubmitted
|
this.onSubmitted
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
|
@ -32,28 +38,40 @@ class Input extends Scrollable {
|
||||||
assert(key != null);
|
assert(key != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initial editable text for the widget.
|
/// Initial editable text for the input field.
|
||||||
final String initialValue;
|
final String initialValue;
|
||||||
|
|
||||||
/// The type of keyboard to use for editing the text.
|
/// The type of keyboard to use for editing the text.
|
||||||
final KeyboardType keyboardType;
|
final KeyboardType keyboardType;
|
||||||
|
|
||||||
/// Hint text to show when the widget doesn't contain editable text.
|
/// An icon to show adjacent to the input field.
|
||||||
final String placeholder;
|
final String 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).
|
/// Whether to hide the text being edited (e.g., for passwords).
|
||||||
final bool hideText;
|
final bool hideText;
|
||||||
|
|
||||||
/// Whether the input widget is part of a dense form (i.e., uses less vertical space).
|
/// 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 widget should focus itself is nothing else is already focused.
|
/// Whether this input field should focus itself is nothing else is already focused.
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
|
||||||
/// Called when the text being edited changes.
|
/// Called when the text being edited changes.
|
||||||
final ValueChanged<String> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
/// Called when the user indicates that they are done editing the text in the widget.
|
/// Called when the user indicates that they are done editing the text in the field.
|
||||||
final ValueChanged<String> onSubmitted;
|
final ValueChanged<String> onSubmitted;
|
||||||
|
|
||||||
InputState createState() => new InputState();
|
InputState createState() => new InputState();
|
||||||
|
@ -95,6 +113,57 @@ class InputState extends ScrollableState<Input> {
|
||||||
config.onSubmitted(_value);
|
config.onSubmitted(_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildEditableField({
|
||||||
|
ThemeData themeData,
|
||||||
|
bool focused,
|
||||||
|
Color focusHighlightColor,
|
||||||
|
TextStyle textStyle,
|
||||||
|
double topPadding
|
||||||
|
}) {
|
||||||
|
Color cursorColor = themeData.primarySwatch == null ?
|
||||||
|
themeData.accentColor :
|
||||||
|
themeData.primarySwatch[200];
|
||||||
|
|
||||||
|
EdgeDims margin = new EdgeDims.only(bottom: config.isDense ? 4.0 : 8.0);
|
||||||
|
EdgeDims padding = new EdgeDims.only(top: topPadding, bottom: 8.0);
|
||||||
|
Color borderColor = focusHighlightColor;
|
||||||
|
double borderWidth = focused ? 2.0 : 1.0;
|
||||||
|
|
||||||
|
if (config.errorText != null) {
|
||||||
|
borderColor = Colors.red[700];
|
||||||
|
borderWidth = 2.0;
|
||||||
|
if (!config.isDense) {
|
||||||
|
margin = const EdgeDims.only(bottom: 15.0);
|
||||||
|
padding = new EdgeDims.only(top: topPadding, bottom: 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Container(
|
||||||
|
margin: margin,
|
||||||
|
padding: padding,
|
||||||
|
decoration: new BoxDecoration(
|
||||||
|
border: new Border(
|
||||||
|
bottom: new BorderSide(
|
||||||
|
color: borderColor,
|
||||||
|
width: borderWidth
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
child: new SizeObserver(
|
||||||
|
onSizeChanged: _handleContainerSizeChanged,
|
||||||
|
child: new RawEditableLine(
|
||||||
|
value: _editableString,
|
||||||
|
focused: focused,
|
||||||
|
style: textStyle,
|
||||||
|
hideText: config.hideText,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
onContentSizeChanged: _handleContentSizeChanged,
|
||||||
|
scrollOffset: scrollOffsetVector
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildContent(BuildContext context) {
|
Widget buildContent(BuildContext context) {
|
||||||
assert(debugCheckHasMaterial(context));
|
assert(debugCheckHasMaterial(context));
|
||||||
ThemeData themeData = Theme.of(context);
|
ThemeData themeData = Theme.of(context);
|
||||||
|
@ -109,35 +178,72 @@ class InputState extends ScrollableState<Input> {
|
||||||
_keyboardHandle.release();
|
_keyboardHandle.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
TextStyle textStyle = themeData.text.subhead;
|
TextStyle textStyle = config.style ?? themeData.text.subhead;
|
||||||
List<Widget> textChildren = <Widget>[];
|
|
||||||
|
|
||||||
if (config.placeholder != null && _value.isEmpty) {
|
|
||||||
Widget child = new Opacity(
|
|
||||||
key: const ValueKey<String>('placeholder'),
|
|
||||||
child: new Text(config.placeholder, style: textStyle),
|
|
||||||
opacity: themeData.hintOpacity
|
|
||||||
);
|
|
||||||
textChildren.add(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color focusHighlightColor = themeData.accentColor;
|
Color focusHighlightColor = themeData.accentColor;
|
||||||
Color cursorColor = themeData.accentColor;
|
if (themeData.primarySwatch != null)
|
||||||
if (themeData.primarySwatch != null) {
|
|
||||||
cursorColor = themeData.primarySwatch[200];
|
|
||||||
focusHighlightColor = focused ? themeData.primarySwatch[400] : themeData.hintColor;
|
focusHighlightColor = focused ? themeData.primarySwatch[400] : themeData.hintColor;
|
||||||
|
double topPadding = config.isDense ? 12.0 : 16.0;
|
||||||
|
|
||||||
|
List<Widget> stackChildren = <Widget>[];
|
||||||
|
|
||||||
|
if (config.labelText != null) {
|
||||||
|
TextStyle labelStyle = themeData.text.caption.copyWith(color: focused ? focusHighlightColor : themeData.hintColor);
|
||||||
|
stackChildren.add(new Positioned(
|
||||||
|
left: 0.0,
|
||||||
|
top: topPadding,
|
||||||
|
child: new Text(config.labelText, style: labelStyle)
|
||||||
|
));
|
||||||
|
topPadding += labelStyle.fontSize + (config.isDense ? 4.0 : 8.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
textChildren.add(new RawEditableLine(
|
if (config.hintText != null && _value.isEmpty) {
|
||||||
value: _editableString,
|
TextStyle hintStyle = textStyle.copyWith(color: themeData.hintColor);
|
||||||
|
stackChildren.add(new Positioned(
|
||||||
|
left: 0.0,
|
||||||
|
top: topPadding,
|
||||||
|
child: new Text(config.hintText, style: hintStyle)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
stackChildren.add(_buildEditableField(
|
||||||
|
themeData: themeData,
|
||||||
focused: focused,
|
focused: focused,
|
||||||
style: textStyle,
|
focusHighlightColor: focusHighlightColor,
|
||||||
hideText: config.hideText,
|
textStyle: textStyle,
|
||||||
cursorColor: cursorColor,
|
topPadding: topPadding
|
||||||
onContentSizeChanged: _handleContentSizeChanged,
|
|
||||||
scrollOffset: scrollOffsetVector
|
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if (config.errorText != null && !config.isDense) {
|
||||||
|
TextStyle errorStyle = themeData.text.caption.copyWith(color: Colors.red[700]);
|
||||||
|
stackChildren.add(new Positioned(
|
||||||
|
left: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
child: new Text(config.errorText, style: errorStyle)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget child = new Stack(children: stackChildren);
|
||||||
|
|
||||||
|
if (config.icon != null) {
|
||||||
|
double iconSize = config.isDense ? 18.0 : 24.0;
|
||||||
|
double iconTop = topPadding + (textStyle.fontSize - iconSize) / 2.0;
|
||||||
|
child = new Row(
|
||||||
|
alignItems: FlexAlignItems.start,
|
||||||
|
children: [
|
||||||
|
new Container(
|
||||||
|
margin: new EdgeDims.only(right: 16.0, top: iconTop),
|
||||||
|
width: config.isDense ? 40.0 : 48.0,
|
||||||
|
child: new Icon(
|
||||||
|
icon: config.icon,
|
||||||
|
color: focused ? focusHighlightColor : Colors.black45,
|
||||||
|
size: config.isDense ? IconSize.s18 : IconSize.s24
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new Flexible(child: child)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return new GestureDetector(
|
return new GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -149,23 +255,9 @@ class InputState extends ScrollableState<Input> {
|
||||||
// we'll get told to rebuild and we'll take care of the keyboard then
|
// we'll get told to rebuild and we'll take care of the keyboard then
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: new SizeObserver(
|
child: new Padding(
|
||||||
onSizeChanged: _handleContainerSizeChanged,
|
padding: const EdgeDims.symmetric(horizontal: 16.0),
|
||||||
child: new Container(
|
child: child
|
||||||
child: new Stack(children: textChildren),
|
|
||||||
margin: config.isDense ?
|
|
||||||
const EdgeDims.symmetric(vertical: 4.0) :
|
|
||||||
const EdgeDims.symmetric(vertical: 8.0),
|
|
||||||
padding: const EdgeDims.symmetric(vertical: 8.0),
|
|
||||||
decoration: new BoxDecoration(
|
|
||||||
border: new Border(
|
|
||||||
bottom: new BorderSide(
|
|
||||||
color: focusHighlightColor,
|
|
||||||
width: focused ? 2.0 : 1.0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ void main() {
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Input(
|
child: new Input(
|
||||||
key: inputKey,
|
key: inputKey,
|
||||||
placeholder: 'Placeholder',
|
hintText: 'Placeholder',
|
||||||
onChanged: (String value) { inputValue = value; }
|
onChanged: (String value) { inputValue = value; }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -81,7 +81,7 @@ void main() {
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Input(
|
child: new Input(
|
||||||
key: inputKey,
|
key: inputKey,
|
||||||
placeholder: 'Placeholder'
|
hintText: 'Placeholder'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -123,7 +123,7 @@ void main() {
|
||||||
child: new Material(
|
child: new Material(
|
||||||
child: new Input(
|
child: new Input(
|
||||||
key: inputKey,
|
key: inputKey,
|
||||||
placeholder: 'Placeholder'
|
hintText: 'Placeholder'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -159,7 +159,7 @@ void main() {
|
||||||
child: new Input(
|
child: new Input(
|
||||||
key: inputKey,
|
key: inputKey,
|
||||||
hideText: true,
|
hideText: true,
|
||||||
placeholder: 'Placeholder'
|
hintText: 'Placeholder'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue