checkbox.dart 36.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/cupertino.dart';
6

7
import 'checkbox_theme.dart';
8 9
import 'color_scheme.dart';
import 'colors.dart';
10
import 'constants.dart';
11
import 'debug.dart';
12
import 'material_state.dart';
13
import 'theme.dart';
14
import 'theme_data.dart';
15
import 'toggleable.dart';
16

17 18 19 20
// Examples can assume:
// bool _throwShotAway = false;
// late StateSetter setState;

21 22
enum _CheckboxType { material, adaptive }

23
/// A Material Design checkbox.
24 25
///
/// The checkbox itself does not maintain any state. Instead, when the state of
26 27 28
/// 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
29 30
/// the checkbox.
///
31 32 33 34
/// 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.
///
35 36
/// Requires one of its ancestors to be a [Material] widget.
///
37
/// {@tool dartpad}
38
/// This example shows how you can override the default theme of
39
/// a [Checkbox] with a [MaterialStateProperty].
40 41 42 43
/// In this example, the checkbox's color will be `Colors.blue` when the [Checkbox]
/// is being pressed, hovered, or focused. Otherwise, the checkbox's color will
/// be `Colors.red`.
///
44
/// ** See code in examples/api/lib/material/checkbox/checkbox.0.dart **
45 46
/// {@end-tool}
///
47 48 49 50 51 52
/// {@tool dartpad}
/// This example shows what the checkbox error state looks like.
///
/// ** See code in examples/api/lib/material/checkbox/checkbox.1.dart **
/// {@end-tool}
///
53
/// See also:
54
///
55 56 57
///  * [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].
58 59
///  * [Radio], for selecting among a set of explicit values.
///  * [Slider], for selecting a value in a range.
60 61
///  * <https://material.io/design/components/selection-controls.html#checkboxes>
///  * <https://material.io/design/components/lists.html#types>
62
class Checkbox extends StatefulWidget {
63
  /// Creates a Material Design checkbox.
64
  ///
65 66 67 68 69 70
  /// 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.
  ///
71 72
  /// The following arguments are required:
  ///
73
  /// * [value], which determines whether the checkbox is checked. The [value]
74
  ///   can only be null if [tristate] is true.
75 76
  /// * [onChanged], which is called when the value of the checkbox should
  ///   change. It can be set to null to disable the checkbox.
77
  const Checkbox({
78
    super.key,
79
    required this.value,
80
    this.tristate = false,
81
    required this.onChanged,
82
    this.mouseCursor,
83
    this.activeColor,
84
    this.fillColor,
85
    this.checkColor,
86 87
    this.focusColor,
    this.hoverColor,
88
    this.overlayColor,
89
    this.splashRadius,
90
    this.materialTapTargetSize,
91
    this.visualDensity,
92 93
    this.focusNode,
    this.autofocus = false,
94 95
    this.shape,
    this.side,
96
    this.isError = false,
97
    this.semanticLabel,
98 99 100 101 102 103 104 105 106 107 108 109 110
  }) : _checkboxType = _CheckboxType.material,
       assert(tristate || value != null);

