Commit 0911e5d3 authored by Adam Barth's avatar Adam Barth

Merge pull request #1352 from abarth/input_features

Add material design features to Input
parents b240cda8 5f3b2d48
...@@ -4,79 +4,41 @@ ...@@ -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;
class AddressBookHome extends StatelessComponent {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Row( return new Scaffold(
children: <Widget>[ toolBar: new ToolBar(
new Padding( center: new Text('Edit contact'),
padding: const EdgeDims.symmetric(horizontal: 16.0), right: <Widget>[
child: new Icon(icon: icon) new IconButton(icon: 'navigation/check')
),
new Flexible(
child: new Input(
key: inputKey,
placeholder: placeholder
)
)
] ]
); ),
} body: new Block(
} children: <Widget>[
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( new AspectRatio(
aspectRatio: 16.0 / 9.0, aspectRatio: 16.0 / 9.0,
child: new Container( child: new Container(
decoration: new BoxDecoration(backgroundColor: Colors.purple[300]) decoration: new BoxDecoration(backgroundColor: Colors.purple[300])
) )
), ),
new Field(inputKey: nameKey, icon: "social/person", placeholder: "Name"), new Input(key: _kNameKey, icon: 'social/person', labelText: 'Name', style: Typography.black.display1),
new Field(inputKey: phoneKey, icon: "communication/phone", placeholder: "Phone"), new Input(key: _kPhoneKey, icon: 'communication/phone', hintText: 'Phone'),
new Field(inputKey: emailKey, icon: "communication/email", placeholder: "Email"), new Input(key: _kEmailKey, icon: 'communication/email', hintText: 'Email'),
new Field(inputKey: addressKey, icon: "maps/place", placeholder: "Address"), new Input(key: _kAddressKey, icon: 'maps/place', hintText: 'Address'),
new Field(inputKey: ringtoneKey, icon: "av/volume_up", placeholder: "Ringtone"), new Input(key: _kRingtoneKey, icon: 'av/volume_up', hintText: 'Ringtone'),
new Field(inputKey: noteKey, icon: "content/add", placeholder: "Add note"), new Input(key: _kNoteKey, icon: 'content/add', hintText: 'Add note'),
]); ]
} ),
floatingActionButton: new FloatingActionButton(
Widget build(BuildContext context) { child: new Icon(icon: 'image/photo_camera')
return new Scaffold( )
toolBar: buildToolBar(context),
body: buildBody(context),
floatingActionButton: buildFloatingActionButton(context)
); );
} }
} }
......
...@@ -90,7 +90,7 @@ class MealFragmentState extends State<MealFragment> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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> { ...@@ -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'
), ),
] ]
); );
......
...@@ -2,71 +2,87 @@ ...@@ -2,71 +2,87 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart'; 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 StatefulComponent {
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,
initialScrollOffset: 0.0,
scrollDirection: Axis.horizontal
) {
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();
} }
class InputState extends ScrollableState<Input> { const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.ease;
class _InputState extends State<Input> {
String _value; String _value;
EditableString _editableString; EditableString _editableString;
KeyboardHandle _keyboardHandle = KeyboardHandle.unattached; KeyboardHandle _keyboardHandle = KeyboardHandle.unattached;
double _contentWidth = 0.0; // Used by tests.
double _containerWidth = 0.0;
EditableString get editableValue => _editableString; EditableString get editableValue => _editableString;
void initState() { void initState() {
...@@ -79,6 +95,23 @@ class InputState extends ScrollableState<Input> { ...@@ -79,6 +95,23 @@ class InputState extends ScrollableState<Input> {
); );
} }
void dispose() {
if (_keyboardHandle.attached)
_keyboardHandle.release();
super.dispose();
}
void _attachOrDetachKeyboard(bool focused) {
if (focused && !_keyboardHandle.attached) {
_keyboardHandle = keyboard.show(_editableString.stub, config.keyboardType);
_keyboardHandle.setText(_editableString.text);
_keyboardHandle.setSelection(_editableString.selection.start,
_editableString.selection.end);
} else if (!focused && _keyboardHandle.attached) {
_keyboardHandle.release();
}
}
void _handleTextUpdated() { void _handleTextUpdated() {
if (_value != _editableString.text) { if (_value != _editableString.text) {
setState(() { setState(() {
...@@ -95,49 +128,124 @@ class InputState extends ScrollableState<Input> { ...@@ -95,49 +128,124 @@ class InputState extends ScrollableState<Input> {
config.onSubmitted(_value); config.onSubmitted(_value);
} }
Widget buildContent(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
ThemeData themeData = Theme.of(context); ThemeData themeData = Theme.of(context);
bool focused = Focus.at(context, autofocus: config.autofocus); bool focused = Focus.at(context, autofocus: config.autofocus);
if (focused && !_keyboardHandle.attached) { _attachOrDetachKeyboard(focused);
_keyboardHandle = keyboard.show(_editableString.stub, config.keyboardType);
_keyboardHandle.setText(_editableString.text);
_keyboardHandle.setSelection(_editableString.selection.start,
_editableString.selection.end);
} else if (!focused && _keyboardHandle.attached) {
_keyboardHandle.release();
}
TextStyle textStyle = themeData.text.subhead; TextStyle textStyle = config.style ?? themeData.text.subhead;
List<Widget> textChildren = <Widget>[]; Color focusHighlightColor = themeData.accentColor;
if (themeData.primarySwatch != null)
focusHighlightColor = focused ? themeData.primarySwatch[400] : themeData.hintColor;
double topPadding = config.isDense ? 12.0 : 16.0;
if (config.placeholder != null && _value.isEmpty) { List<Widget> stackChildren = <Widget>[];
Widget child = new Opacity(
key: const ValueKey<String>('placeholder'), bool hasInlineLabel = config.labelText != null && !focused && !_value.isNotEmpty;
child: new Text(config.placeholder, style: textStyle),
opacity: themeData.hintOpacity if (config.labelText != null) {
); TextStyle labelStyle = hasInlineLabel ?
textChildren.add(child); themeData.text.subhead.copyWith(color: themeData.hintColor) :
themeData.text.caption.copyWith(color: focused ? focusHighlightColor : themeData.hintColor);
double topPaddingIncrement = themeData.text.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 Text(config.labelText, style: labelStyle)
));
topPadding += topPaddingIncrement;
} }
Color focusHighlightColor = themeData.accentColor; if (config.hintText != null && _value.isEmpty && !hasInlineLabel) {
Color cursorColor = themeData.accentColor; TextStyle hintStyle = themeData.text.subhead.copyWith(color: themeData.hintColor);
if (themeData.primarySwatch != null) { stackChildren.add(new Positioned(
cursorColor = themeData.primarySwatch[200]; left: 0.0,
focusHighlightColor = focused ? themeData.primarySwatch[400] : themeData.hintColor; top: topPadding + textStyle.fontSize - hintStyle.fontSize,
child: new Text(config.hintText, style: hintStyle)
));
} }
textChildren.add(new RawEditableLine( 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 = themeData.errorColor;
borderWidth = 2.0;
if (!config.isDense) {
margin = const EdgeDims.only(bottom: 15.0);
padding = new EdgeDims.only(top: topPadding, bottom: 1.0);
}
}
stackChildren.add(new AnimatedContainer(
margin: margin,
padding: padding,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
decoration: new BoxDecoration(
border: new Border(
bottom: new BorderSide(
color: borderColor,
width: borderWidth
)
)
),
child: new RawEditableLine(
value: _editableString, value: _editableString,
focused: focused, focused: focused,
style: textStyle, style: textStyle,
hideText: config.hideText, hideText: config.hideText,
cursorColor: cursorColor, cursorColor: cursorColor
onContentSizeChanged: _handleContentSizeChanged, )
scrollOffset: scrollOffsetVector
)); ));
if (config.errorText != null && !config.isDense) {
TextStyle errorStyle = themeData.text.caption.copyWith(color: themeData.errorColor);
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,53 +257,10 @@ class InputState extends ScrollableState<Input> { ...@@ -149,53 +257,10 @@ 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
)
)
)
)
) )
); );
} }
void dispose() {
if (_keyboardHandle.attached)
_keyboardHandle.release();
super.dispose();
}
ScrollBehavior createScrollBehavior() => new BoundedBehavior();
BoundedBehavior get scrollBehavior => super.scrollBehavior;
void _handleContainerSizeChanged(Size newSize) {
_containerWidth = newSize.width;
_updateScrollBehavior();
}
void _handleContentSizeChanged(Size newSize) {
_contentWidth = newSize.width;
_updateScrollBehavior();
}
void _updateScrollBehavior() {
// Set the scroll offset to match the content width so that the cursor
// (which is always at the end of the text) will be visible.
scrollTo(scrollBehavior.updateExtents(
contentExtent: _contentWidth,
containerExtent: _containerWidth,
scrollOffset: _contentWidth
));
}
} }
...@@ -46,6 +46,7 @@ class ThemeData { ...@@ -46,6 +46,7 @@ class ThemeData {
this.indicatorColor, this.indicatorColor,
this.hintColor, this.hintColor,
this.hintOpacity, this.hintOpacity,
this.errorColor,
this.text, this.text,
this.primaryTextTheme, this.primaryTextTheme,
this.primaryIconTheme this.primaryIconTheme
...@@ -66,6 +67,7 @@ class ThemeData { ...@@ -66,6 +67,7 @@ class ThemeData {
assert(indicatorColor != null); assert(indicatorColor != null);
assert(hintColor != null); assert(hintColor != null);
assert(hintOpacity != null); assert(hintOpacity != null);
assert(errorColor != null);
assert(text != null); assert(text != null);
assert(primaryTextTheme != null); assert(primaryTextTheme != null);
assert(primaryIconTheme != null); assert(primaryIconTheme != null);
...@@ -88,6 +90,7 @@ class ThemeData { ...@@ -88,6 +90,7 @@ class ThemeData {
Color indicatorColor, Color indicatorColor,
Color hintColor, Color hintColor,
double hintOpacity, double hintOpacity,
Color errorColor,
TextTheme text, TextTheme text,
TextTheme primaryTextTheme, TextTheme primaryTextTheme,
IconThemeData primaryIconTheme IconThemeData primaryIconTheme
...@@ -108,6 +111,7 @@ class ThemeData { ...@@ -108,6 +111,7 @@ class ThemeData {
indicatorColor ??= accentColor == primaryColor ? Colors.white : accentColor; indicatorColor ??= accentColor == primaryColor ? Colors.white : accentColor;
hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000); hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000);
hintOpacity ??= hintColor != null ? hintColor.alpha / 0xFF : isDark ? 0.26 : 0.30; hintOpacity ??= hintColor != null ? hintColor.alpha / 0xFF : isDark ? 0.26 : 0.30;
errorColor ??= Colors.red[700];
text ??= isDark ? Typography.white : Typography.black; text ??= isDark ? Typography.white : Typography.black;
primaryTextTheme ??= primaryColorBrightness == ThemeBrightness.dark ? Typography.white : Typography.black; primaryTextTheme ??= primaryColorBrightness == ThemeBrightness.dark ? Typography.white : Typography.black;
primaryIconTheme ??= primaryColorBrightness == ThemeBrightness.dark ? const IconThemeData(color: IconThemeColor.white) : const IconThemeData(color: IconThemeColor.black); primaryIconTheme ??= primaryColorBrightness == ThemeBrightness.dark ? const IconThemeData(color: IconThemeColor.white) : const IconThemeData(color: IconThemeColor.black);
...@@ -128,6 +132,7 @@ class ThemeData { ...@@ -128,6 +132,7 @@ class ThemeData {
indicatorColor: indicatorColor, indicatorColor: indicatorColor,
hintColor: hintColor, hintColor: hintColor,
hintOpacity: hintOpacity, hintOpacity: hintOpacity,
errorColor: errorColor,
text: text, text: text,
primaryTextTheme: primaryTextTheme, primaryTextTheme: primaryTextTheme,
primaryIconTheme: primaryIconTheme primaryIconTheme: primaryIconTheme
...@@ -182,6 +187,9 @@ class ThemeData { ...@@ -182,6 +187,9 @@ class ThemeData {
final Color hintColor; final Color hintColor;
final double hintOpacity; final double hintOpacity;
/// The color to use for input validation errors.
final Color errorColor;
/// Text with a color that contrasts with the card and canvas colors. /// Text with a color that contrasts with the card and canvas colors.
final TextTheme text; final TextTheme text;
...@@ -217,6 +225,7 @@ class ThemeData { ...@@ -217,6 +225,7 @@ class ThemeData {
indicatorColor: Color.lerp(begin.indicatorColor, end.indicatorColor, t), indicatorColor: Color.lerp(begin.indicatorColor, end.indicatorColor, t),
hintColor: Color.lerp(begin.hintColor, end.hintColor, t), hintColor: Color.lerp(begin.hintColor, end.hintColor, t),
hintOpacity: lerpDouble(begin.hintOpacity, end.hintOpacity, t), hintOpacity: lerpDouble(begin.hintOpacity, end.hintOpacity, t),
errorColor: Color.lerp(begin.errorColor, end.errorColor, t),
text: TextTheme.lerp(begin.text, end.text, t), text: TextTheme.lerp(begin.text, end.text, t),
primaryTextTheme: TextTheme.lerp(begin.primaryTextTheme, end.primaryTextTheme, t), primaryTextTheme: TextTheme.lerp(begin.primaryTextTheme, end.primaryTextTheme, t),
primaryIconTheme: IconThemeData.lerp(begin.primaryIconTheme, end.primaryIconTheme, t) primaryIconTheme: IconThemeData.lerp(begin.primaryIconTheme, end.primaryIconTheme, t)
...@@ -243,6 +252,7 @@ class ThemeData { ...@@ -243,6 +252,7 @@ class ThemeData {
(otherData.indicatorColor == indicatorColor) && (otherData.indicatorColor == indicatorColor) &&
(otherData.hintColor == hintColor) && (otherData.hintColor == hintColor) &&
(otherData.hintOpacity == hintOpacity) && (otherData.hintOpacity == hintOpacity) &&
(otherData.errorColor == errorColor) &&
(otherData.text == text) && (otherData.text == text) &&
(otherData.primaryTextTheme == primaryTextTheme) && (otherData.primaryTextTheme == primaryTextTheme) &&
(otherData.primaryIconTheme == primaryIconTheme); (otherData.primaryIconTheme == primaryIconTheme);
...@@ -263,12 +273,15 @@ class ThemeData { ...@@ -263,12 +273,15 @@ class ThemeData {
disabledColor, disabledColor,
accentColor, accentColor,
accentColorBrightness, accentColorBrightness,
hashValues( // Too many values.
indicatorColor, indicatorColor,
hintColor, hintColor,
hintOpacity, hintOpacity,
errorColor,
text, text,
primaryTextTheme, primaryTextTheme,
primaryIconTheme primaryIconTheme
)
); );
} }
......
...@@ -29,6 +29,25 @@ class BorderSide { ...@@ -29,6 +29,25 @@ class BorderSide {
/// A black border side of zero width. /// A black border side of zero width.
static const none = const BorderSide(width: 0.0); static const none = const BorderSide(width: 0.0);
BorderSide copyWith({
Color color,
double width
}) {
return new BorderSide(
color: color ?? this.color,
width: width ?? this.width
);
}
static BorderSide lerp(BorderSide a, BorderSide b, double t) {
assert(a != null);
assert(b != null);
return new BorderSide(
color: Color.lerp(a.color, b.color, t),
width: ui.lerpDouble(a.width, b.width, t)
);
}
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
if (identical(this, other)) if (identical(this, other))
return true; return true;
...@@ -79,6 +98,30 @@ class Border { ...@@ -79,6 +98,30 @@ class Border {
return new EdgeDims.TRBL(top.width, right.width, bottom.width, left.width); return new EdgeDims.TRBL(top.width, right.width, bottom.width, left.width);
} }
Border scale(double t) {
return new Border(
top: top.copyWith(width: t * top.width),
right: right.copyWith(width: t * right.width),
bottom: bottom.copyWith(width: t * bottom.width),
left: left.copyWith(width: t * left.width)
);
}
static Border lerp(Border a, Border b, double t) {
if (a == null && b == null)
return null;
if (a == null)
return b.scale(t);
if (b == null)
return a.scale(1.0 - t);
return new Border(
top: BorderSide.lerp(a.top, b.top, t),
right: BorderSide.lerp(a.right, b.right, t),
bottom: BorderSide.lerp(a.bottom, b.bottom, t),
left: BorderSide.lerp(a.left, b.left, t)
);
}
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
if (identical(this, other)) if (identical(this, other))
return true; return true;
...@@ -718,7 +761,7 @@ class BoxDecoration extends Decoration { ...@@ -718,7 +761,7 @@ class BoxDecoration extends Decoration {
return new BoxDecoration( return new BoxDecoration(
backgroundColor: Color.lerp(null, backgroundColor, factor), backgroundColor: Color.lerp(null, backgroundColor, factor),
backgroundImage: backgroundImage, backgroundImage: backgroundImage,
border: border, border: Border.lerp(null, border, factor),
borderRadius: ui.lerpDouble(null, borderRadius, factor), borderRadius: ui.lerpDouble(null, borderRadius, factor),
boxShadow: BoxShadow.lerpList(null, boxShadow, factor), boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
gradient: gradient, gradient: gradient,
...@@ -740,7 +783,7 @@ class BoxDecoration extends Decoration { ...@@ -740,7 +783,7 @@ class BoxDecoration extends Decoration {
return new BoxDecoration( return new BoxDecoration(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
backgroundImage: b.backgroundImage, backgroundImage: b.backgroundImage,
border: b.border, border: Border.lerp(a.border, b.border, t),
borderRadius: ui.lerpDouble(a.borderRadius, b.borderRadius, t), borderRadius: ui.lerpDouble(a.borderRadius, b.borderRadius, t),
boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t), boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),
gradient: b.gradient, gradient: b.gradient,
......
...@@ -22,13 +22,14 @@ class RenderEditableLine extends RenderBox { ...@@ -22,13 +22,14 @@ class RenderEditableLine extends RenderBox {
RenderEditableLine({ RenderEditableLine({
StyledTextSpan text, StyledTextSpan text,
Color cursorColor, Color cursorColor,
bool showCursor, bool showCursor: false,
this.onContentSizeChanged, Offset paintOffset: Offset.zero,
Offset scrollOffset this.onContentSizeChanged
}) : _textPainter = new TextPainter(text), }) : _textPainter = new TextPainter(text),
_cursorColor = cursorColor, _cursorColor = cursorColor,
_showCursor = showCursor, _showCursor = showCursor,
_scrollOffset = scrollOffset { _paintOffset = paintOffset {
assert(!showCursor || cursorColor != null);
// TODO(abarth): These min/max values should be the default for TextPainter. // TODO(abarth): These min/max values should be the default for TextPainter.
_textPainter _textPainter
..minWidth = 0.0 ..minWidth = 0.0
...@@ -71,12 +72,12 @@ class RenderEditableLine extends RenderBox { ...@@ -71,12 +72,12 @@ class RenderEditableLine extends RenderBox {
markNeedsPaint(); markNeedsPaint();
} }
Offset get scrollOffset => _scrollOffset; Offset get paintOffset => _paintOffset;
Offset _scrollOffset; Offset _paintOffset;
void set scrollOffset(Offset value) { void set paintOffset(Offset value) {
if (_scrollOffset == value) if (_paintOffset == value)
return; return;
_scrollOffset = value; _paintOffset = value;
markNeedsPaint(); markNeedsPaint();
} }
...@@ -155,12 +156,12 @@ class RenderEditableLine extends RenderBox { ...@@ -155,12 +156,12 @@ class RenderEditableLine extends RenderBox {
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
_textPainter.paint(context.canvas, offset - _scrollOffset); _textPainter.paint(context.canvas, offset + _paintOffset);
if (_showCursor) { if (_showCursor) {
Rect cursorRect = new Rect.fromLTWH( Rect cursorRect = new Rect.fromLTWH(
offset.dx + _contentSize.width - _kCursorWidth - _scrollOffset.dx, offset.dx + _paintOffset.dx + _contentSize.width - _kCursorWidth,
offset.dy + _kCursorHeightOffset - _scrollOffset.dy, offset.dy + _paintOffset.dy + _kCursorHeightOffset,
_kCursorWidth, _kCursorWidth,
size.height - 2.0 * _kCursorHeightOffset size.height - 2.0 * _kCursorHeightOffset
); );
......
...@@ -11,26 +11,39 @@ import 'package:flutter/rendering.dart'; ...@@ -11,26 +11,39 @@ import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'scrollable.dart';
import 'scroll_behavior.dart';
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500); const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
/// A range of characters in a string of tet. /// A range of characters in a string of tet.
class TextRange { class TextRange {
const TextRange({ this.start, this.end }); const TextRange({ this.start, this.end });
/// A text range that starts and ends at position.
const TextRange.collapsed(int position) const TextRange.collapsed(int position)
: start = position, : start = position,
end = position; end = position;
/// A text range that contains nothing and is not in the text.
const TextRange.empty() const TextRange.empty()
: start = -1, : start = -1,
end = -1; end = -1;
/// The index of the first character in the range.
final int start; final int start;
/// The next index after the characters in this range.
final int end; final int end;
/// Whether this range represents a valid position in the text.
bool get isValid => start >= 0 && end >= 0; bool get isValid => start >= 0 && end >= 0;
/// Whether this range is empty (but still potentially placed inside the text).
bool get isCollapsed => start == end; bool get isCollapsed => start == end;
} }
/// A string that can be manipulated by a keyboard.
class EditableString implements KeyboardClient { class EditableString implements KeyboardClient {
EditableString({this.text: '', this.onUpdated, this.onSubmitted}) { EditableString({this.text: '', this.onUpdated, this.onSubmitted}) {
assert(onUpdated != null); assert(onUpdated != null);
...@@ -39,23 +52,35 @@ class EditableString implements KeyboardClient { ...@@ -39,23 +52,35 @@ class EditableString implements KeyboardClient {
selection = new TextRange(start: text.length, end: text.length); selection = new TextRange(start: text.length, end: text.length);
} }
/// The current text being edited.
String text; String text;
// The range of text that is still being composed.
TextRange composing = const TextRange.empty(); TextRange composing = const TextRange.empty();
/// The range of text that is currently selected.
TextRange selection; TextRange selection;
/// Called whenever the text changes.
final VoidCallback onUpdated; final VoidCallback onUpdated;
/// Called whenever the user indicates they are done editing the string.
final VoidCallback onSubmitted; final VoidCallback onSubmitted;
/// A keyboard client stub that can be attached to a keyboard service.
KeyboardClientStub stub; KeyboardClientStub stub;
/// The text before the given range.
String textBefore(TextRange range) { String textBefore(TextRange range) {
return text.substring(0, range.start); return text.substring(0, range.start);
} }
/// The text after the given range.
String textAfter(TextRange range) { String textAfter(TextRange range) {
return text.substring(range.end); return text.substring(range.end);
} }
/// The text inside the given range.
String textInside(TextRange range) { String textInside(TextRange range) {
return text.substring(range.start, range.end); return text.substring(range.start, range.end);
} }
...@@ -141,35 +166,72 @@ class EditableString implements KeyboardClient { ...@@ -141,35 +166,72 @@ class EditableString implements KeyboardClient {
} }
} }
class RawEditableLine extends StatefulComponent { /// A basic single-line input control.
///
/// This control is not intended to be used directly. Instead, consider using
/// [Input], which provides focus management and material design.
class RawEditableLine extends Scrollable {
RawEditableLine({ RawEditableLine({
Key key, Key key,
this.value, this.value,
this.focused: false, this.focused: false,
this.hideText: false, this.hideText: false,
this.style, this.style,
this.cursorColor, this.cursorColor
this.onContentSizeChanged, }) : super(
this.scrollOffset key: key,
}) : super(key: key); initialScrollOffset: 0.0,
scrollDirection: Axis.horizontal
);
/// The editable string being displayed in this widget.
final EditableString value; final EditableString value;
/// Whether this widget is focused.
final bool focused; final bool focused;
/// Whether to hide the text being edited (e.g., for passwords).
final bool hideText; final bool hideText;
/// The text style to use for the editable text.
final TextStyle style; final TextStyle style;
/// The color to use when painting the cursor.
final Color cursorColor; final Color cursorColor;
final SizeChangedCallback onContentSizeChanged;
final Offset scrollOffset;
RawEditableTextState createState() => new RawEditableTextState(); RawEditableTextState createState() => new RawEditableTextState();
} }
class RawEditableTextState extends State<RawEditableLine> { class RawEditableTextState extends ScrollableState<RawEditableLine> {
// TODO(abarth): Move the cursor timer into RenderEditableLine so we can
// remove this extra widget.
Timer _cursorTimer; Timer _cursorTimer;
bool _showCursor = false; bool _showCursor = false;
double _contentWidth = 0.0;
double _containerWidth = 0.0;
ScrollBehavior createScrollBehavior() => new BoundedBehavior();
BoundedBehavior get scrollBehavior => super.scrollBehavior;
void _handleContainerSizeChanged(Size newSize) {
_containerWidth = newSize.width;
_updateScrollBehavior();
}
void _handleContentSizeChanged(Size newSize) {
_contentWidth = newSize.width;
_updateScrollBehavior();
}
void _updateScrollBehavior() {
// Set the scroll offset to match the content width so that the cursor
// (which is always at the end of the text) will be visible.
scrollTo(scrollBehavior.updateExtents(
contentExtent: _contentWidth,
containerExtent: _containerWidth,
scrollOffset: _contentWidth
));
}
/// Whether the blinking cursor is actually visible at this precise moment /// Whether the blinking cursor is actually visible at this precise moment
/// (it's hidden half the time, since it blinks). /// (it's hidden half the time, since it blinks).
bool get cursorCurrentlyVisible => _showCursor; bool get cursorCurrentlyVisible => _showCursor;
...@@ -202,7 +264,7 @@ class RawEditableTextState extends State<RawEditableLine> { ...@@ -202,7 +264,7 @@ class RawEditableTextState extends State<RawEditableLine> {
_showCursor = false; _showCursor = false;
} }
Widget build(BuildContext context) { Widget buildContent(BuildContext context) {
assert(config.style != null); assert(config.style != null);
assert(config.focused != null); assert(config.focused != null);
assert(config.cursorColor != null); assert(config.cursorColor != null);
...@@ -212,14 +274,17 @@ class RawEditableTextState extends State<RawEditableLine> { ...@@ -212,14 +274,17 @@ class RawEditableTextState extends State<RawEditableLine> {
else if (!config.focused && _cursorTimer != null) else if (!config.focused && _cursorTimer != null)
_stopCursorTimer(); _stopCursorTimer();
return new _EditableLineWidget( return new SizeObserver(
onSizeChanged: _handleContainerSizeChanged,
child: new _EditableLineWidget(
value: config.value, value: config.value,
style: config.style, style: config.style,
cursorColor: config.cursorColor, cursorColor: config.cursorColor,
showCursor: _showCursor, showCursor: _showCursor,
hideText: config.hideText, hideText: config.hideText,
onContentSizeChanged: config.onContentSizeChanged, onContentSizeChanged: _handleContentSizeChanged,
scrollOffset: config.scrollOffset paintOffset: new Offset(-scrollOffset, 0.0)
)
); );
} }
} }
...@@ -233,7 +298,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -233,7 +298,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
this.showCursor, this.showCursor,
this.hideText, this.hideText,
this.onContentSizeChanged, this.onContentSizeChanged,
this.scrollOffset this.paintOffset
}) : super(key: key); }) : super(key: key);
final EditableString value; final EditableString value;
...@@ -242,7 +307,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -242,7 +307,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
final bool showCursor; final bool showCursor;
final bool hideText; final bool hideText;
final SizeChangedCallback onContentSizeChanged; final SizeChangedCallback onContentSizeChanged;
final Offset scrollOffset; final Offset paintOffset;
RenderEditableLine createRenderObject() { RenderEditableLine createRenderObject() {
return new RenderEditableLine( return new RenderEditableLine(
...@@ -250,7 +315,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -250,7 +315,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
cursorColor: cursorColor, cursorColor: cursorColor,
showCursor: showCursor, showCursor: showCursor,
onContentSizeChanged: onContentSizeChanged, onContentSizeChanged: onContentSizeChanged,
scrollOffset: scrollOffset paintOffset: paintOffset
); );
} }
...@@ -260,7 +325,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ...@@ -260,7 +325,7 @@ class _EditableLineWidget extends LeafRenderObjectWidget {
renderObject.cursorColor = cursorColor; renderObject.cursorColor = cursorColor;
renderObject.showCursor = showCursor; renderObject.showCursor = showCursor;
renderObject.onContentSizeChanged = onContentSizeChanged; renderObject.onContentSizeChanged = onContentSizeChanged;
renderObject.scrollOffset = scrollOffset; renderObject.paintOffset = paintOffset;
} }
StyledTextSpan get _styledTextSpan { StyledTextSpan get _styledTextSpan {
......
...@@ -41,7 +41,7 @@ void main() { ...@@ -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() { ...@@ -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() { ...@@ -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'
) )
) )
); );
...@@ -133,7 +133,7 @@ void main() { ...@@ -133,7 +133,7 @@ void main() {
const String testValue = 'ABC'; const String testValue = 'ABC';
mockKeyboard.client.commitText(testValue, testValue.length); mockKeyboard.client.commitText(testValue, testValue.length);
InputState input = tester.findStateOfType(InputState); dynamic input = inputKey.currentState;
// Delete characters and verify that the selection follows the length // Delete characters and verify that the selection follows the length
// of the text. // of the text.
...@@ -159,7 +159,7 @@ void main() { ...@@ -159,7 +159,7 @@ void main() {
child: new Input( child: new Input(
key: inputKey, key: inputKey,
hideText: true, hideText: true,
placeholder: 'Placeholder' hintText: 'Placeholder'
) )
) )
); );
......
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