88/// @docImport 'switch.dart';
99library ;
1010
11+ import 'package:flutter/foundation.dart' ;
1112import 'package:flutter/widgets.dart' ;
1213
1314import 'colors.dart' ;
1415import 'constants.dart' ;
16+ import 'theme.dart' ;
1517
1618// Examples can assume:
1719// bool _throwShotAway = false;
@@ -23,6 +25,33 @@ const double _kCupertinoFocusColorOpacity = 0.80;
2325const double _kCupertinoFocusColorBrightness = 0.69 ;
2426const double _kCupertinoFocusColorSaturation = 0.835 ;
2527
28+ // Eyeballed from a checkbox on a physical Macbook Pro running macOS version 14.5.
29+ const Color _kDisabledCheckColor = CupertinoDynamicColor .withBrightness (
30+ color: Color .fromARGB (64 , 0 , 0 , 0 ),
31+ darkColor: Color .fromARGB (64 , 255 , 255 , 255 ),
32+ );
33+ const Color _kDisabledBorderColor = CupertinoDynamicColor .withBrightness (
34+ color: Color .fromARGB (13 , 0 , 0 , 0 ),
35+ darkColor: Color .fromARGB (13 , 0 , 0 , 0 ),
36+ );
37+ const CupertinoDynamicColor _kDefaultBorderColor = CupertinoDynamicColor .withBrightness (
38+ color: Color .fromARGB (255 , 209 , 209 , 214 ),
39+ darkColor: Color .fromARGB (50 , 128 , 128 , 128 ),
40+ );
41+ const CupertinoDynamicColor _kDefaultFillColor = CupertinoDynamicColor .withBrightness (
42+ color: CupertinoColors .activeBlue,
43+ darkColor: Color .fromARGB (255 , 50 , 100 , 215 ),
44+ );
45+ const Color _kDefaultCheckColor = CupertinoDynamicColor .withBrightness (
46+ color: CupertinoColors .white,
47+ darkColor: Color .fromARGB (255 , 222 , 232 , 248 ),
48+ );
49+ const double _kPressedOverlayOpacity = 0.15 ;
50+ // In dark mode, the fill color of a checkbox is an opacity gradient of the
51+ // background color.
52+ const List <double > _kDarkGradientOpacities = < double > [0.14 , 0.29 ];
53+ const List <double > _kDisabledDarkGradientOpacities = < double > [0.08 , 0.14 ];
54+
2655/// A macOS style checkbox.
2756///
2857/// The checkbox itself does not maintain any state. Instead, when the state of
@@ -133,7 +162,8 @@ class CupertinoCheckbox extends StatefulWidget {
133162
134163 /// The color to use for the check icon when this checkbox is checked.
135164 ///
136- /// If null, then the value of [CupertinoColors.white] is used.
165+ /// If null, then the value of [CupertinoColors.white] is used if the checkbox
166+ /// is enabled. If the checkbox is disabled, a grey-black color is used.
137167 final Color ? checkColor;
138168
139169 /// If true, the checkbox's [value] can be true, false, or null.
@@ -162,8 +192,25 @@ class CupertinoCheckbox extends StatefulWidget {
162192
163193 /// The color and width of the checkbox's border.
164194 ///
165- /// If this property is null, then the side defaults to a one pixel wide
166- /// black, solid border.
195+ /// This property can be a [WidgetStateBorderSide] that can
196+ /// specify different border color and widths depending on the
197+ /// checkbox's state.
198+ ///
199+ /// Resolves in the following states:
200+ /// * [WidgetState.pressed] .
201+ /// * [WidgetState.selected] .
202+ /// * [WidgetState.hovered] .
203+ /// * [WidgetState.focused] .
204+ /// * [WidgetState.disabled] .
205+ /// * [WidgetState.error] .
206+ ///
207+ /// If this property is not a [WidgetStateBorderSide] and it is
208+ /// non-null, then it is only rendered when the checkbox's value is
209+ /// false. The difference in interpretation is for backwards
210+ /// compatibility.
211+ ///
212+ /// If this property is null and the checkbox's value is false, then the side
213+ /// defaults to a one pixel wide grey-black border.
167214 final BorderSide ? side;
168215
169216 /// The shape of the checkbox.
@@ -180,7 +227,7 @@ class CupertinoCheckbox extends StatefulWidget {
180227 final String ? semanticLabel;
181228
182229 /// The width of a checkbox widget.
183- static const double width = 18 .0 ;
230+ static const double width = 14 .0 ;
184231
185232 @override
186233 State <CupertinoCheckbox > createState () => _CupertinoCheckboxState ();
@@ -221,19 +268,67 @@ class _CupertinoCheckboxState extends State<CupertinoCheckbox> with TickerProvid
221268 @override
222269 bool ? get value => widget.value;
223270
224- void onFocusChange (bool value) {
225- if (focused != value) {
226- focused = value;
271+ WidgetStateProperty <Color > get _defaultFillColor {
272+ return WidgetStateProperty .resolveWith ((Set <WidgetState > states) {
273+ if (states.contains (WidgetState .disabled)) {
274+ return CupertinoColors .white.withOpacity (0.5 );
275+ }
276+ if (states.contains (WidgetState .selected)) {
277+ return widget.activeColor ?? CupertinoDynamicColor .resolve (_kDefaultFillColor, context);
278+ }
279+ return CupertinoColors .white;
280+ });
281+ }
282+
283+ WidgetStateProperty <Color > get _defaultCheckColor {
284+ return WidgetStateProperty .resolveWith ((Set <WidgetState > states) {
285+ if (states.contains (WidgetState .disabled) && states.contains (WidgetState .selected)) {
286+ return widget.checkColor ?? CupertinoDynamicColor .resolve (_kDisabledCheckColor, context);
287+ }
288+ if (states.contains (WidgetState .selected)) {
289+ return widget.checkColor ?? CupertinoDynamicColor .resolve (_kDefaultCheckColor, context);
290+ }
291+ return CupertinoColors .white;
292+ });
293+ }
294+
295+ WidgetStateProperty <BorderSide > get _defaultSide {
296+ return WidgetStateProperty .resolveWith ((Set <WidgetState > states) {
297+ if ((states.contains (WidgetState .selected) || states.contains (WidgetState .focused))
298+ && ! states.contains (WidgetState .disabled)) {
299+ return const BorderSide (width: 0.0 , color: CupertinoColors .transparent);
300+ }
301+ if (states.contains (WidgetState .disabled)) {
302+ return BorderSide (color: CupertinoDynamicColor .resolve (_kDisabledBorderColor, context));
303+ }
304+ return BorderSide (color: CupertinoDynamicColor .resolve (_kDefaultBorderColor, context));
305+ });
306+ }
307+
308+ BorderSide ? _resolveSide (BorderSide ? side, Set <WidgetState > states) {
309+ if (side is WidgetStateBorderSide ) {
310+ return WidgetStateProperty .resolveAs <BorderSide ?>(side, states);
227311 }
312+ if (! states.contains (WidgetState .selected)) {
313+ return side;
314+ }
315+ return null ;
228316 }
229317
230318 @override
231319 Widget build (BuildContext context) {
232- final Color effectiveActiveColor = widget.activeColor
233- ?? CupertinoColors .activeBlue;
234- final Color ? inactiveColor = widget.inactiveColor;
235- final Color effectiveInactiveColor = inactiveColor
236- ?? CupertinoColors .inactiveGray;
320+ // Colors need to be resolved in selected and non selected states separately.
321+ final Set <WidgetState > activeStates = states..add (WidgetState .selected);
322+ final Set <WidgetState > inactiveStates = states..remove (WidgetState .selected);
323+
324+ // Since the states getter always makes a new set, make a copy to use
325+ // throughout the lifecycle of this build method.
326+ final Set <WidgetState > currentStates = states;
327+
328+ final Color effectiveActiveColor = _defaultFillColor.resolve (activeStates);
329+
330+ final BorderSide effectiveBorderSide = _resolveSide (widget.side, currentStates)
331+ ?? _defaultSide.resolve (currentStates);
237332
238333 final Color effectiveFocusOverlayColor = widget.focusColor
239334 ?? HSLColor
@@ -242,32 +337,32 @@ class _CupertinoCheckboxState extends State<CupertinoCheckbox> with TickerProvid
242337 .withSaturation (_kCupertinoFocusColorSaturation)
243338 .toColor ();
244339
245- final Color effectiveCheckColor = widget.checkColor
246- ?? CupertinoColors .white;
247-
248340 return Semantics (
249341 label: widget.semanticLabel,
250342 checked: widget.value ?? false ,
251343 mixed: widget.tristate ? widget.value == null : null ,
252344 child: buildToggleable (
253345 focusNode: widget.focusNode,
254346 autofocus: widget.autofocus,
255- onFocusChange: onFocusChange,
256347 size: const Size .square (kMinInteractiveDimensionCupertino),
257348 painter: _painter
349+ ..position = position
350+ ..reaction = reaction
258351 ..focusColor = effectiveFocusOverlayColor
259- ..isFocused = focused
260352 ..downPosition = downPosition
353+ ..isFocused = currentStates.contains (WidgetState .focused)
354+ ..isHovered = currentStates.contains (WidgetState .hovered)
261355 ..activeColor = effectiveActiveColor
262- ..inactiveColor = effectiveInactiveColor
263- ..checkColor = effectiveCheckColor
356+ ..inactiveColor = _defaultFillColor. resolve (inactiveStates)
357+ ..checkColor = _defaultCheckColor. resolve (currentStates)
264358 ..value = value
265359 ..previousValue = _previousValue
266360 ..isActive = widget.onChanged != null
267361 ..shape = widget.shape ?? RoundedRectangleBorder (
268362 borderRadius: BorderRadius .circular (4.0 ),
269363 )
270- ..side = widget.side,
364+ ..side = effectiveBorderSide
365+ ..brightness = CupertinoTheme .of (context).brightness
271366 ),
272367 );
273368 }
@@ -314,16 +409,26 @@ class _CheckboxPainter extends ToggleablePainter {
314409 notifyListeners ();
315410 }
316411
317- BorderSide ? get side => _side;
412+ BorderSide get side => _side! ;
318413 BorderSide ? _side;
319- set side (BorderSide ? value) {
414+ set side (BorderSide value) {
320415 if (_side == value) {
321416 return ;
322417 }
323418 _side = value;
324419 notifyListeners ();
325420 }
326421
422+ Brightness ? get brightness => _brightness;
423+ Brightness ? _brightness;
424+ set brightness (Brightness ? value) {
425+ if (_brightness == value) {
426+ return ;
427+ }
428+ _brightness = value;
429+ notifyListeners ();
430+ }
431+
327432 Rect _outerRectAt (Offset origin) {
328433 const double size = CupertinoCheckbox .width;
329434 final Rect rect = Rect .fromLTWH (origin.dx, origin.dy, size, size);
@@ -341,12 +446,36 @@ class _CheckboxPainter extends ToggleablePainter {
341446 return Paint ()
342447 ..color = checkColor
343448 ..style = PaintingStyle .stroke
344- ..strokeWidth = 2.5
449+ ..strokeWidth = 2.0
345450 ..strokeCap = StrokeCap .round;
346451 }
347452
348- void _drawBox (Canvas canvas, Rect outer, Paint paint, BorderSide ? side, bool fill) {
349- if (fill) {
453+ // Draw a gradient from the top to the bottom of the checkbox.
454+ void _drawFillGradient (Canvas canvas, Rect outer, Color topColor, Color bottomColor) {
455+ final LinearGradient fillGradient = LinearGradient (
456+ begin: Alignment .topCenter,
457+ end: Alignment .bottomCenter,
458+ // Eyeballed from a checkbox on a physical Macbook Pro running macOS version 14.5.
459+ colors: < Color > [
460+ topColor,
461+ bottomColor,
462+ ],
463+ );
464+ final Paint gradientPaint = Paint ()
465+ ..shader = fillGradient.createShader (outer);
466+ canvas.drawPath (shape.getOuterPath (outer), gradientPaint);
467+ }
468+
469+ void _drawBox (Canvas canvas, Rect outer, Paint paint, BorderSide ? side, bool value) {
470+ // Draw a gradient in dark mode except when the checkbox is enabled and checked.
471+ if (brightness == Brightness .dark && ! (isActive && value)) {
472+ _drawFillGradient (
473+ canvas,
474+ outer,
475+ paint.color.withOpacity (isActive ? _kDarkGradientOpacities[0 ] : _kDisabledDarkGradientOpacities[0 ]),
476+ paint.color.withOpacity (isActive ? _kDarkGradientOpacities[1 ] : _kDisabledDarkGradientOpacities[1 ]),
477+ );
478+ } else {
350479 canvas.drawPath (shape.getOuterPath (outer), paint);
351480 }
352481 if (side != null ) {
@@ -359,12 +488,11 @@ class _CheckboxPainter extends ToggleablePainter {
359488 // The ratios for the offsets below were found from looking at the checkbox
360489 // examples on in the HIG docs. The distance from the needed point to the
361490 // edge was measured, then divided by the total width.
362- const Offset start = Offset (CupertinoCheckbox .width * 0.25 , CupertinoCheckbox .width * 0.52 );
363- const Offset mid = Offset (CupertinoCheckbox .width * 0.46 , CupertinoCheckbox .width * 0.75 );
364- const Offset end = Offset (CupertinoCheckbox .width * 0.72 , CupertinoCheckbox .width * 0.29 );
491+ const Offset start = Offset (CupertinoCheckbox .width * 0.22 , CupertinoCheckbox .width * 0.54 );
492+ const Offset mid = Offset (CupertinoCheckbox .width * 0.40 , CupertinoCheckbox .width * 0.75 );
493+ const Offset end = Offset (CupertinoCheckbox .width * 0.78 , CupertinoCheckbox .width * 0.25 );
365494 path.moveTo (origin.dx + start.dx, origin.dy + start.dy);
366495 path.lineTo (origin.dx + mid.dx, origin.dy + mid.dy);
367- canvas.drawPath (path, paint);
368496 path.moveTo (origin.dx + mid.dx, origin.dy + mid.dy);
369497 path.lineTo (origin.dx + end.dx, origin.dy + end.dy);
370498 canvas.drawPath (path, paint);
@@ -382,33 +510,34 @@ class _CheckboxPainter extends ToggleablePainter {
382510 void paint (Canvas canvas, Size size) {
383511 final Paint strokePaint = _createStrokePaint ();
384512 final Offset origin = size / 2.0 - const Size .square (CupertinoCheckbox .width) / 2.0 as Offset ;
385-
386513 final Rect outer = _outerRectAt (origin);
387514 final Paint paint = Paint ()..color = _colorAt (value ?? true );
388515
389- if (value == false ) {
390-
391- final BorderSide border = side ?? BorderSide (color: paint.color);
392- _drawBox (canvas, outer, paint, border, false );
393- } else {
394-
395- _drawBox (canvas, outer, paint, side, true );
396- if (value ?? false ) {
516+ switch (value) {
517+ case false :
518+ _drawBox (canvas, outer, paint, side, value ?? true );
519+ case true :
520+ _drawBox (canvas, outer, paint, side, value ?? true );
397521 _drawCheck (canvas, origin, strokePaint);
398- } else {
522+ case null :
523+ _drawBox (canvas, outer, paint, side, value ?? true );
399524 _drawDash (canvas, origin, strokePaint);
400- }
401525 }
402-
526+ // The checkbox's opacity changes when pressed.
527+ if (downPosition != null ) {
528+ final Paint pressedPaint = Paint ()
529+ ..color = brightness == Brightness .light
530+ ? CupertinoColors .black.withOpacity (_kPressedOverlayOpacity)
531+ : CupertinoColors .white.withOpacity (_kPressedOverlayOpacity);
532+ canvas.drawPath (shape.getOuterPath (outer), pressedPaint);
533+ }
403534 if (isFocused) {
404535 final Rect focusOuter = outer.inflate (1 );
405-
406536 final Paint borderPaint = Paint ()
407537 ..color = focusColor
408538 ..style = PaintingStyle .stroke
409539 ..strokeWidth = 3.5 ;
410-
411- _drawBox (canvas, focusOuter, borderPaint, side, true );
540+ _drawBox (canvas, focusOuter, borderPaint, side, value ?? true );
412541 }
413542 }
414543}
0 commit comments