  /// Creates an adaptive [Checkbox] based on whether the target platform is iOS
  /// or macOS, following Material design's
  /// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
  ///
  /// On iOS and macOS, this constructor creates a [CupertinoCheckbox], which has
  /// matching functionality and presentation as Material checkboxes, and are the
  /// graphics expected on iOS. On other platforms, this creates a Material
  /// design [Checkbox].
  ///
  /// If a [CupertinoCheckbox] is created, the following parameters are ignored:
111
  /// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius],
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
  /// [materialTapTargetSize], [visualDensity], [isError]. However, [shape] and
  /// [side] will still affect the [CupertinoCheckbox] and should be handled if
  /// native fidelity is important.
  ///
  /// The target platform is based on the current [Theme]: [ThemeData.platform].
  const Checkbox.adaptive({
    super.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,
    this.shape,
    this.side,
    this.isError = false,
137
    this.semanticLabel,
138 139
  }) : _checkboxType = _CheckboxType.adaptive,
       assert(tristate || value != null);
140

141
  /// Whether this checkbox is checked.
142
  ///
143 144
  /// When [tristate] is true, a value of null corresponds to the mixed state.
  /// When [tristate] is false, this value must not be null.
145
  final bool? value;
146

147
  /// Called when the value of the checkbox should change.
148 149 150 151 152
  ///
  /// 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.
  ///
153 154 155 156 157 158
  /// 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.
159
  ///
160
  /// The callback provided to [onChanged] should update the state of the parent
161 162 163 164
  /// [StatefulWidget] using the [State.setState] method, so that the parent
  /// gets rebuilt; for example:
  ///
  /// ```dart
165
  /// Checkbox(
166
  ///   value: _throwShotAway,
Abhishek Ghaskata's avatar
Abhishek Ghaskata committed
167
  ///   onChanged: (bool? newValue) {
168
  ///     setState(() {
Abhishek Ghaskata's avatar
Abhishek Ghaskata committed
169
  ///       _throwShotAway = newValue!;
170 171
  ///     });
  ///   },
172
  /// )
173
  /// ```
174
  final ValueChanged<bool?>? onChanged;
175

176
  /// {@template flutter.material.checkbox.mouseCursor}
177 178 179 180 181 182 183 184 185 186
  /// The cursor for a mouse pointer when it enters or is hovering over the
  /// widget.
  ///
  /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
  /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
  ///
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
187
  /// {@endtemplate}
188 189 190 191
  ///
  /// When [value] is null and [tristate] is true, [MaterialState.selected] is
  /// included as a state.
  ///
192 193 194 195 196 197 198 199
  /// 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<MouseCursor>].
200
  final MouseCursor? mouseCursor;
201

202 203
  /// The color to use when this checkbox is checked.
  ///
204
  /// Defaults to [ColorScheme.secondary].
205 206 207
  ///
  /// If [fillColor] returns a non-null color in the [MaterialState.selected]
  /// state, it will be used instead of this color.
208
  final Color? activeColor;
209

210 211
  /// {@template flutter.material.checkbox.fillColor}
  /// The color that fills the checkbox, in all [MaterialState]s.
212 213 214 215 216 217
  ///
  /// Resolves in the following states:
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
218 219 220 221 222 223 224 225 226 227
  ///
  /// {@tool snippet}
  /// This example resolves the [fillColor] based on the current [MaterialState]
  /// of the [Checkbox], providing a different [Color] when it is
  /// [MaterialState.disabled].
  ///
  /// ```dart
  /// Checkbox(
  ///   value: true,
  ///   onChanged: (_){},
228
  ///   fillColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
229 230 231 232 233 234 235 236
  ///     if (states.contains(MaterialState.disabled)) {
  ///       return Colors.orange.withOpacity(.32);
  ///     }
  ///     return Colors.orange;
  ///   })
  /// )
  /// ```
  /// {@end-tool}
237 238 239 240 241
  /// {@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
242
  /// the disabled state, [ColorScheme.secondary] is used in the
243 244
  /// selected state, and [ThemeData.unselectedWidgetColor] is used in the
  /// default state.
245 246
  final MaterialStateProperty<Color?>? fillColor;

247
  /// {@template flutter.material.checkbox.checkColor}
248
  /// The color to use for the check icon when this checkbox is checked.
249
  /// {@endtemplate}
250
  ///
251 252
  /// If null, then the value of [CheckboxThemeData.checkColor] is used. If
  /// that is also null, then Color(0xFFFFFFFF) is used.
253
  final Color? checkColor;
254

255 256
  /// If true the checkbox's [value] can be true, false, or null.
  ///
257
  /// [Checkbox] displays a dash when its value is null.
258
  ///
259 260 261 262
  /// 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).
263 264 265 266
  ///
  /// If tristate is false (the default), [value] must not be null.
  final bool tristate;

267
  /// {@template flutter.material.checkbox.materialTapTargetSize}
268
  /// Configures the minimum size of the tap target.
269
  /// {@endtemplate}
270
  ///
271 272 273
  /// If null, then the value of [CheckboxThemeData.materialTapTargetSize] is
  /// used. If that is also null, then the value of
  /// [ThemeData.materialTapTargetSize] is used.
274 275 276
  ///
  /// See also:
  ///
277
  ///  * [MaterialTapTargetSize], for a description of how this affects tap targets.
278
  final MaterialTapTargetSize? materialTapTargetSize;
279

280
  /// {@template flutter.material.checkbox.visualDensity}
281
  /// Defines how compact the checkbox's layout will be.
282
  /// {@endtemplate}
283 284 285
  ///
  /// {@macro flutter.material.themedata.visualDensity}
  ///
286 287 288
  /// If null, then the value of [CheckboxThemeData.visualDensity] is used. If
  /// that is also null, then the value of [ThemeData.visualDensity] is used.
  ///
289 290
  /// See also:
  ///
291 292
  ///  * [ThemeData.visualDensity], which specifies the [visualDensity] for all
  ///    widgets within a [Theme].
293
  final VisualDensity? visualDensity;
294

295
  /// The color for the checkbox's [Material] when it has the input focus.
296
  ///
297 298 299
  /// If [overlayColor] returns a non-null color in the [MaterialState.focused]
  /// state, it will be used instead.
  ///
300 301 302
  /// 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.
303
  final Color? focusColor;
304

305
  /// {@template flutter.material.checkbox.hoverColor}
306
  /// The color for the checkbox's [Material] when a pointer is hovering over it.
307
  ///
308 309
  /// If [overlayColor] returns a non-null color in the [MaterialState.hovered]
  /// state, it will be used instead.
310
  /// {@endtemplate}
311
  ///
312 313 314
  /// 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.
315
  final Color? hoverColor;
316

317 318 319 320 321 322 323 324 325 326 327 328 329 330
  /// {@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
331
  /// also null, then the value of [ColorScheme.secondary] with alpha
332 333 334 335
  /// [kRadialReactionAlpha], [ThemeData.focusColor] and [ThemeData.hoverColor]
  /// is used in the pressed, focused and hovered state.
  final MaterialStateProperty<Color?>? overlayColor;

336
  /// {@template flutter.material.checkbox.splashRadius}
337
  /// The splash radius of the circular [Material] ink response.
338
  /// {@endtemplate}
339
  ///
340 341
  /// If null, then the value of [CheckboxThemeData.splashRadius] is used. If
  /// that is also null, then [kRadialReactionRadius] is used.
342 343
  final double? splashRadius;

344
  /// {@macro flutter.widgets.Focus.focusNode}
345
  final FocusNode? focusNode;
346 347 348 349

