input.dart 9.13 KB
Newer Older
1 2 3 4
// 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.

5 6
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
7

8
import 'colors.dart';
9
import 'debug.dart';
10
import 'icon.dart';
Ian Hickson's avatar
Ian Hickson committed
11 12
import 'icon_theme.dart';
import 'icon_theme_data.dart';
13 14
import 'material.dart';
import 'text_selection.dart';
15
import 'theme.dart';
16

17
export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType;
18

19
/// A material design text input field.
20 21 22 23
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
24
///
25
///  * <https://www.google.com/design/spec/components/text-fields.html>
26
class Input extends StatefulWidget {
27 28 29
  /// Creates a text input field.
  ///
  /// By default, the input uses a keyboard appropriate for text entry.
Eric Seidel's avatar
Eric Seidel committed
30
  Input({
31
    Key key,
32
    this.value,
33 34 35 36 37 38
    this.keyboardType: KeyboardType.text,
    this.icon,
    this.labelText,
    this.hintText,
    this.errorText,
    this.style,
Adam Barth's avatar
Adam Barth committed
39
    this.hideText: false,
40
    this.isDense: false,
41
    this.autofocus: false,
42
    this.formField,
43
    this.onChanged,
44
    this.onSubmitted
45
  }) : super(key: key);
46

47 48
  /// The text of the input field.
  final InputValue value;
49

50
  /// The type of keyboard to use for editing the text.
51
  final KeyboardType keyboardType;
52

53
  /// An icon to show adjacent to the input field.
Ian Hickson's avatar
Ian Hickson committed
54 55 56 57 58 59 60
  ///
  /// 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;
61 62 63 64 65 66 67 68 69 70 71 72

  /// 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;
73 74

  /// Whether to hide the text being edited (e.g., for passwords).
Adam Barth's avatar
Adam Barth committed
75
  final bool hideText;
76

77
  /// Whether the input field is part of a dense form (i.e., uses less vertical space).
78
  final bool isDense;
79

80
  /// Whether this input field should focus itself is nothing else is already focused.
81 82
  final bool autofocus;

83 84 85
  /// Form-specific data, required if this Input is part of a Form.
  final FormField<String> formField;

86
  /// Called when the text being edited changes.
87
  final ValueChanged<InputValue> onChanged;
88

89
  /// Called when the user indicates that they are done editing the text in the field.
90
  final ValueChanged<InputValue> onSubmitted;
91

92
  @override
93
  _InputState createState() => new _InputState();
94
}
Eric Seidel's avatar
Eric Seidel committed
95

96 97 98
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.ease;

