checkbox.dart 11.9 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
import 'dart:math' as math;
6

7
import 'package:flutter/foundation.dart';
8 9
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
10

11
import 'constants.dart';
12
import 'debug.dart';
13
import 'theme.dart';
14
import 'toggleable.dart';
15

16
/// A material design checkbox.
17 18
///
/// The checkbox itself does not maintain any state. Instead, when the state of
19 20 21
/// the checkbox changes, the widget calls the [onChanged] callback. Most
/// widgets that use a checkbox will listen for the [onChanged] callback and
/// rebuild the checkbox with a new [value] to update the visual appearance of
22 23
/// the checkbox.
///
24 25 26 27
/// The checkbox can optionally display three values - true, false, and null -
/// if [tristate] is true. When [value] is null a dash is displayed. By default
/// [tristate] is false and the checkbox's [value] must be true or false.
///
28 29 30
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
31
///
32 33 34
///  * [CheckboxListTile], which combines this widget with a [ListTile] so that
///    you can give the checkbox a label.
///  * [Switch], a widget with semantics similar to [Checkbox].
35 36
///  * [Radio], for selecting among a set of explicit values.
///  * [Slider], for selecting a value in a range.
37 38
///  * <https://material.google.com/components/selection-controls.html#selection-controls-checkbox>
///  * <https://material.google.com/components/lists-controls.html#lists-controls-types-of-list-controls>
39
class Checkbox extends StatefulWidget {
40
  /// Creates a material design checkbox.
41
  ///
42 43 44 45 46 47
  /// The checkbox itself does not maintain any state. Instead, when the state of
  /// the checkbox changes, the widget calls the [onChanged] callback. Most
  /// widgets that use a checkbox will listen for the [onChanged] callback and
  /// rebuild the checkbox with a new [value] to update the visual appearance of
  /// the checkbox.
  ///
48 49
  /// The following arguments are required:
  ///
50 51
  /// * [value], which determines whether the checkbox is checked. The [value]
  ///   can only be be null if [tristate] is true.
52 53
  /// * [onChanged], which is called when the value of the checkbox should
  ///   change. It can be set to null to disable the checkbox.
54 55
  ///
  /// The value of [tristate] must not be null.
56
  const Checkbox({
57
    Key key,
58
    @required this.value,
59
    this.tristate: false,
60
    @required this.onChanged,
61
    this.activeColor,
62 63
  }) : assert(tristate != null),
       assert(tristate || value != null),
64
       super(key: key);
65

66
  /// Whether this checkbox is checked.
67 68
  ///
  /// This property must not be null.
69
  final bool value;
70

71
  /// Called when the value of the checkbox should change.
72 73 74 75 76
  ///
  /// The checkbox passes the new value to the callback but does not actually
  /// change state until the parent widget rebuilds the checkbox with the new
  /// value.
  ///
77 78 79 80 81 82
  /// If this callback is null, the checkbox will be displayed as disabled
  /// and will not respond to input gestures.
  ///
  /// When the checkbox is tapped, if [tristate] is false (the default) then
  /// the [onChanged] callback will be applied to `!value`. If [tristate] is
  /// true this callback cycle from false to true to null.
83
  ///
84
  /// The callback provided to [onChanged] should update the state of the parent
85 86 87 88 89 90 91 92 93 94 95
  /// [StatefulWidget] using the [State.setState] method, so that the parent
  /// gets rebuilt; for example:
  ///
  /// ```dart
  /// new Checkbox(
  ///   value: _throwShotAway,
  ///   onChanged: (bool newValue) {
  ///     setState(() {
  ///       _throwShotAway = newValue;
  ///     });
  ///   },
96
  /// )
97
  /// ```
Hixie's avatar
Hixie committed
98
  final ValueChanged<bool> onChanged;
99

100 101 102 103 104
  /// The color to use when this checkbox is checked.
  ///
  /// Defaults to accent color of the current [Theme].
  final Color activeColor;

105 106 107 108 109 110 111 112 113 114 115 116
  /// If true the checkbox's [value] can be true, false, or null.
  ///
  /// Checkbox displays a dash when its value is null.
  ///
  /// When a tri-state checkbox is tapped its [onChanged] callback will be
  /// applied to true if the current value is null or false, false otherwise.
  /// Typically tri-state checkboxes are disabled (the onChanged callback is
  /// null) so they don't respond to taps.
  ///
  /// If tristate is false (the default), [value] must not be null.
  final bool tristate;

117 118 119
  /// The width of a checkbox widget.
  static const double width = 18.0;

120 121 122 123 124
  @override
  _CheckboxState createState() => new _CheckboxState();
}