  /// {@macro flutter.widgets.Focus.autofocus}
  final bool autofocus;

350 351 352 353 354 355
  /// {@template flutter.material.checkbox.shape}
  /// The shape of the checkbox's [Material].
  /// {@endtemplate}
  ///
  /// If this property is null then [CheckboxThemeData.shape] of [ThemeData.checkboxTheme]
  /// is used. If that's null then the shape will be a [RoundedRectangleBorder]
356
  /// with a circular corner radius of 1.0 in Material 2, and 2.0 in Material 3.
357 358 359
  final OutlinedBorder? shape;

  /// {@template flutter.material.checkbox.side}
360 361 362 363 364 365 366 367 368 369 370 371
  /// The color and width of the checkbox's border.
  ///
  /// This property can be a [MaterialStateBorderSide] that can
  /// specify different border color and widths depending on the
  /// checkbox's state.
  ///
  /// Resolves in the following states:
  ///  * [MaterialState.pressed].
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
372
  ///  * [MaterialState.error].
373 374 375 376 377
  ///
  /// If this property is not a [MaterialStateBorderSide] and it is
  /// non-null, then it is only rendered when the checkbox's value is
  /// false. The difference in interpretation is for backwards
  /// compatibility.
378 379
  /// {@endtemplate}
  ///
380 381 382
  /// If this property is null, then [CheckboxThemeData.side] of
  /// [ThemeData.checkboxTheme] is used. If that is also null, then the side
  /// will be width 2.
383 384
  final BorderSide? side;

385
  /// {@template flutter.material.checkbox.isError}
386 387 388 389
  /// True if this checkbox wants to show an error state.
  ///
  /// The checkbox will have different default container color and check color when
  /// this is true. This is only used when [ThemeData.useMaterial3] is set to true.
390
  /// {@endtemplate}
391
  ///
392
  /// Defaults to false.
393 394
  final bool isError;

395 396 397 398 399 400 401 402 403
  /// {@template flutter.material.checkbox.semanticLabel}
  /// The semantic label for the checkobox that will be announced by screen readers.
  ///
  /// This is announced in accessibility modes (e.g TalkBack/VoiceOver).
  ///
  /// This label does not show in the UI.
  /// {@endtemplate}
  final String? semanticLabel;

404 405 406
  /// The width of a checkbox widget.
  static const double width = 18.0;

407 408
  final _CheckboxType _checkboxType;

409
  @override
410
  State<Checkbox> createState() => _CheckboxState();
411 412
}