99
class _InputState extends State<Input> {
100
  GlobalKey<RawInputLineState> _rawInputLineKey = new GlobalKey<RawInputLineState>();
101

102 103
  GlobalKey get focusKey => config.key is GlobalKey ? config.key : _rawInputLineKey;

104 105 106
  // Optional state to retain if we are inside a Form widget.
  _FormFieldData _formData;

107
  @override
108
  Widget build(BuildContext context) {
109
    assert(debugCheckHasMaterial(context));
110
    ThemeData themeData = Theme.of(context);
111 112
    BuildContext focusContext = focusKey.currentContext;
    bool focused = focusContext != null && Focus.at(focusContext, autofocus: config.autofocus);
113 114 115 116 117 118 119 120 121
    if (_formData == null)
      _formData = _FormFieldData.maybeCreate(context, this);
    InputValue value = config.value ?? _formData?.value ?? InputValue.empty;
    ValueChanged<InputValue> onChanged = config.onChanged ?? _formData?.onChanged;
    ValueChanged<InputValue> onSubmitted = config.onSubmitted ?? _formData?.onSubmitted;
    String errorText = config.errorText;

    if (errorText == null && config.formField != null && config.formField.validator != null)
      errorText = config.formField.validator(value.text);
122

123
    TextStyle textStyle = config.style ?? themeData.textTheme.subhead;
124 125 126
    Color activeColor = themeData.hintColor;
    if (focused) {
      switch (themeData.brightness) {
127
        case Brightness.dark:
128 129
          activeColor = themeData.accentColor;
          break;
130
        case Brightness.light:
131 132 133 134
          activeColor = themeData.primaryColor;
          break;
      }
    }
135
    double topPadding = config.isDense ? 12.0 : 16.0;
136

137 138
    List<Widget> stackChildren = <Widget>[];

139
    bool hasInlineLabel = config.labelText != null && !focused && !value.text.isNotEmpty;
140

141
    if (config.labelText != null) {
142
      TextStyle labelStyle = hasInlineLabel ?
143 144
        themeData.textTheme.subhead.copyWith(color: themeData.hintColor) :
        themeData.textTheme.caption.copyWith(color: activeColor);
145

146
      double topPaddingIncrement = themeData.textTheme.caption.fontSize + (config.isDense ? 4.0 : 8.0);
147 148 149 150 151
      double top = topPadding;
      if (hasInlineLabel)
        top += topPaddingIncrement + textStyle.fontSize - labelStyle.fontSize;

      stackChildren.add(new AnimatedPositioned(
152
        left: 0.0,
153 154 155
        top: top,
        duration: _kTransitionDuration,
        curve: _kTransitionCurve,
156 157
        child: new Text(config.labelText, style: labelStyle)
      ));
158 159

      topPadding += topPaddingIncrement;
160 161
    }

162
    if (config.hintText != null && value.text.isEmpty && !hasInlineLabel) {
163
      TextStyle hintStyle = themeData.textTheme.subhead.copyWith(color: themeData.hintColor);
164 165
      stackChildren.add(new Positioned(
        left: 0.0,
166
        top: topPadding + textStyle.fontSize - hintStyle.fontSize,
167 168
        child: new Text(config.hintText, style: hintStyle)
      ));
169 170
    }

171 172
    EdgeInsets margin = new EdgeInsets.only(bottom: config.isDense ? 4.0 : 8.0);
    EdgeInsets padding = new EdgeInsets.only(top: topPadding, bottom: 8.0);
173
    Color borderColor = activeColor;
174 175
    double borderWidth = focused ? 2.0 : 1.0;

176
    if (errorText != null) {
177
      borderColor = themeData.errorColor;
178 179
      borderWidth = 2.0;
      if (!config.isDense) {
180 181
        margin = const EdgeInsets.only(bottom: 15.0);
        padding = new EdgeInsets.only(top: topPadding, bottom: 1.0);
182 183 184
      }
    }

185
    stackChildren.add(new AnimatedContainer(
186 187
      margin: margin,
      padding: padding,
188 189
      duration: _kTransitionDuration,
      curve: _kTransitionCurve,
190 191 192 193 194 195 196 197
      decoration: new BoxDecoration(
        border: new Border(
          bottom: new BorderSide(
            color: borderColor,
            width: borderWidth
          )
        )
      ),
198 199
      child: new RawInputLine(
        key: _rawInputLineKey,
200
        value: value,
201
        focusKey: focusKey,
202 203
        style: textStyle,
        hideText: config.hideText,
204 205
        cursorColor: themeData.textSelectionColor,
        selectionColor: themeData.textSelectionColor,
206 207
        selectionHandleBuilder: buildTextSelectionHandle,
        selectionToolbarBuilder: buildTextSelectionToolbar,
208
        platform: Theme.of(context).platform,
209
        keyboardType: config.keyboardType,
210 211
        onChanged: onChanged,
        onSubmitted: onSubmitted
212
      )
213 214
    ));

215
    if (errorText != null && !config.isDense) {
216
      TextStyle errorStyle = themeData.textTheme.caption.copyWith(color: themeData.errorColor);
217 218 219
      stackChildren.add(new Positioned(
        left: 0.0,
        bottom: 0.0,
220
        child: new Text(errorText, style: errorStyle)
221 222 223 224 225 226 227 228 229
      ));
    }

    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(
230
        crossAxisAlignment: CrossAxisAlignment.start,
231
        children: <Widget>[
232
          new Container(
233
            margin: new EdgeInsets.only(right: 16.0, top: iconTop),
234
            width: config.isDense ? 40.0 : 48.0,
Ian Hickson's avatar
Ian Hickson committed
235 236 237 238 239 240 241
            child: new IconTheme.merge(
              context: context,
              data: new IconThemeData(
                color: focused ? activeColor : Colors.black45,
                size: config.isDense ? 18.0 : 24.0
              ),
              child: config.icon
242 243 244 245 246 247 248
            )
          ),
          new Flexible(child: child)
        ]
      );
    }

249 250
    return new GestureDetector(
      behavior: HitTestBehavior.opaque,
251
      onTap: () => _rawInputLineKey.currentState?.requestKeyboard(),
252
      child: new Padding(
253
        padding: const EdgeInsets.symmetric(horizontal: 16.0),
254
        child: child
255
      )
256 257 258
    );
  }
}
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291

class _FormFieldData {
  _FormFieldData(this.inputState) {
    assert(field != null);
  }

  InputValue value = new InputValue();
  final _InputState inputState;
  FormField<String> get field => inputState.config.formField;

  static _FormFieldData maybeCreate(BuildContext context, _InputState inputState) {
    // Only create a _FormFieldData if this Input is a descendent of a Form.
    if (FormScope.of(context) != null)
      return new _FormFieldData(inputState);
    return null;
  }

  void onChanged(InputValue value) {
    FormScope scope = FormScope.of(inputState.context);
    assert(scope != null);
    this.value = value;
    if (field.setter != null)
      field.setter(value.text);
    scope.onFieldChanged();
  }

  void onSubmitted(InputValue value) {
    FormScope scope = FormScope.of(inputState.context);
    assert(scope != null);
    scope.form.onSubmitted();
    scope.onFieldChanged();
  }
}