checkbox.dart 6.37 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 8
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
9
import 'package:meta/meta.dart';
10

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

16 17 18
/// A material design checkbox
///
/// 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
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
27
///
28 29 30 31 32
///  * [Radio]
///  * [Switch]
///  * [Slider]
///  * <https://www.google.com/design/spec/components/selection-controls.html#selection-controls-checkbox>
///  * <https://www.google.com/design/spec/components/lists-controls.html#lists-controls-types-of-list-controls>
33
class Checkbox extends StatefulWidget {
34
  /// Creates a material design checkbox.
35
  ///
36 37 38 39 40 41 42 43 44
  /// 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.
  ///
  /// * [value] determines whether the checkbox is checked.
  /// * [onChanged] is called when the value of the checkbox should change.
  Checkbox({
45
    Key key,
46 47 48
    @required this.value,
    @required this.onChanged,
    this.activeColor
49
  }) : super(key: key);
50

51
  /// Whether this checkbox is checked.
52
  final bool value;
53

54
  /// Called when the value of the checkbox should change.
55 56 57 58 59 60
  ///
  /// 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.
  ///
  /// If null, the checkbox will be displayed as disabled.
Hixie's avatar
Hixie committed
61
  final ValueChanged<bool> onChanged;
62

63 64 65 66 67
  /// The color to use when this checkbox is checked.
  ///
  /// Defaults to accent color of the current [Theme].
  final Color activeColor;

68 69 70
  /// The width of a checkbox widget.
  static const double width = 18.0;

71 72 73 74 75
  @override
  _CheckboxState createState() => new _CheckboxState();
}

class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
76
  @override
77
  Widget build(BuildContext context) {
78
    assert(debugCheckHasMaterial(context));
79
    ThemeData themeData = Theme.of(context);
80
    return new _CheckboxRenderObjectWidget(
81 82 83 84 85
      value: config.value,
      activeColor: config.activeColor ?? themeData.accentColor,
      inactiveColor: config.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
      onChanged: config.onChanged,
      vsync: this,
86
    );
87 88 89
  }
}

90 91
class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
  _CheckboxRenderObjectWidget({
92
    Key key,
93 94 95 96 97
    @required this.value,
    @required this.activeColor,
    @required this.inactiveColor,
    @required this.onChanged,
    @required this.vsync,
98
  }) : super(key: key) {
99 100 101
    assert(value != null);
    assert(activeColor != null);
    assert(inactiveColor != null);
102
    assert(vsync != null);
103
  }
104 105

  final bool value;
106 107
  final Color activeColor;
  final Color inactiveColor;
108
  final ValueChanged<bool> onChanged;
109
  final TickerProvider vsync;
110

111
  @override
112
  _RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox(
113
    value: value,
114 115
    activeColor: activeColor,
    inactiveColor: inactiveColor,
116 117
    onChanged: onChanged,
    vsync: vsync,
118
  );
119

120
  @override
121
  void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
122 123 124 125
    renderObject
      ..value = value
      ..activeColor = activeColor
      ..inactiveColor = inactiveColor
126 127
      ..onChanged = onChanged
      ..vsync = vsync;
128 129 130
  }
}

131
const double _kMidpoint = 0.5;
132
const double _kEdgeSize = Checkbox.width;
133
const Radius _kEdgeRadius = const Radius.circular(1.0);
134 135
const double _kStrokeWidth = 2.0;

136
class _RenderCheckbox extends RenderToggleable {
137 138
  _RenderCheckbox({
    bool value,
139 140
    Color activeColor,
    Color inactiveColor,
141 142
    ValueChanged<bool> onChanged,
    @required TickerProvider vsync,
143 144 145 146 147
  }): super(
    value: value,
    activeColor: activeColor,
    inactiveColor: inactiveColor,
    onChanged: onChanged,
148 149
    size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
    vsync: vsync,
150
  );
151

152
  @override
153
  void paint(PaintingContext context, Offset offset) {
154

155 156
    final Canvas canvas = context.canvas;

157 158 159 160
    final double offsetX = offset.dx + (size.width - _kEdgeSize) / 2.0;
    final double offsetY = offset.dy + (size.height - _kEdgeSize) / 2.0;

    paintRadialReaction(canvas, offset, size.center(Point.origin));
161

162 163 164 165 166 167
    double t = position.value;

    Color borderColor = inactiveColor;
    if (onChanged != null)
      borderColor = t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0);

168
    Paint paint = new Paint()
169
      ..color = borderColor;
170

171
    double inset = 1.0 - (t - 0.5).abs() * 2.0;
172
    double rectSize = _kEdgeSize - inset * _kStrokeWidth;
173
    Rect rect = new Rect.fromLTWH(offsetX + inset, offsetY + inset, rectSize, rectSize);
174

175
    RRect outer = new RRect.fromRectAndRadius(rect, _kEdgeRadius);
176 177 178 179 180 181 182
    if (t <= 0.5) {
      // Outline
      RRect inner = outer.deflate(math.min(rectSize / 2.0, _kStrokeWidth + rectSize * t));
      canvas.drawDRRect(outer, inner, paint);
    } else {
      // Background
      canvas.drawRRect(outer, paint);
183 184

      // White inner check
185
      double value = (t - 0.5) * 2.0;
186 187
      paint
        ..color = const Color(0xFFFFFFFF)
188 189
        ..style = PaintingStyle.stroke
        ..strokeWidth = _kStrokeWidth;
190 191 192 193
      Path path = new Path();
      Point start = new Point(_kEdgeSize * 0.15, _kEdgeSize * 0.45);
      Point mid = new Point(_kEdgeSize * 0.4, _kEdgeSize * 0.7);
      Point end = new Point(_kEdgeSize * 0.85, _kEdgeSize * 0.25);
194 195
      Point drawStart = Point.lerp(start, mid, 1.0 - value);
      Point drawEnd = Point.lerp(mid, end, value);
196 197 198
      path.moveTo(offsetX + drawStart.x, offsetY + drawStart.y);
      path.lineTo(offsetX + mid.x, offsetY + mid.y);
      path.lineTo(offsetX + drawEnd.x, offsetY + drawEnd.y);
199 200 201 202
      canvas.drawPath(path, paint);
    }
  }
}