413 414 415
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, ToggleableStateMixin {
  final _CheckboxPainter _painter = _CheckboxPainter();
  bool? _previousValue;
416 417 418 419

  @override
  void initState() {
    super.initState();
420
    _previousValue = widget.value;
421 422
  }

423 424 425 426 427 428
  @override
  void didUpdateWidget(Checkbox oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.value != widget.value) {
      _previousValue = oldWidget.value;
      animateToValue();
429 430 431
    }
  }

432 433 434 435
  @override
  void dispose() {
    _painter.dispose();
    super.dispose();
436 437
  }

438 439 440 441 442
  @override
  ValueChanged<bool?>? get onChanged => widget.onChanged;

  @override
  bool get tristate => widget.tristate;
443

444 445
  @override
  bool? get value => widget.value;
446 447 448 449 450 451 452 453 454 455 456 457 458

  MaterialStateProperty<Color?> get _widgetFillColor {
    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        return null;
      }
      if (states.contains(MaterialState.selected)) {
        return widget.activeColor;
      }
      return null;
    });
  }

459
  BorderSide? _resolveSide(BorderSide? side, Set<MaterialState> states) {
460
    if (side is MaterialStateBorderSide) {
461
      return MaterialStateProperty.resolveAs<BorderSide?>(side, states);
462 463
    }
    if (!states.contains(MaterialState.selected)) {
464
      return side;
465
    }
466 467 468
    return null;
  }

469
  @override
470
  Widget build(BuildContext context) {
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
    switch (widget._checkboxType) {
      case _CheckboxType.material:
        break;

      case _CheckboxType.adaptive:
        final ThemeData theme = Theme.of(context);
        switch (theme.platform) {
          case TargetPlatform.android:
          case TargetPlatform.fuchsia:
          case TargetPlatform.linux:
          case TargetPlatform.windows:
            break;
          case TargetPlatform.iOS:
          case TargetPlatform.macOS:
            return CupertinoCheckbox(
              value: value,
              tristate: tristate,
              onChanged: onChanged,
              activeColor: widget.activeColor,
              checkColor: widget.checkColor,
              focusColor: widget.focusColor,
              focusNode: widget.focusNode,
              autofocus: widget.autofocus,
              side: widget.side,
              shape: widget.shape,
            );
        }
    }

500
    assert(debugCheckHasMaterial(context));
501
    final CheckboxThemeData checkboxTheme = CheckboxTheme.of(context);
502 503 504
    final CheckboxThemeData defaults = Theme.of(context).useMaterial3
      ? _CheckboxDefaultsM3(context)
      : _CheckboxDefaultsM2(context);
505
    final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize
506
      ?? checkboxTheme.materialTapTargetSize
507
      ?? defaults.materialTapTargetSize!;
508
    final VisualDensity effectiveVisualDensity = widget.visualDensity
509
      ?? checkboxTheme.visualDensity
510
      ?? defaults.visualDensity!;
511
    Size size;
512
    switch (effectiveMaterialTapTargetSize) {
513
      case MaterialTapTargetSize.padded:
514
        size = const Size(kMinInteractiveDimension, kMinInteractiveDimension);
515
      case MaterialTapTargetSize.shrinkWrap:
516
        size = const Size(kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0);
517
    }
518
    size += effectiveVisualDensity.baseSizeAdjustment;
519 520 521

    final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
      return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
522
        ?? checkboxTheme.mouseCursor?.resolve(states)
523 524 525
        ?? MaterialStateMouseCursor.clickable.resolve(states);
    });

526
    // Colors need to be resolved in selected and non selected states separately
527 528 529 530 531 532
    final Set<MaterialState> activeStates = states..add(MaterialState.selected);
    final Set<MaterialState> inactiveStates = states..remove(MaterialState.selected);
    if (widget.isError) {
      activeStates.add(MaterialState.error);
      inactiveStates.add(MaterialState.error);
    }
533
    final Color? activeColor = widget.fillColor?.resolve(activeStates)
534
      ?? _widgetFillColor.resolve(activeStates)
535 536 537 538
      ?? checkboxTheme.fillColor?.resolve(activeStates);
    final Color effectiveActiveColor = activeColor
      ?? defaults.fillColor!.resolve(activeStates)!;
    final Color? inactiveColor = widget.fillColor?.resolve(inactiveStates)