class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
125
  @override
126
  Widget build(BuildContext context) {
127
    assert(debugCheckHasMaterial(context));
128
    final ThemeData themeData = Theme.of(context);
129
    return new _CheckboxRenderObjectWidget(
130
      value: widget.value,
131
      tristate: widget.tristate,
132 133 134
      activeColor: widget.activeColor ?? themeData.accentColor,
      inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
      onChanged: widget.onChanged,
135
      vsync: this,
136
    );
137 138 139
  }
}

140
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
141
  const _CheckboxRenderObjectWidget({
142
    Key key,
143
    @required this.value,
144
    @required this.tristate,
145 146 147 148
    @required this.activeColor,
    @required this.inactiveColor,
    @required this.onChanged,
    @required this.vsync,
149 150
  }) : assert(tristate != null),
       assert(tristate || value != null),
151 152 153 154
       assert(activeColor != null),
       assert(inactiveColor != null),
       assert(vsync != null),
       super(key: key);
155 156

  final bool value;
157
  final bool tristate;
158 159
  final Color activeColor;
  final Color inactiveColor;
160
  final ValueChanged<bool> onChanged;
161
  final TickerProvider vsync;
162

163
  @override
164
  _RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox(
165
    value: value,
166
    tristate: tristate,
167 168
    activeColor: activeColor,
    inactiveColor: inactiveColor,
169 170
    onChanged: onChanged,
    vsync: vsync,
171
  );
172

173
  @override
174
  void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
175 176
    renderObject
      ..value = value
177
      ..tristate = tristate
178 179
      ..activeColor = activeColor
      ..inactiveColor = inactiveColor
180 181
      ..onChanged = onChanged
      ..vsync = vsync;
182 183 184
  }
}

185
const double _kEdgeSize = Checkbox.width;
186
const Radius _kEdgeRadius = const Radius.circular(1.0);
187 188
const double _kStrokeWidth = 2.0;

189
class _RenderCheckbox extends RenderToggleable {
190 191
  _RenderCheckbox({
    bool value,
192
    bool tristate,
193 194
    Color activeColor,
    Color inactiveColor,
195 196
    ValueChanged<bool> onChanged,
    @required TickerProvider vsync,
197 198 199 200 201 202 203 204 205 206 207 208
  }): _oldValue = value,
      super(
        value: value,
        tristate: tristate,
        activeColor: activeColor,
        inactiveColor: inactiveColor,
        onChanged: onChanged,
        size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
        vsync: vsync,
      );

  bool _oldValue;
209

210
  @override
211 212 213 214 215 216
  set value(bool newValue) {
    if (newValue == value)
      return;
    _oldValue = value;
    super.value = newValue;
  }
217

218 219 220 221 222 223 224 225 226 227
  // The square outer bounds of the checkbox at t, with the specified origin.
  // At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width)
  // At t == 0.5, .. is _kEdgeSize - _kStrokeWidth
  // At t == 1.0, .. is _kEdgeSize
  RRect _outerRectAt(Offset origin, double t) {
    final double inset = 1.0 - (t - 0.5).abs() * 2.0;
    final double size = _kEdgeSize - inset * _kStrokeWidth;
    final Rect rect = new Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size);
    return new RRect.fromRectAndRadius(rect, _kEdgeRadius);
  }
