input.dart 6.73 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';
11
import 'icons.dart';
12
import 'theme.dart';
13

14
export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType;
15

16
/// A material design text input field.
17
class Input extends StatefulWidget {
Eric Seidel's avatar
Eric Seidel committed
18
  Input({
19
    Key key,
20
    this.value: InputValue.empty,
21 22 23 24 25 26
    this.keyboardType: KeyboardType.text,
    this.icon,
    this.labelText,
    this.hintText,
    this.errorText,
    this.style,
Adam Barth's avatar
Adam Barth committed
27
    this.hideText: false,
28
    this.isDense: false,
29
    this.autofocus: false,
30
    this.onChanged,
31
    this.onSubmitted
32
  }) : super(key: key);
33

34 35
  /// The text of the input field.
  final InputValue value;
36

37
  /// The type of keyboard to use for editing the text.
38
  final KeyboardType keyboardType;
39

40
  /// An icon to show adjacent to the input field.
41
  final IconData icon;
42 43 44 45 46 47 48 49 50 51 52 53

  /// 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;
54 55

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

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

61
  /// Whether this input field should focus itself is nothing else is already focused.
62 63 64
  final bool autofocus;

  /// Called when the text being edited changes.
65
  final ValueChanged<InputValue> onChanged;
66

67
  /// Called when the user indicates that they are done editing the text in the field.
68
  final ValueChanged<InputValue> onSubmitted;
69

70
  _InputState createState() => new _InputState();
71
}
Eric Seidel's avatar
Eric Seidel committed
72

73 74 75
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.ease;

76
class _InputState extends State<Input> {
77
  GlobalKey<RawInputLineState> _rawInputLineKey = new GlobalKey<RawInputLineState>();
78

79 80
  GlobalKey get focusKey => config.key is GlobalKey ? config.key : _rawInputLineKey;

81
  Widget build(BuildContext context) {
82
    assert(debugCheckHasMaterial(context));
83
    ThemeData themeData = Theme.of(context);
84 85
    BuildContext focusContext = focusKey.currentContext;
    bool focused = focusContext != null && Focus.at(focusContext, autofocus: config.autofocus);
86

87
    TextStyle textStyle = config.style ?? themeData.textTheme.subhead;
88 89 90 91 92 93 94 95 96 97 98
    Color activeColor = themeData.hintColor;
    if (focused) {
      switch (themeData.brightness) {
        case ThemeBrightness.dark:
          activeColor = themeData.accentColor;
          break;
        case ThemeBrightness.light:
          activeColor = themeData.primaryColor;
          break;
      }
    }
99
    double topPadding = config.isDense ? 12.0 : 16.0;
100

101 102
    List<Widget> stackChildren = <Widget>[];

103
    bool hasInlineLabel = config.labelText != null && !focused && !config.value.text.isNotEmpty;
104

105
    if (config.labelText != null) {
106
      TextStyle labelStyle = hasInlineLabel ?
107 108
        themeData.textTheme.subhead.copyWith(color: themeData.hintColor) :
        themeData.textTheme.caption.copyWith(color: activeColor);
109

110
      double topPaddingIncrement = themeData.textTheme.caption.fontSize + (config.isDense ? 4.0 : 8.0);
111 112 113 114 115
      double top = topPadding;
      if (hasInlineLabel)
        top += topPaddingIncrement + textStyle.fontSize - labelStyle.fontSize;

      stackChildren.add(new AnimatedPositioned(
116
        left: 0.0,
117 118 119
        top: top,
        duration: _kTransitionDuration,
        curve: _kTransitionCurve,
120 121
        child: new Text(config.labelText, style: labelStyle)
      ));
122 123

      topPadding += topPaddingIncrement;
124 125
    }

126
    if (config.hintText != null && config.value.text.isEmpty && !hasInlineLabel) {
127
      TextStyle hintStyle = themeData.textTheme.subhead.copyWith(color: themeData.hintColor);
128 129
      stackChildren.add(new Positioned(
        left: 0.0,
130
        top: topPadding + textStyle.fontSize - hintStyle.fontSize,
131 132
        child: new Text(config.hintText, style: hintStyle)
      ));
133 134
    }

135 136
    EdgeInsets margin = new EdgeInsets.only(bottom: config.isDense ? 4.0 : 8.0);
    EdgeInsets padding = new EdgeInsets.only(top: topPadding, bottom: 8.0);
137
    Color borderColor = activeColor;
138 139 140
    double borderWidth = focused ? 2.0 : 1.0;

    if (config.errorText != null) {
141
      borderColor = themeData.errorColor;
142 143
      borderWidth = 2.0;
      if (!config.isDense) {
144 145
        margin = const EdgeInsets.only(bottom: 15.0);
        padding = new EdgeInsets.only(top: topPadding, bottom: 1.0);
146 147 148
      }
    }

149
    stackChildren.add(new AnimatedContainer(
150 151
      margin: margin,
      padding: padding,
152 153
      duration: _kTransitionDuration,
      curve: _kTransitionCurve,
154 155 156 157 158 159 160 161
      decoration: new BoxDecoration(
        border: new Border(
          bottom: new BorderSide(
            color: borderColor,
            width: borderWidth
          )
        )
      ),
162 163 164
      child: new RawInputLine(
        key: _rawInputLineKey,
        value: config.value,
165
        focusKey: focusKey,
166 167
        style: textStyle,
        hideText: config.hideText,
168 169
        cursorColor: themeData.selectionColor,
        selectionColor: themeData.selectionColor,
170 171 172
        keyboardType: config.keyboardType,
        onChanged: config.onChanged,
        onSubmitted: config.onSubmitted
173
      )
174 175
    ));

176
    if (config.errorText != null && !config.isDense) {
177
      TextStyle errorStyle = themeData.textTheme.caption.copyWith(color: themeData.errorColor);
178 179 180 181 182 183 184 185 186 187 188 189 190
      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(
191
        crossAxisAlignment: CrossAxisAlignment.start,
192 193
        children: [
          new Container(
194
            margin: new EdgeInsets.only(right: 16.0, top: iconTop),
195 196 197
            width: config.isDense ? 40.0 : 48.0,
            child: new Icon(
              icon: config.icon,
198
              color: focused ? activeColor : Colors.black45,
199
              size: config.isDense ? 18.0 : 24.0
200 201 202 203 204 205 206
            )
          ),
          new Flexible(child: child)
        ]
      );
    }

207 208
    return new GestureDetector(
      behavior: HitTestBehavior.opaque,
209
      onTap: () => _rawInputLineKey.currentState?.requestKeyboard(),
210
      child: new Padding(
211
        padding: const EdgeInsets.symmetric(horizontal: 16.0),
212
        child: child
213
      )
214 215 216
    );
  }
}