539
      ?? _widgetFillColor.resolve(inactiveStates)
540 541 542
      ?? checkboxTheme.fillColor?.resolve(inactiveStates);
    final Color effectiveInactiveColor = inactiveColor
      ?? defaults.fillColor!.resolve(inactiveStates)!;
543

544 545 546 547 548 549 550 551 552 553 554
    final BorderSide activeSide = _resolveSide(widget.side, activeStates)
      ?? _resolveSide(checkboxTheme.side, activeStates)
      ?? _resolveSide(defaults.side, activeStates)!;
    final BorderSide inactiveSide = _resolveSide(widget.side, inactiveStates)
      ?? _resolveSide(checkboxTheme.side, inactiveStates)
      ?? _resolveSide(defaults.side, inactiveStates)!;

    final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
    if (widget.isError) {
      focusedStates.add(MaterialState.error);
    }
555
    Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
556
      ?? widget.focusColor
557
      ?? checkboxTheme.overlayColor?.resolve(focusedStates)
558
      ?? defaults.overlayColor!.resolve(focusedStates)!;
559

560 561 562 563
    final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
    if (widget.isError) {
      hoveredStates.add(MaterialState.error);
    }
564
    Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
565 566 567
      ?? widget.hoverColor
      ?? checkboxTheme.overlayColor?.resolve(hoveredStates)
      ?? defaults.overlayColor!.resolve(hoveredStates)!;
568

569 570
    final Set<MaterialState> activePressedStates = activeStates..add(MaterialState.pressed);
    final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates)
571 572 573
      ?? checkboxTheme.overlayColor?.resolve(activePressedStates)
      ?? activeColor?.withAlpha(kRadialReactionAlpha)
      ?? defaults.overlayColor!.resolve(activePressedStates)!;
574 575 576

    final Set<MaterialState> inactivePressedStates = inactiveStates..add(MaterialState.pressed);
    final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates)
577 578 579
      ?? checkboxTheme.overlayColor?.resolve(inactivePressedStates)
      ?? inactiveColor?.withAlpha(kRadialReactionAlpha)
      ?? defaults.overlayColor!.resolve(inactivePressedStates)!;
580

581 582 583 584 585 586 587 588 589 590
    if (downPosition != null) {
      effectiveHoverOverlayColor = states.contains(MaterialState.selected)
        ? effectiveActivePressedOverlayColor
        : effectiveInactivePressedOverlayColor;
      effectiveFocusOverlayColor = states.contains(MaterialState.selected)
        ? effectiveActivePressedOverlayColor
        : effectiveInactivePressedOverlayColor;
    }

    final Set<MaterialState> checkStates = widget.isError ? (states..add(MaterialState.error)) : states;
591
    final Color effectiveCheckColor = widget.checkColor
592 593
      ?? checkboxTheme.checkColor?.resolve(checkStates)
      ?? defaults.checkColor!.resolve(checkStates)!;
594 595 596 597

    final double effectiveSplashRadius = widget.splashRadius
      ?? checkboxTheme.splashRadius
      ?? defaults.splashRadius!;
598

599
    return Semantics(
600
      label: widget.semanticLabel,
601
      checked: widget.value ?? false,
602
      mixed: widget.tristate ? widget.value == null : null,
603 604 605 606 607 608 609 610 611 612 613 614 615 616
      child: buildToggleable(
        mouseCursor: effectiveMouseCursor,
        focusNode: widget.focusNode,
        autofocus: widget.autofocus,
        size: size,
        painter: _painter
          ..position = position
          ..reaction = reaction
          ..reactionFocusFade = reactionFocusFade
          ..reactionHoverFade = reactionHoverFade
          ..inactiveReactionColor = effectiveInactivePressedOverlayColor
          ..reactionColor = effectiveActivePressedOverlayColor
          ..hoverColor = effectiveHoverOverlayColor
          ..focusColor = effectiveFocusOverlayColor
617
          ..splashRadius = effectiveSplashRadius
618 619 620 621 622 623 624 625
          ..downPosition = downPosition
          ..isFocused = states.contains(MaterialState.focused)
          ..isHovered = states.contains(MaterialState.hovered)
          ..activeColor = effectiveActiveColor
          ..inactiveColor = effectiveInactiveColor
          ..checkColor = effectiveCheckColor
          ..value = value
          ..previousValue = _previousValue
626
          ..shape = widget.shape ?? checkboxTheme.shape ?? defaults.shape!
627 628
          ..activeSide = activeSide
          ..inactiveSide = inactiveSide,
629
      ),
630
    );
