| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'constants.dart'; |
| import 'debug.dart'; |
| import 'theme.dart'; |
| import 'theme_data.dart'; |
| import 'toggleable.dart'; |
| |
| /// A material design checkbox. |
| /// |
| /// 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. |
| /// |
| /// 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. |
| /// |
| /// Requires one of its ancestors to be a [Material] widget. |
| /// |
| /// See also: |
| /// |
| /// * [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]. |
| /// * [Radio], for selecting among a set of explicit values. |
| /// * [Slider], for selecting a value in a range. |
| /// * <https://material.io/design/components/selection-controls.html#checkboxes> |
| /// * <https://material.io/design/components/lists.html#types> |
| class Checkbox extends StatefulWidget { |
| /// Creates a material design checkbox. |
| /// |
| /// 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. |
| /// |
| /// The following arguments are required: |
| /// |
| /// * [value], which determines whether the checkbox is checked. The [value] |
| /// can only be null if [tristate] is true. |
| /// * [onChanged], which is called when the value of the checkbox should |
| /// change. It can be set to null to disable the checkbox. |
| /// |
| /// The values of [tristate] and [autofocus] must not be null. |
| const Checkbox({ |
| Key key, |
| @required this.value, |
| this.tristate = false, |
| @required this.onChanged, |
| this.activeColor, |
| this.checkColor, |
| this.focusColor, |
| this.hoverColor, |
| this.materialTapTargetSize, |
| this.visualDensity, |
| this.focusNode, |
| this.autofocus = false, |
| }) : assert(tristate != null), |
| assert(tristate || value != null), |
| assert(autofocus != null), |
| super(key: key); |
| |
| /// Whether this checkbox is checked. |
| /// |
| /// This property must not be null. |
| final bool value; |
| |
| /// Called when the value of the checkbox should change. |
| /// |
| /// 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 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. |
| /// |
| /// The callback provided to [onChanged] should update the state of the parent |
| /// [StatefulWidget] using the [State.setState] method, so that the parent |
| /// gets rebuilt; for example: |
| /// |
| /// ```dart |
| /// Checkbox( |
| /// value: _throwShotAway, |
| /// onChanged: (bool newValue) { |
| /// setState(() { |
| /// _throwShotAway = newValue; |
| /// }); |
| /// }, |
| /// ) |
| /// ``` |
| final ValueChanged<bool> onChanged; |
| |
| /// The color to use when this checkbox is checked. |
| /// |
| /// Defaults to [ThemeData.toggleableActiveColor]. |
| final Color activeColor; |
| |
| /// The color to use for the check icon when this checkbox is checked. |
| /// |
| /// Defaults to Color(0xFFFFFFFF) |
| final Color checkColor; |
| |
| /// 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 ([tristate] is true) is tapped, its [onChanged] |
| /// callback will be applied to true if the current value is false, to null if |
| /// value is true, and to false if value is null (i.e. it cycles through false |
| /// => true => null => false when tapped). |
| /// |
| /// If tristate is false (the default), [value] must not be null. |
| final bool tristate; |
| |
| /// Configures the minimum size of the tap target. |
| /// |
| /// Defaults to [ThemeData.materialTapTargetSize]. |
| /// |
| /// See also: |
| /// |
| /// * [MaterialTapTargetSize], for a description of how this affects tap targets. |
| final MaterialTapTargetSize materialTapTargetSize; |
| |
| /// Defines how compact the checkbox's layout will be. |
| /// |
| /// {@macro flutter.material.themedata.visualDensity} |
| /// |
| /// See also: |
| /// |
| /// * [ThemeData.visualDensity], which specifies the [density] for all widgets |
| /// within a [Theme]. |
| final VisualDensity visualDensity; |
| |
| /// The color for the checkbox's [Material] when it has the input focus. |
| final Color focusColor; |
| |
| /// The color for the checkbox's [Material] when a pointer is hovering over it. |
| final Color hoverColor; |
| |
| /// {@macro flutter.widgets.Focus.focusNode} |
| final FocusNode focusNode; |
| |
| /// {@macro flutter.widgets.Focus.autofocus} |
| final bool autofocus; |
| |
| /// The width of a checkbox widget. |
| static const double width = 18.0; |
| |
| @override |
| _CheckboxState createState() => _CheckboxState(); |
| } |
| |
| class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { |
| bool get enabled => widget.onChanged != null; |
| Map<LocalKey, ActionFactory> _actionMap; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _actionMap = <LocalKey, ActionFactory>{ |
| ActivateAction.key: _createAction, |
| }; |
| } |
| |
| void _actionHandler(FocusNode node, Intent intent){ |
| if (widget.onChanged != null) { |
| switch (widget.value) { |
| case false: |
| widget.onChanged(true); |
| break; |
| case true: |
| widget.onChanged(widget.tristate ? null : false); |
| break; |
| default: // case null: |
| widget.onChanged(false); |
| break; |
| } |
| } |
| final RenderObject renderObject = node.context.findRenderObject(); |
| renderObject.sendSemanticsEvent(const TapSemanticEvent()); |
| } |
| |
| Action _createAction() { |
| return CallbackAction( |
| ActivateAction.key, |
| onInvoke: _actionHandler, |
| ); |
| } |
| |
| bool _focused = false; |
| void _handleFocusHighlightChanged(bool focused) { |
| if (focused != _focused) { |
| setState(() { _focused = focused; }); |
| } |
| } |
| |
| bool _hovering = false; |
| void _handleHoverChanged(bool hovering) { |
| if (hovering != _hovering) { |
| setState(() { _hovering = hovering; }); |
| } |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMaterial(context)); |
| final ThemeData themeData = Theme.of(context); |
| Size size; |
| switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) { |
| case MaterialTapTargetSize.padded: |
| size = const Size(2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0); |
| break; |
| case MaterialTapTargetSize.shrinkWrap: |
| size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius); |
| break; |
| } |
| size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment; |
| final BoxConstraints additionalConstraints = BoxConstraints.tight(size); |
| return FocusableActionDetector( |
| actions: _actionMap, |
| focusNode: widget.focusNode, |
| autofocus: widget.autofocus, |
| enabled: enabled, |
| onShowFocusHighlight: _handleFocusHighlightChanged, |
| onShowHoverHighlight: _handleHoverChanged, |
| child: Builder( |
| builder: (BuildContext context) { |
| return _CheckboxRenderObjectWidget( |
| value: widget.value, |
| tristate: widget.tristate, |
| activeColor: widget.activeColor ?? themeData.toggleableActiveColor, |
| checkColor: widget.checkColor ?? const Color(0xFFFFFFFF), |
| inactiveColor: enabled ? themeData.unselectedWidgetColor : themeData.disabledColor, |
| focusColor: widget.focusColor ?? themeData.focusColor, |
| hoverColor: widget.hoverColor ?? themeData.hoverColor, |
| onChanged: widget.onChanged, |
| additionalConstraints: additionalConstraints, |
| vsync: this, |
| hasFocus: _focused, |
| hovering: _hovering, |
| ); |
| }, |
| ), |
| ); |
| } |
| } |
| |
| class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { |
| const _CheckboxRenderObjectWidget({ |
| Key key, |
| @required this.value, |
| @required this.tristate, |
| @required this.activeColor, |
| @required this.checkColor, |
| @required this.inactiveColor, |
| @required this.focusColor, |
| @required this.hoverColor, |
| @required this.onChanged, |
| @required this.vsync, |
| @required this.additionalConstraints, |
| @required this.hasFocus, |
| @required this.hovering, |
| }) : assert(tristate != null), |
| assert(tristate || value != null), |
| assert(activeColor != null), |
| assert(inactiveColor != null), |
| assert(vsync != null), |
| super(key: key); |
| |
| final bool value; |
| final bool tristate; |
| final bool hasFocus; |
| final bool hovering; |
| final Color activeColor; |
| final Color checkColor; |
| final Color inactiveColor; |
| final Color focusColor; |
| final Color hoverColor; |
| final ValueChanged<bool> onChanged; |
| final TickerProvider vsync; |
| final BoxConstraints additionalConstraints; |
| |
| @override |
| _RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox( |
| value: value, |
| tristate: tristate, |
| activeColor: activeColor, |
| checkColor: checkColor, |
| inactiveColor: inactiveColor, |
| focusColor: focusColor, |
| hoverColor: hoverColor, |
| onChanged: onChanged, |
| vsync: vsync, |
| additionalConstraints: additionalConstraints, |
| hasFocus: hasFocus, |
| hovering: hovering, |
| ); |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) { |
| renderObject |
| ..value = value |
| ..tristate = tristate |
| ..activeColor = activeColor |
| ..checkColor = checkColor |
| ..inactiveColor = inactiveColor |
| ..focusColor = focusColor |
| ..hoverColor = hoverColor |
| ..onChanged = onChanged |
| ..additionalConstraints = additionalConstraints |
| ..vsync = vsync |
| ..hasFocus = hasFocus |
| ..hovering = hovering; |
| } |
| } |
| |
| const double _kEdgeSize = Checkbox.width; |
| const Radius _kEdgeRadius = Radius.circular(1.0); |
| const double _kStrokeWidth = 2.0; |
| |
| class _RenderCheckbox extends RenderToggleable { |
| _RenderCheckbox({ |
| bool value, |
| bool tristate, |
| Color activeColor, |
| this.checkColor, |
| Color inactiveColor, |
| Color focusColor, |
| Color hoverColor, |
| BoxConstraints additionalConstraints, |
| ValueChanged<bool> onChanged, |
| bool hasFocus, |
| bool hovering, |
| @required TickerProvider vsync, |
| }) : _oldValue = value, |
| super( |
| value: value, |
| tristate: tristate, |
| activeColor: activeColor, |
| inactiveColor: inactiveColor, |
| focusColor: focusColor, |
| hoverColor: hoverColor, |
| onChanged: onChanged, |
| additionalConstraints: additionalConstraints, |
| vsync: vsync, |
| hasFocus: hasFocus, |
| hovering: hovering, |
| ); |
| |
| bool _oldValue; |
| Color checkColor; |
| |
| @override |
| set value(bool newValue) { |
| if (newValue == value) |
| return; |
| _oldValue = value; |
| super.value = newValue; |
| } |
| |
| @override |
| void describeSemanticsConfiguration(SemanticsConfiguration config) { |
| super.describeSemanticsConfiguration(config); |
| config.isChecked = value == true; |
| } |
| |
| // 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 = Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size); |
| return RRect.fromRectAndRadius(rect, _kEdgeRadius); |
| } |
| |
| // 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. |
| Paint _createStrokePaint() { |
| return Paint() |
| ..color = checkColor |
| ..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 check mark strokes from the |
| // short side to the long side. |
| final Path path = Path(); |
| const Offset start = Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45); |
| const Offset mid = Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7); |
| const Offset end = Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25); |
| if (t < 0.5) { |
| final double strokeT = t * 2.0; |
| final Offset drawMid = Offset.lerp(start, mid, strokeT); |
| path.moveTo(origin.dx + start.dx, origin.dy + start.dy); |
| path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy); |
| } else { |
| final double strokeT = (t - 0.5) * 2.0; |
| final Offset drawEnd = Offset.lerp(mid, end, strokeT); |
| path.moveTo(origin.dx + start.dx, origin.dy + start.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); |
| } |
| |
| 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 = Offset(_kEdgeSize * 0.2, _kEdgeSize * 0.5); |
| const Offset mid = Offset(_kEdgeSize * 0.5, _kEdgeSize * 0.5); |
| const Offset end = 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; |
| paintRadialReaction(canvas, offset, size.center(Offset.zero)); |
| |
| final Paint strokePaint = _createStrokePaint(); |
| final Offset origin = offset + (size / 2.0 - const Size.square(_kEdgeSize) / 2.0 as Offset); |
| final AnimationStatus status = position.status; |
| final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed |
| ? position.value |
| : 1.0 - position.value; |
| |
| // 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 = Paint()..color = _colorAt(t); |
| |
| if (t <= 0.5) { |
| _drawBorder(canvas, outer, t, paint); |
| } else { |
| canvas.drawRRect(outer, paint); |
| |
| final double tShrink = (t - 0.5) * 2.0; |
| if (_oldValue == null || value == null) |
| _drawDash(canvas, origin, tShrink, strokePaint); |
| else |
| _drawCheck(canvas, origin, tShrink, strokePaint); |
| } |
| } else { // Two cases: null to true, true to null |
| final RRect outer = _outerRectAt(origin, 1.0); |
| final Paint paint = Paint() ..color = _colorAt(1.0); |
| canvas.drawRRect(outer, paint); |
| |
| if (tNormalized <= 0.5) { |
| final double tShrink = 1.0 - tNormalized * 2.0; |
| if (_oldValue == true) |
| _drawCheck(canvas, origin, tShrink, strokePaint); |
| else |
| _drawDash(canvas, origin, tShrink, strokePaint); |
| } else { |
| final double tExpand = (tNormalized - 0.5) * 2.0; |
| if (value == true) |
| _drawCheck(canvas, origin, tExpand, strokePaint); |
| else |
| _drawDash(canvas, origin, tExpand, strokePaint); |
| } |
| } |
| } |
| } |