228

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
  // The checkbox's border color if value == false, or its fill color when
  // value == true or null.
  Color _colorAt(double t) {
    // As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor.
    return onChanged == null
      ? inactiveColor
      : (t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0));
  }

  // White stroke used to paint the check and dash.
  void _initStrokePaint(Paint paint) {
    paint
      ..color = const Color(0xFFFFFFFF)
      ..style = PaintingStyle.stroke
      ..strokeWidth = _kStrokeWidth;
  }

  void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) {
    assert(t >= 0.0 && t <= 0.5);
    final double size = outer.width;
    // As t goes from 0.0 to 1.0, gradually fill the outer RRect.
    final RRect inner = outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t));
    canvas.drawDRRect(outer, inner, paint);
  }

  void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
    assert(t >= 0.0 && t <= 1.0);
    // As t goes from 0.0 to 1.0, animate the two checkmark strokes from the
    // mid point outwards.
    final Path path = new Path();
    const Offset start = const Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45);
    const Offset mid = const Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7);
    const Offset end = const Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25);
    final Offset drawStart = Offset.lerp(start, mid, 1.0 - t);
    final Offset drawEnd = Offset.lerp(mid, end, t);
    path.moveTo(origin.dx + drawStart.dx, origin.dy + drawStart.dy);
    path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
    path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy);
    canvas.drawPath(path, paint);
  }
269

270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
  void _drawDash(Canvas canvas, Offset origin, double t, Paint paint) {
    assert(t >= 0.0 && t <= 1.0);
    // As t goes from 0.0 to 1.0, animate the horizontal line from the
    // mid point outwards.
    const Offset start = const Offset(_kEdgeSize * 0.2, _kEdgeSize * 0.5);
    const Offset mid = const Offset(_kEdgeSize * 0.5, _kEdgeSize * 0.5);
    const Offset end = const Offset(_kEdgeSize * 0.8, _kEdgeSize * 0.5);
    final Offset drawStart = Offset.lerp(start, mid, 1.0 - t);
    final Offset drawEnd = Offset.lerp(mid, end, t);
    canvas.drawLine(origin + drawStart, origin + drawEnd, paint);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;
285
    paintRadialReaction(canvas, offset, size.center(Offset.zero));
286

287 288 289 290 291
    final Offset origin = offset + (size / 2.0 - const Size.square(_kEdgeSize) / 2.0);
    final AnimationStatus status = position.status;
    final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed
      ? position.value
      : 1.0 - position.value;
292

293 294 295 296 297
    // Four cases: false to null, false to true, null to false, true to false
    if (_oldValue == false || value == false) {
      final double t = value == false ? 1.0 - tNormalized : tNormalized;
      final RRect outer = _outerRectAt(origin, t);
      final Paint paint = new Paint()..color = _colorAt(t);
298

299 300 301 302
      if (t <= 0.5) {
        _drawBorder(canvas, outer, t, paint);
      } else {
        canvas.drawRRect(outer, paint);
303

304 305 306 307 308 309 310 311 312 313
        _initStrokePaint(paint);
        final double tShrink = (t - 0.5) * 2.0;
        if (_oldValue == null)
          _drawDash(canvas, origin, tShrink, paint);
        else
          _drawCheck(canvas, origin, tShrink, paint);
      }
    } else { // Two cases: null to true, true to null
      final RRect outer = _outerRectAt(origin, 1.0);
      final Paint paint = new Paint() ..color = _colorAt(1.0);
314
      canvas.drawRRect(outer, paint);
315

316 317 318 319 320 321 322 323 324 325 326 327 328 329
      _initStrokePaint(paint);
      if (tNormalized <= 0.5) {
        final double tShrink = 1.0 - tNormalized * 2.0;
        if (_oldValue == true)
          _drawCheck(canvas, origin, tShrink, paint);
        else
          _drawDash(canvas, origin, tShrink, paint);
      } else {
        final double tExpand = (tNormalized - 0.5) * 2.0;
        if (value == true)
          _drawCheck(canvas, origin, tExpand, paint);
        else
          _drawDash(canvas, origin, tExpand, paint);
      }
330 331
    }
  }
332 333 334 335 336 337 338

  // TODO(hmuller): smooth segues for cases where the value changes
  // in the middle of position's animation cycle.
  // https://github.com/flutter/flutter/issues/14674

  // TODO(hmuller): accessibility support for tristate checkboxes.
  // https://github.com/flutter/flutter/issues/14677
339
}