631 632 633
  }
}

634 635
const double _kEdgeSize = Checkbox.width;
const double _kStrokeWidth = 2.0;
636

637 638 639 640 641 642 643 644 645
class _CheckboxPainter extends ToggleablePainter {
  Color get checkColor => _checkColor!;
  Color? _checkColor;
  set checkColor(Color value) {
    if (_checkColor == value) {
      return;
    }
    _checkColor = value;
    notifyListeners();
646
  }
647

648 649 650 651 652 653 654 655 656
  bool? get value => _value;
  bool? _value;
  set value(bool? value) {
    if (_value == value) {
      return;
    }
    _value = value;
    notifyListeners();
  }
657

658 659 660 661 662 663 664 665 666
  bool? get previousValue => _previousValue;
  bool? _previousValue;
  set previousValue(bool? value) {
    if (_previousValue == value) {
      return;
    }
    _previousValue = value;
    notifyListeners();
  }
667

668 669 670 671
  OutlinedBorder get shape => _shape!;
  OutlinedBorder? _shape;
  set shape(OutlinedBorder value) {
    if (_shape == value) {
672
      return;
673 674 675
    }
    _shape = value;
    notifyListeners();
676
  }
677

678 679 680 681 682 683 684 685 686 687 688 689 690 691
  BorderSide get activeSide => _activeSide!;
  BorderSide? _activeSide;
  set activeSide(BorderSide value) {
    if (_activeSide == value) {
      return;
    }
    _activeSide = value;
    notifyListeners();
  }

  BorderSide get inactiveSide => _inactiveSide!;
  BorderSide? _inactiveSide;
  set inactiveSide(BorderSide value) {
    if (_inactiveSide == value) {
692 693
      return;
    }
694
    _inactiveSide = value;
695
    notifyListeners();
696 697
  }

698 699 700 701
  // 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
702
  Rect _outerRectAt(Offset origin, double t) {
703 704
    final double inset = 1.0 - (t - 0.5).abs() * 2.0;
    final double size = _kEdgeSize - inset * _kStrokeWidth;
705
    final Rect rect = Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size);
706
    return rect;
707
  }
708

709
  // The checkbox's fill color
710 711
  Color _colorAt(double t) {
    // As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor.
712
    return t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0)!;
713 714 715
  }

  // White stroke used to paint the check and dash.
716 717
  Paint _createStrokePaint() {
    return Paint()
718
      ..color = checkColor
719 720 721 722
      ..style = PaintingStyle.stroke
      ..strokeWidth = _kStrokeWidth;
  }

723 724
  void _drawBox(Canvas canvas, Rect outer, Paint paint, BorderSide? side) {
    canvas.drawPath(shape.getOuterPath(outer), paint);
725 726
    if (side != null) {
      shape.copyWith(side: side).paint(canvas, outer);
727
    }
728 729 730 731
  }

  void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
    assert(t >= 0.0 && t <= 1.0);
732 733
    // As t goes from 0.0 to 1.0, animate the two check mark strokes from the
    // short side to the long side.
734
    final Path path = Path();
735 736 737
    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);
738 739
    if (t < 0.5) {
      final double strokeT = t * 2.0;
740
      final Offset drawMid = Offset.lerp(start, mid, strokeT)!;
741 742 743 744
      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;
745
      final Offset drawEnd = Offset.lerp(mid, end, strokeT)!;
746 747 748 749
      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);
    }
750 751
    canvas.drawPath(path, paint);
  }
752

753 754 755 756
  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.
757 758 759
    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);
760 761
    final Offset drawStart = Offset.lerp(start, mid, 1.0 - t)!;
    final Offset drawEnd = Offset.lerp(mid, end, t)!;
762 763 764 765
    canvas.drawLine(origin + drawStart, origin + drawEnd, paint);
  }

  @override
766 767
  void paint(Canvas canvas, Size size) {
    paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero));
768

769
    final Paint strokePaint = _createStrokePaint();
770
    final Offset origin = size / 2.0 - const Size.square(_kEdgeSize) / 2.0 as Offset;
771 772 773 774
    final AnimationStatus status = position.status;
    final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed
      ? position.value
      : 1.0 - position.value;
