// 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 'material_state.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. /// * /// * 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.mouseCursor, this.activeColor, this.fillColor, this.checkColor, this.focusColor, this.hoverColor, this.overlayColor, this.splashRadius, 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? onChanged; /// {@template flutter.material.checkbox.mouseCursor} /// The cursor for a mouse pointer when it enters or is hovering over the /// widget. /// /// If [mouseCursor] is a [MaterialStateProperty], /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: /// /// * [MaterialState.selected]. /// * [MaterialState.hovered]. /// * [MaterialState.focused]. /// * [MaterialState.disabled]. /// {@endtemplate} /// /// When [value] is null and [tristate] is true, [MaterialState.selected] is /// included as a state. /// /// If null, then the value of [CheckboxThemeData.mouseCursor] is used. If /// that is also null, then [MaterialStateMouseCursor.clickable] is used. /// /// See also: /// /// * [MaterialStateMouseCursor], a [MouseCursor] that implements /// `MaterialStateProperty` which is used in APIs that need to accept /// either a [MouseCursor] or a [MaterialStateProperty]. final MouseCursor? mouseCursor; /// The color to use when this checkbox is checked. /// /// Defaults to [ThemeData.toggleableActiveColor]. /// /// If [fillColor] returns a non-null color in the [MaterialState.selected] /// state, it will be used instead of this color. final Color? activeColor; /// {@template flutter.material.checkbox.fillColor} /// The color that fills the checkbox, in all [MaterialState]s. /// /// Resolves in the following states: /// * [MaterialState.selected]. /// * [MaterialState.hovered]. /// * [MaterialState.focused]. /// * [MaterialState.disabled]. /// {@endtemplate} /// /// If null, then the value of [activeColor] is used in the selected /// state. If that is also null, the value of [CheckboxThemeData.fillColor] /// is used. If that is also null, then [ThemeData.disabledColor] is used in /// the disabled state, [ThemeData.toggleableActiveColor] is used in the /// selected state, and [ThemeData.unselectedWidgetColor] is used in the /// default state. final MaterialStateProperty? fillColor; /// {@template flutter.material.checkbox.checkColor} /// The color to use for the check icon when this checkbox is checked. /// {@endtemplate} /// /// If null, then the value of [CheckboxThemeData.checkColor] is used. If /// that is also null, then Color(0xFFFFFFFF) is used. 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; /// {@template flutter.material.checkbox.materialTapTargetSize} /// Configures the minimum size of the tap target. /// {@endtemplate} /// /// If null, then the value of [CheckboxThemeData.materialTapTargetSize] is /// used. If that is also null, then the value of /// [ThemeData.materialTapTargetSize] is used. /// /// See also: /// /// * [MaterialTapTargetSize], for a description of how this affects tap targets. final MaterialTapTargetSize? materialTapTargetSize; /// {@template flutter.material.checkbox.visualDensity} /// Defines how compact the checkbox's layout will be. /// {@endtemplate} /// /// {@macro flutter.material.themedata.visualDensity} /// /// If null, then the value of [CheckboxThemeData.visualDensity] is used. If /// that is also null, then the value of [ThemeData.visualDensity] is used. /// /// See also: /// /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all /// widgets within a [Theme]. final VisualDensity? visualDensity; /// The color for the checkbox's [Material] when it has the input focus. /// /// If [overlayColor] returns a non-null color in the [MaterialState.focused] /// state, it will be used instead. /// /// If null, then the value of [CheckboxThemeData.overlayColor] is used in the /// focused state. If that is also null, then the value of /// [ThemeData.focusColor] is used. final Color? focusColor; /// The color for the checkbox's [Material] when a pointer is hovering over it. /// /// If [overlayColor] returns a non-null color in the [MaterialState.hovered] /// state, it will be used instead. /// /// If null, then the value of [CheckboxThemeData.overlayColor] is used in the /// hovered state. If that is also null, then the value of /// [ThemeData.hoverColor] is used. final Color? hoverColor; /// {@template flutter.material.checkbox.overlayColor} /// The color for the checkbox's [Material]. /// /// Resolves in the following states: /// * [MaterialState.pressed]. /// * [MaterialState.selected]. /// * [MaterialState.hovered]. /// * [MaterialState.focused]. /// {@endtemplate} /// /// If null, then the value of [activeColor] with alpha /// [kRadialReactionAlpha], [focusColor] and [hoverColor] is used in the /// pressed, focused and hovered state. If that is also null, /// the value of [CheckboxThemeData.overlayColor] is used. If that is /// also null, then the value of [ThemeData.toggleableActiveColor] with alpha /// [kRadialReactionAlpha], [ThemeData.focusColor] and [ThemeData.hoverColor] /// is used in the pressed, focused and hovered state. final MaterialStateProperty? overlayColor; /// {@template flutter.material.checkbox.splashRadius} /// The splash radius of the circular [Material] ink response. /// {@endtemplate} /// /// If null, then the value of [CheckboxThemeData.splashRadius] is used. If /// that is also null, then [kRadialReactionRadius] is used. final double? splashRadius; /// {@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 with TickerProviderStateMixin { bool get enabled => widget.onChanged != null; late Map> _actionMap; @override void initState() { super.initState(); _actionMap = >{ ActivateIntent: CallbackAction(onInvoke: _actionHandler), }; } void _actionHandler(ActivateIntent intent) { if (widget.onChanged != null) { switch (widget.value) { case false: widget.onChanged!(true); break; case true: widget.onChanged!(widget.tristate ? null : false); break; case null: widget.onChanged!(false); break; } } final RenderObject renderObject = context.findRenderObject()!; renderObject.sendSemanticsEvent(const TapSemanticEvent()); } 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; }); } } Set get _states => { if (!enabled) MaterialState.disabled, if (_hovering) MaterialState.hovered, if (_focused) MaterialState.focused, if (widget.value == null || widget.value!) MaterialState.selected, }; MaterialStateProperty get _widgetFillColor { return MaterialStateProperty.resolveWith((Set states) { if (states.contains(MaterialState.disabled)) { return null; } if (states.contains(MaterialState.selected)) { return widget.activeColor; } return null; }); } MaterialStateProperty get _defaultFillColor { final ThemeData themeData = Theme.of(context); return MaterialStateProperty.resolveWith((Set states) { if (states.contains(MaterialState.disabled)) { return themeData.disabledColor; } if (states.contains(MaterialState.selected)) { return themeData.toggleableActiveColor; } return themeData.unselectedWidgetColor; }); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); final ThemeData themeData = Theme.of(context); final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize ?? themeData.checkboxTheme.materialTapTargetSize ?? themeData.materialTapTargetSize; final VisualDensity effectiveVisualDensity = widget.visualDensity ?? themeData.checkboxTheme.visualDensity ?? themeData.visualDensity; Size size; switch (effectiveMaterialTapTargetSize) { case MaterialTapTargetSize.padded: size = const Size(kMinInteractiveDimension, kMinInteractiveDimension); break; case MaterialTapTargetSize.shrinkWrap: size = const Size(kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0); break; } size += effectiveVisualDensity.baseSizeAdjustment; final BoxConstraints additionalConstraints = BoxConstraints.tight(size); final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs(widget.mouseCursor, _states) ?? themeData.checkboxTheme.mouseCursor?.resolve(_states) ?? MaterialStateProperty.resolveAs(MaterialStateMouseCursor.clickable, _states); // Colors need to be resolved in selected and non selected states separately // so that they can be lerped between. final Set activeStates = _states..add(MaterialState.selected); final Set inactiveStates = _states..remove(MaterialState.selected); final Color effectiveActiveColor = widget.fillColor?.resolve(activeStates) ?? _widgetFillColor.resolve(activeStates) ?? themeData.checkboxTheme.fillColor?.resolve(activeStates) ?? _defaultFillColor.resolve(activeStates); final Color effectiveInactiveColor = widget.fillColor?.resolve(inactiveStates) ?? _widgetFillColor.resolve(inactiveStates) ?? themeData.checkboxTheme.fillColor?.resolve(inactiveStates) ?? _defaultFillColor.resolve(inactiveStates); final Set focusedStates = _states..add(MaterialState.focused); final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates) ?? widget.focusColor ?? themeData.checkboxTheme.overlayColor?.resolve(focusedStates) ?? themeData.focusColor; final Set hoveredStates = _states..add(MaterialState.hovered); final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates) ?? widget.hoverColor ?? themeData.checkboxTheme.overlayColor?.resolve(hoveredStates) ?? themeData.hoverColor; final Set activePressedStates = activeStates..add(MaterialState.pressed); final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates) ?? themeData.checkboxTheme.overlayColor?.resolve(activePressedStates) ?? effectiveActiveColor.withAlpha(kRadialReactionAlpha); final Set inactivePressedStates = inactiveStates..add(MaterialState.pressed); final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates) ?? themeData.checkboxTheme.overlayColor?.resolve(inactivePressedStates) ?? effectiveActiveColor.withAlpha(kRadialReactionAlpha); final Color effectiveCheckColor = widget.checkColor ?? themeData.checkboxTheme.checkColor?.resolve(_states) ?? const Color(0xFFFFFFFF); return FocusableActionDetector( actions: _actionMap, focusNode: widget.focusNode, autofocus: widget.autofocus, enabled: enabled, onShowFocusHighlight: _handleFocusHighlightChanged, onShowHoverHighlight: _handleHoverChanged, mouseCursor: effectiveMouseCursor, child: Builder( builder: (BuildContext context) { return _CheckboxRenderObjectWidget( value: widget.value, tristate: widget.tristate, activeColor: effectiveActiveColor, checkColor: effectiveCheckColor, inactiveColor: effectiveInactiveColor, focusColor: effectiveFocusOverlayColor, hoverColor: effectiveHoverOverlayColor, reactionColor: effectiveActivePressedOverlayColor, inactiveReactionColor: effectiveInactivePressedOverlayColor, splashRadius: widget.splashRadius ?? themeData.checkboxTheme.splashRadius ?? kRadialReactionRadius, 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.reactionColor, required this.inactiveReactionColor, required this.splashRadius, 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 Color reactionColor; final Color inactiveReactionColor; final double splashRadius; final ValueChanged? 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, reactionColor: reactionColor, inactiveReactionColor: inactiveReactionColor, splashRadius: splashRadius, onChanged: onChanged, vsync: vsync, additionalConstraints: additionalConstraints, hasFocus: hasFocus, hovering: hovering, ); @override void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) { renderObject // The `tristate` must be changed before `value` due to the assertion at // the beginning of `set value`. ..tristate = tristate ..value = value ..activeColor = activeColor ..checkColor = checkColor ..inactiveColor = inactiveColor ..focusColor = focusColor ..hoverColor = hoverColor ..reactionColor = reactionColor ..inactiveReactionColor = inactiveReactionColor ..splashRadius = splashRadius ..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, required bool tristate, required Color activeColor, required this.checkColor, required Color inactiveColor, Color? focusColor, Color? hoverColor, Color? reactionColor, Color? inactiveReactionColor, required double splashRadius, required BoxConstraints additionalConstraints, ValueChanged? onChanged, required bool hasFocus, required bool hovering, required TickerProvider vsync, }) : _oldValue = value, super( value: value, tristate: tristate, activeColor: activeColor, inactiveColor: inactiveColor, focusColor: focusColor, hoverColor: hoverColor, reactionColor: reactionColor, inactiveReactionColor: inactiveReactionColor, splashRadius: splashRadius, 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 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); } } } }