// 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); 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); } } } }