775

776
    // Four cases: false to null, false to true, null to false, true to false
777
    if (previousValue == false || value == false) {
778
      final double t = value == false ? 1.0 - tNormalized : tNormalized;
779
      final Rect outer = _outerRectAt(origin, t);
780
      final Paint paint = Paint()..color = _colorAt(t);
781

782
      if (t <= 0.5) {
783 784
        final BorderSide border = BorderSide.lerp(inactiveSide, activeSide, t);
        _drawBox(canvas, outer, paint, border);
785
      } else {
786
        _drawBox(canvas, outer, paint, activeSide);
787
        final double tShrink = (t - 0.5) * 2.0;
788
        if (previousValue == null || value == null) {
789
          _drawDash(canvas, origin, tShrink, strokePaint);
790
        } else {
791
          _drawCheck(canvas, origin, tShrink, strokePaint);
792
        }
793 794
      }
    } else { // Two cases: null to true, true to null
795
      final Rect outer = _outerRectAt(origin, 1.0);
796
      final Paint paint = Paint() ..color = _colorAt(1.0);
797

798
      _drawBox(canvas, outer, paint, activeSide);
799 800
      if (tNormalized <= 0.5) {
        final double tShrink = 1.0 - tNormalized * 2.0;
801
        if (previousValue ?? false) {
802
          _drawCheck(canvas, origin, tShrink, strokePaint);
803
        } else {
804
          _drawDash(canvas, origin, tShrink, strokePaint);
805
        }
806 807
      } else {
        final double tExpand = (tNormalized - 0.5) * 2.0;
808
        if (value ?? false) {
809
          _drawCheck(canvas, origin, tExpand, strokePaint);
810
        } else {
811
          _drawDash(canvas, origin, tExpand, strokePaint);
812
        }
813
      }
814 815 816
    }
  }
}
817 818 819 820 821 822 823 824 825 826

// Hand coded defaults based on Material Design 2.
class _CheckboxDefaultsM2 extends CheckboxThemeData {
  _CheckboxDefaultsM2(BuildContext context)
    : _theme = Theme.of(context),
      _colors = Theme.of(context).colorScheme;

  final ThemeData _theme;
  final ColorScheme _colors;

827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842
  @override
  MaterialStateBorderSide? get side {
    return MaterialStateBorderSide.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        if (states.contains(MaterialState.selected)) {
          return const BorderSide(width: 2.0, color: Colors.transparent);
        }
        return BorderSide(width: 2.0, color: _theme.disabledColor);
      }
      if (states.contains(MaterialState.selected)) {
        return const BorderSide(width: 2.0, color: Colors.transparent);
      }
      return BorderSide(width: 2.0, color: _theme.unselectedWidgetColor);
    });
  }

843 844 845 846
  @override
  MaterialStateProperty<Color> get fillColor {
    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
847 848 849 850
        if (states.contains(MaterialState.selected)) {
          return _theme.disabledColor;
        }
        return Colors.transparent;
851 852 853 854
      }
      if (states.contains(MaterialState.selected)) {
        return _colors.secondary;
      }
855
      return Colors.transparent;
856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887
    });
  }

  @override
  MaterialStateProperty<Color> get checkColor {
    return MaterialStateProperty.all<Color>(const Color(0xFFFFFFFF));
  }

  @override
  MaterialStateProperty<Color?> get overlayColor {
    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.pressed)) {
        return fillColor.resolve(states).withAlpha(kRadialReactionAlpha);
      }
      if (states.contains(MaterialState.hovered)) {
        return _theme.hoverColor;
      }
      if (states.contains(MaterialState.focused)) {
        return _theme.focusColor;
      }
      return Colors.transparent;
    });
  }

  @override
  double get splashRadius => kRadialReactionRadius;

  @override
  MaterialTapTargetSize get materialTapTargetSize => _theme.materialTapTargetSize;

  @override
  VisualDensity get visualDensity => _theme.visualDensity;
888 889 890 891 892

  @override
  OutlinedBorder get shape => const RoundedRectangleBorder(
    borderRadius: BorderRadius.all(Radius.circular(1.0)),
  );
893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910
}

// BEGIN GENERATED TOKEN PROPERTIES - Checkbox

// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
//   dev/tools/gen_defaults/bin/gen_defaults.dart.

class _CheckboxDefaultsM3 extends CheckboxThemeData {
  _CheckboxDefaultsM3(BuildContext context)
    : _theme = Theme.of(context),
      _colors = Theme.of(context).colorScheme;

  final ThemeData _theme;
  final ColorScheme _colors;

  @override
911 912
  MaterialStateBorderSide? get side {
    return MaterialStateBorderSide.resolveWith((Set<MaterialState> states) {
913
      if (states.contains(MaterialState.disabled)) {
914 915 916 917
        if (states.contains(MaterialState.selected)) {
          return const BorderSide(width: 2.0, color: Colors.transparent);
        }
        return BorderSide(width: 2.0, color: _colors.onSurface.withOpacity(0.38));
918
      }
919
      if (states.contains(MaterialState.selected)) {
920 921 922 923
        return const BorderSide(width: 0.0, color: Colors.transparent);
      }
      if (states.contains(MaterialState.error)) {
        return BorderSide(width: 2.0, color: _colors.error);
924
      }
925
      if (states.contains(MaterialState.pressed)) {
926
        return BorderSide(width: 2.0, color: _colors.onSurface);
927 928
      }
      if (states.contains(MaterialState.hovered)) {
929
        return BorderSide(width: 2.0, color: _colors.onSurface);
930 931
      }
      if (states.contains(MaterialState.focused)) {
932
        return BorderSide(width: 2.0, color: _colors.onSurface);
933
      }
934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953
      return BorderSide(width: 2.0, color: _colors.onSurfaceVariant);
    });
  }

  @override
  MaterialStateProperty<Color> get fillColor {
    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        if (states.contains(MaterialState.selected)) {
          return _colors.onSurface.withOpacity(0.38);
        }
        return Colors.transparent;
      }
      if (states.contains(MaterialState.selected)) {
        if (states.contains(MaterialState.error)) {
          return _colors.error;
        }
        return _colors.primary;
      }
      return Colors.transparent;
954 955 956 957 958 959 960 961 962 963 964 965 966
    });
  }

  @override
  MaterialStateProperty<Color> get checkColor {
    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        if (states.contains(MaterialState.selected)) {
          return _colors.surface;
        }
        return Colors.transparent; // No icons available when the checkbox is unselected.
      }
      if (states.contains(MaterialState.selected)) {
967 968
        if (states.contains(MaterialState.error)) {
          return _colors.onError;
969 970 971 972 973 974 975 976 977 978
        }
        return _colors.onPrimary;
      }
      return Colors.transparent; // No icons available when the checkbox is unselected.
    });
  }

  @override
  MaterialStateProperty<Color> get overlayColor {
    return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
979 980 981 982 983 984 985 986 987 988 989
      if (states.contains(MaterialState.error)) {
        if (states.contains(MaterialState.pressed)) {
          return _colors.error.withOpacity(0.12);
        }
        if (states.contains(MaterialState.hovered)) {
          return _colors.error.withOpacity(0.08);
        }
        if (states.contains(MaterialState.focused)) {
          return _colors.error.withOpacity(0.12);
        }
      }
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
      if (states.contains(MaterialState.selected)) {
        if (states.contains(MaterialState.pressed)) {
          return _colors.onSurface.withOpacity(0.12);
        }
        if (states.contains(MaterialState.hovered)) {
          return _colors.primary.withOpacity(0.08);
        }
        if (states.contains(MaterialState.focused)) {
          return _colors.primary.withOpacity(0.12);
        }
        return Colors.transparent;
      }
      if (states.contains(MaterialState.pressed)) {
        return _colors.primary.withOpacity(0.12);
      }
      if (states.contains(MaterialState.hovered)) {
        return _colors.onSurface.withOpacity(0.08);
      }
      if (states.contains(MaterialState.focused)) {
        return _colors.onSurface.withOpacity(0.12);
      }
      return Colors.transparent;
    });
  }

  @override
  double get splashRadius => 40.0 / 2;

  @override
  MaterialTapTargetSize get materialTapTargetSize => _theme.materialTapTargetSize;

  @override
  VisualDensity get visualDensity => _theme.visualDensity;
1023 1024

  @override
1025 1026
  OutlinedBorder get shape => const RoundedRectangleBorder(
    borderRadius: BorderRadius.all(Radius.circular(2.0)),
1027
  );
1028 1029 1030
}

// END GENERATED TOKEN PROPERTIES - Checkbox