parent
8de2433a4c
commit
1ba3f99bb0
@ -27,6 +27,7 @@ export 'src/cupertino/adaptive_text_selection_toolbar.dart';
|
||||
export 'src/cupertino/app.dart';
|
||||
export 'src/cupertino/bottom_tab_bar.dart';
|
||||
export 'src/cupertino/button.dart';
|
||||
export 'src/cupertino/checkbox.dart';
|
||||
export 'src/cupertino/colors.dart';
|
||||
export 'src/cupertino/constants.dart';
|
||||
export 'src/cupertino/context_menu.dart';
|
||||
|
396
packages/flutter/lib/src/cupertino/checkbox.dart
Normal file
396
packages/flutter/lib/src/cupertino/checkbox.dart
Normal file
@ -0,0 +1,396 @@
|
||||
// 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 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'constants.dart';
|
||||
import 'toggleable.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// bool _throwShotAway = false;
|
||||
// late StateSetter setState;
|
||||
|
||||
// The relative values needed to transform a color to it's equivilant focus
|
||||
// outline color.
|
||||
const double _kCupertinoFocusColorOpacity = 0.80;
|
||||
const double _kCupertinoFocusColorBrightness = 0.69;
|
||||
const double _kCupertinoFocusColorSaturation = 0.835;
|
||||
|
||||
/// A macOS style 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.
|
||||
///
|
||||
/// In the Apple Human Interface Guidelines (HIG), checkboxes are encouraged for
|
||||
/// use on macOS, but is silent about their use on iOS. If a multi-selection
|
||||
/// component is needed on iOS, the HIG encourages the developer to use switches
|
||||
/// ([CupertinoSwitch] in Flutter) instead, or to find a creative custom
|
||||
/// solution.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Checkbox], the Material Design equivalent.
|
||||
/// * [CupertinoSwitch], a widget with semantics similar to [CupertinoCheckbox].
|
||||
/// * [CupertinoSlider], for selecting a value in a range.
|
||||
/// * <https://developer.apple.com/design/human-interface-guidelines/components/selection-and-input/toggles/>
|
||||
class CupertinoCheckbox extends StatefulWidget {
|
||||
/// Creates a macOS-styled 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 CupertinoCheckbox({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.tristate = false,
|
||||
required this.onChanged,
|
||||
this.activeColor,
|
||||
this.inactiveColor,
|
||||
this.checkColor,
|
||||
this.focusColor,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.side,
|
||||
this.shape,
|
||||
}) : assert(tristate || value != null);
|
||||
|
||||
/// Whether this checkbox is checked.
|
||||
///
|
||||
/// When [tristate] is true, a value of null corresponds to the mixed state.
|
||||
/// When [tristate] is false, this value must not be null. This is asserted in
|
||||
/// debug mode.
|
||||
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 and back to false
|
||||
/// again.
|
||||
///
|
||||
/// 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
|
||||
/// CupertinoCheckbox(
|
||||
/// value: _throwShotAway,
|
||||
/// onChanged: (bool? newValue) {
|
||||
/// setState(() {
|
||||
/// _throwShotAway = newValue!;
|
||||
/// });
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
|
||||
/// The color to use when this checkbox is checked.
|
||||
///
|
||||
/// Defaults to [CupertinoColors.activeBlue].
|
||||
final Color? activeColor;
|
||||
|
||||
/// The color used if the checkbox is inactive.
|
||||
///
|
||||
/// By default, [CupertinoColors.inactiveGray] is used.
|
||||
final Color? inactiveColor;
|
||||
|
||||
/// The color to use for the check icon when this checkbox is checked.
|
||||
///
|
||||
/// If null, then the value of [CupertinoColors.white] is used.
|
||||
final Color? checkColor;
|
||||
|
||||
/// If true, the checkbox's [value] can be true, false, or null.
|
||||
///
|
||||
/// [CupertinoCheckbox] 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, and
|
||||
/// [onChanged] will only toggle between true and false.
|
||||
final bool tristate;
|
||||
|
||||
/// The color for the checkbox's border shadow when it has the input focus.
|
||||
///
|
||||
/// If null, then a paler form of the [activeColor] will be used.
|
||||
final Color? focusColor;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// The color and width of the checkbox's border.
|
||||
///
|
||||
/// If this property is null, then the side defaults to a one pixel wide
|
||||
/// black, solid border.
|
||||
final BorderSide? side;
|
||||
|
||||
/// The shape of the checkbox.
|
||||
///
|
||||
/// If this property is null then the shape defaults to a
|
||||
/// [RoundedRectangleBorder] with a circular corner radius of 4.0.
|
||||
final OutlinedBorder? shape;
|
||||
|
||||
/// The width of a checkbox widget.
|
||||
static const double width = 18.0;
|
||||
|
||||
@override
|
||||
State<CupertinoCheckbox> createState() => _CupertinoCheckboxState();
|
||||
}
|
||||
|
||||
class _CupertinoCheckboxState extends State<CupertinoCheckbox> with TickerProviderStateMixin, ToggleableStateMixin {
|
||||
final _CheckboxPainter _painter = _CheckboxPainter();
|
||||
bool? _previousValue;
|
||||
|
||||
bool focused = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_previousValue = widget.value;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CupertinoCheckbox oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.value != widget.value) {
|
||||
_previousValue = oldWidget.value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_painter.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
ValueChanged<bool?>? get onChanged => widget.onChanged;
|
||||
|
||||
@override
|
||||
bool get tristate => widget.tristate;
|
||||
|
||||
@override
|
||||
bool? get value => widget.value;
|
||||
|
||||
void onFocusChange(bool value) {
|
||||
if (focused != value) {
|
||||
focused = value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color effectiveActiveColor = widget.activeColor
|
||||
?? CupertinoColors.activeBlue;
|
||||
final Color? inactiveColor = widget.inactiveColor;
|
||||
final Color effectiveInactiveColor = inactiveColor
|
||||
?? CupertinoColors.inactiveGray;
|
||||
|
||||
final Color effectiveFocusOverlayColor = widget.focusColor
|
||||
?? HSLColor
|
||||
.fromColor(effectiveActiveColor.withOpacity(_kCupertinoFocusColorOpacity))
|
||||
.withLightness(_kCupertinoFocusColorBrightness)
|
||||
.withSaturation(_kCupertinoFocusColorSaturation)
|
||||
.toColor();
|
||||
|
||||
final Color effectiveCheckColor = widget.checkColor
|
||||
?? CupertinoColors.white;
|
||||
|
||||
return Semantics(
|
||||
checked: widget.value ?? false,
|
||||
mixed: widget.tristate ? widget.value == null : null,
|
||||
child: buildToggleable(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
size: const Size.square(kMinInteractiveDimensionCupertino),
|
||||
painter: _painter
|
||||
..focusColor = effectiveFocusOverlayColor
|
||||
..isFocused = focused
|
||||
..downPosition = downPosition
|
||||
..activeColor = effectiveActiveColor
|
||||
..inactiveColor = effectiveInactiveColor
|
||||
..checkColor = effectiveCheckColor
|
||||
..value = value
|
||||
..previousValue = _previousValue
|
||||
..isActive = widget.onChanged != null
|
||||
..shape = widget.shape ?? RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
)
|
||||
..side = widget.side,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CheckboxPainter extends ToggleablePainter {
|
||||
Color get checkColor => _checkColor!;
|
||||
Color? _checkColor;
|
||||
set checkColor(Color value) {
|
||||
if (_checkColor == value) {
|
||||
return;
|
||||
}
|
||||
_checkColor = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool? get value => _value;
|
||||
bool? _value;
|
||||
set value(bool? value) {
|
||||
if (_value == value) {
|
||||
return;
|
||||
}
|
||||
_value = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool? get previousValue => _previousValue;
|
||||
bool? _previousValue;
|
||||
set previousValue(bool? value) {
|
||||
if (_previousValue == value) {
|
||||
return;
|
||||
}
|
||||
_previousValue = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
OutlinedBorder get shape => _shape!;
|
||||
OutlinedBorder? _shape;
|
||||
set shape(OutlinedBorder value) {
|
||||
if (_shape == value) {
|
||||
return;
|
||||
}
|
||||
_shape = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
BorderSide? get side => _side;
|
||||
BorderSide? _side;
|
||||
set side(BorderSide? value) {
|
||||
if (_side == value) {
|
||||
return;
|
||||
}
|
||||
_side = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Rect _outerRectAt(Offset origin) {
|
||||
const double size = CupertinoCheckbox.width;
|
||||
final Rect rect = Rect.fromLTWH(origin.dx, origin.dy, size, size);
|
||||
return rect;
|
||||
}
|
||||
|
||||
// The checkbox's border color if value == false, or its fill color when
|
||||
// value == true or null.
|
||||
Color _colorAt(bool value) {
|
||||
return value && isActive ? activeColor : inactiveColor;
|
||||
}
|
||||
|
||||
// White stroke used to paint the check and dash.
|
||||
Paint _createStrokePaint() {
|
||||
return Paint()
|
||||
..color = checkColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.5
|
||||
..strokeCap = StrokeCap.round;
|
||||
}
|
||||
|
||||
void _drawBox(Canvas canvas, Rect outer, Paint paint, BorderSide? side, bool fill) {
|
||||
if (fill) {
|
||||
canvas.drawPath(shape.getOuterPath(outer), paint);
|
||||
}
|
||||
if (side != null) {
|
||||
shape.copyWith(side: side).paint(canvas, outer);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawCheck(Canvas canvas, Offset origin, Paint paint) {
|
||||
final Path path = Path();
|
||||
// The ratios for the offsets below were found from looking at the checkbox
|
||||
// examples on in the HIG docs. The distance from the needed point to the
|
||||
// edge was measured, then devided by the total width.
|
||||
const Offset start = Offset(CupertinoCheckbox.width * 0.25, CupertinoCheckbox.width * 0.52);
|
||||
const Offset mid = Offset(CupertinoCheckbox.width * 0.46, CupertinoCheckbox.width * 0.75);
|
||||
const Offset end = Offset(CupertinoCheckbox.width * 0.72, CupertinoCheckbox.width * 0.29);
|
||||
path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
|
||||
path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
|
||||
canvas.drawPath(path, paint);
|
||||
path.moveTo(origin.dx + mid.dx, origin.dy + mid.dy);
|
||||
path.lineTo(origin.dx + end.dx, origin.dy + end.dy);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawDash(Canvas canvas, Offset origin, Paint paint) {
|
||||
// From measuring the checkbox example in the HIG docs, the dash was found
|
||||
// to be half the total width, centered in the middle.
|
||||
const Offset start = Offset(CupertinoCheckbox.width * 0.25, CupertinoCheckbox.width * 0.5);
|
||||
const Offset end = Offset(CupertinoCheckbox.width * 0.75, CupertinoCheckbox.width * 0.5);
|
||||
canvas.drawLine(origin + start, origin + end, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Paint strokePaint = _createStrokePaint();
|
||||
final Offset origin = size / 2.0 - const Size.square(CupertinoCheckbox.width) / 2.0 as Offset;
|
||||
|
||||
final Rect outer = _outerRectAt(origin);
|
||||
final Paint paint = Paint()..color = _colorAt(value ?? true);
|
||||
|
||||
if (value == false) {
|
||||
|
||||
final BorderSide border = side ?? BorderSide(color: paint.color);
|
||||
_drawBox(canvas, outer, paint, border, false);
|
||||
} else {
|
||||
|
||||
_drawBox(canvas, outer, paint, side, true);
|
||||
if (value ?? false) {
|
||||
_drawCheck(canvas, origin, strokePaint);
|
||||
} else {
|
||||
_drawDash(canvas, origin, strokePaint);
|
||||
}
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
final Rect focusOuter = outer.inflate(1);
|
||||
|
||||
final Paint borderPaint = Paint()
|
||||
..color = focusColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3.5;
|
||||
|
||||
_drawBox(canvas, focusOuter, borderPaint, side, true);
|
||||
}
|
||||
}
|
||||
}
|
247
packages/flutter/lib/src/cupertino/toggleable.dart
Normal file
247
packages/flutter/lib/src/cupertino/toggleable.dart
Normal file
@ -0,0 +1,247 @@
|
||||
// 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 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// A mixin for [StatefulWidget]s that implements iOS-themed toggleable
|
||||
/// controls (e.g.[CupertinoCheckbox]es).
|
||||
///
|
||||
/// This mixin implements the logic for toggling the control when tapped.
|
||||
/// It does not have any opinion about the visual representation of the
|
||||
/// toggleable widget. The visuals are defined by a [CustomPainter] passed to
|
||||
/// the [buildToggleable]. [State] objects using this mixin should call that
|
||||
/// method from their [build] method.
|
||||
///
|
||||
/// This mixin is used to implement the Cupertino components for
|
||||
/// [CupertinoCheckbox] controls.
|
||||
@optionalTypeArgs
|
||||
mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin<S> {
|
||||
|
||||
/// Whether the [value] of this control can be changed by user interaction.
|
||||
///
|
||||
/// The control is considered interactive if the [onChanged] callback is
|
||||
/// non-null. If the callback is null, then the control is disabled and
|
||||
/// non-interactive. A disabled checkbox, for example, is displayed using a
|
||||
/// grey color and its value cannot be changed.
|
||||
bool get isInteractive => onChanged != null;
|
||||
|
||||
/// Called when the control changes value.
|
||||
///
|
||||
/// If the control is tapped, [onChanged] is called immediately with the new
|
||||
/// value.
|
||||
///
|
||||
/// The control is considered interactive (see [isInteractive]) if this
|
||||
/// callback is non-null. If the callback is null, then the control is
|
||||
/// disabled and non-interactive. A disabled checkbox, for example, is
|
||||
/// displayed using a grey color and its value cannot be changed.
|
||||
ValueChanged<bool?>? get onChanged;
|
||||
|
||||
/// The [value] accessor returns false if this control is "inactive" (not
|
||||
/// checked, off, or unselected).
|
||||
///
|
||||
/// If [value] is true then the control "active" (checked, on, or selected). If
|
||||
/// tristate is true and value is null, then the control is considered to be
|
||||
/// in its third or "indeterminate" state..
|
||||
bool? get value;
|
||||
|
||||
/// If true, [value] can be true, false, or null, otherwise [value] must
|
||||
/// be true or false.
|
||||
///
|
||||
/// When [tristate] is true and [value] is null, then the control is
|
||||
/// considered to be in its third or "indeterminate" state.
|
||||
bool get tristate;
|
||||
|
||||
/// The most recent [Offset] at which a pointer touched the Toggleable.
|
||||
///
|
||||
/// This is null if currently no pointer is touching the Toggleable or if
|
||||
/// [isInteractive] is false.
|
||||
Offset? get downPosition => _downPosition;
|
||||
Offset? _downPosition;
|
||||
|
||||
void _handleTapDown(TapDownDetails details) {
|
||||
if (isInteractive) {
|
||||
setState(() {
|
||||
_downPosition = details.localPosition;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTap([Intent? _]) {
|
||||
if (!isInteractive) {
|
||||
return;
|
||||
}
|
||||
switch (value) {
|
||||
case false:
|
||||
onChanged!(true);
|
||||
break;
|
||||
case true:
|
||||
onChanged!(tristate ? null : false);
|
||||
break;
|
||||
case null:
|
||||
onChanged!(false);
|
||||
break;
|
||||
}
|
||||
context.findRenderObject()!.sendSemanticsEvent(const TapSemanticEvent());
|
||||
}
|
||||
|
||||
void _handleTapEnd([TapUpDetails? _]) {
|
||||
if (_downPosition != null) {
|
||||
setState(() { _downPosition = null; });
|
||||
}
|
||||
}
|
||||
|
||||
bool _focused = false;
|
||||
void _handleFocusHighlightChanged(bool focused) {
|
||||
if (focused != _focused) {
|
||||
setState(() { _focused = focused; });
|
||||
}
|
||||
}
|
||||
|
||||
late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
|
||||
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleTap),
|
||||
};
|
||||
|
||||
/// Typically wraps a `painter` that draws the actual visuals of the
|
||||
/// Toggleable with logic to toggle it.
|
||||
///
|
||||
/// Consider providing a subclass of [ToggleablePainter] as a `painter`.
|
||||
///
|
||||
/// This method must be called from the [build] method of the [State] class
|
||||
/// that uses this mixin. The returned [Widget] must be returned from the
|
||||
/// build method - potentially after wrapping it in other widgets.
|
||||
Widget buildToggleable({
|
||||
FocusNode? focusNode,
|
||||
Function(bool)? onFocusChange,
|
||||
bool autofocus = false,
|
||||
required Size size,
|
||||
required CustomPainter painter,
|
||||
}) {
|
||||
return FocusableActionDetector(
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
enabled: isInteractive,
|
||||
actions: _actionMap,
|
||||
onShowFocusHighlight: _handleFocusHighlightChanged,
|
||||
child: GestureDetector(
|
||||
excludeFromSemantics: !isInteractive,
|
||||
onTapDown: isInteractive ? _handleTapDown : null,
|
||||
onTap: isInteractive ? _handleTap : null,
|
||||
onTapUp: isInteractive ? _handleTapEnd : null,
|
||||
onTapCancel: isInteractive ? _handleTapEnd : null,
|
||||
child: Semantics(
|
||||
enabled: isInteractive,
|
||||
child: CustomPaint(
|
||||
size: size,
|
||||
painter: painter,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A base class for a [CustomPainter] that may be passed to
|
||||
/// [ToggleableStateMixin.buildToggleable] to draw the visual representation of
|
||||
/// a Toggleable.
|
||||
///
|
||||
/// Subclasses must implement the [paint] method to draw the actual visuals of
|
||||
/// the Toggleable.
|
||||
abstract class ToggleablePainter extends ChangeNotifier implements CustomPainter {
|
||||
/// The color that should be used in the active state (i.e., when
|
||||
/// [ToggleableStateMixin.value] is true).
|
||||
///
|
||||
/// For example, a checkbox should use this color when checked.
|
||||
Color get activeColor => _activeColor!;
|
||||
Color? _activeColor;
|
||||
set activeColor(Color value) {
|
||||
if (_activeColor == value) {
|
||||
return;
|
||||
}
|
||||
_activeColor = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// The color that should be used in the inactive state (i.e., when
|
||||
/// [ToggleableStateMixin.value] is false).
|
||||
///
|
||||
/// For example, a checkbox should use this color when unchecked.
|
||||
Color get inactiveColor => _inactiveColor!;
|
||||
Color? _inactiveColor;
|
||||
set inactiveColor(Color value) {
|
||||
if (_inactiveColor == value) {
|
||||
return;
|
||||
}
|
||||
_inactiveColor = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// The color that should be used for the reaction when [isFocused] is true.
|
||||
///
|
||||
/// Used when the toggleable needs to change the reaction color/transparency,
|
||||
/// when it has focus.
|
||||
Color get focusColor => _focusColor!;
|
||||
Color? _focusColor;
|
||||
set focusColor(Color value) {
|
||||
if (value == _focusColor) {
|
||||
return;
|
||||
}
|
||||
_focusColor = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// The [Offset] within the Toggleable at which a pointer touched the Toggleable.
|
||||
///
|
||||
/// This is null if currently no pointer is touching the Toggleable.
|
||||
///
|
||||
/// Usually set to [ToggleableStateMixin.downPosition].
|
||||
Offset? get downPosition => _downPosition;
|
||||
Offset? _downPosition;
|
||||
set downPosition(Offset? value) {
|
||||
if (value == _downPosition) {
|
||||
return;
|
||||
}
|
||||
_downPosition = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// True if this toggleable has the input focus.
|
||||
bool get isFocused => _isFocused!;
|
||||
bool? _isFocused;
|
||||
set isFocused(bool? value) {
|
||||
if (value == _isFocused) {
|
||||
return;
|
||||
}
|
||||
_isFocused = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Determines whether the toggleable shows as active.
|
||||
bool get isActive => _isActive!;
|
||||
bool? _isActive;
|
||||
set isActive(bool? value) {
|
||||
if (value == _isActive) {
|
||||
return;
|
||||
}
|
||||
_isActive = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
|
||||
@override
|
||||
bool? hitTest(Offset position) => null;
|
||||
|
||||
@override
|
||||
SemanticsBuilderCallback? get semanticsBuilder => null;
|
||||
|
||||
@override
|
||||
bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => false;
|
||||
|
||||
@override
|
||||
String toString() => describeIdentity(this);
|
||||
}
|
414
packages/flutter/test/cupertino/checkbox_test.dart
Normal file
414
packages/flutter/test/cupertino/checkbox_test.dart
Normal file
@ -0,0 +1,414 @@
|
||||
// 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 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
debugResetSemanticsIdCounter();
|
||||
});
|
||||
|
||||
testWidgets('CupertinoCheckbox semantics', (WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp (
|
||||
home: Center(
|
||||
child: CupertinoCheckbox(
|
||||
value: false,
|
||||
onChanged: (bool? b) { },
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: true,
|
||||
hasTapAction: true,
|
||||
isFocusable: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp (
|
||||
home: Center(
|
||||
child: CupertinoCheckbox(
|
||||
value: true,
|
||||
onChanged: (bool? b) { },
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isChecked: true,
|
||||
isEnabled: true,
|
||||
hasTapAction: true,
|
||||
isFocusable: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp (
|
||||
home: Center(
|
||||
child: CupertinoCheckbox(
|
||||
value: false,
|
||||
onChanged: null,
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSemantics(find.byType(CupertinoCheckbox)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
// isFocusable is delayed by 1 frame.
|
||||
isFocusable: true,
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
// isFocusable should be false now after the 1 frame delay.
|
||||
expect(tester.getSemantics(find.byType(CupertinoCheckbox)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp (
|
||||
home: Center(
|
||||
child: CupertinoCheckbox(
|
||||
value: true,
|
||||
onChanged: null,
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSemantics(find.byType(CupertinoCheckbox)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isChecked: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp (
|
||||
home: Center(
|
||||
child: CupertinoCheckbox(
|
||||
value: null,
|
||||
tristate: true,
|
||||
onChanged: null,
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSemantics(find.byType(CupertinoCheckbox)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isCheckStateMixed: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp (
|
||||
home: Center(
|
||||
child: CupertinoCheckbox(
|
||||
value: true,
|
||||
tristate: true,
|
||||
onChanged: null,
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSemantics(find.byType(CupertinoCheckbox)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isChecked: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(
|
||||
const CupertinoApp (
|
||||
home: Center(
|
||||
child: CupertinoCheckbox(
|
||||
value: false,
|
||||
tristate: true,
|
||||
onChanged: null,
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSemantics(find.byType(CupertinoCheckbox)), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
));
|
||||
|
||||
handle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Can wrap CupertinoCheckbox with Semantics', (WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Semantics(
|
||||
label: 'foo',
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoCheckbox(
|
||||
value: false,
|
||||
onChanged: (bool? b) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
label: 'foo',
|
||||
textDirection: TextDirection.ltr,
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: true,
|
||||
hasTapAction: true,
|
||||
isFocusable: true,
|
||||
));
|
||||
handle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('CupertinoCheckbox tristate: true', (WidgetTester tester) async {
|
||||
bool? checkBoxValue;
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return CupertinoCheckbox(
|
||||
tristate: true,
|
||||
value: checkBoxValue,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
checkBoxValue = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.widget<CupertinoCheckbox>(find.byType(CupertinoCheckbox)).value, null);
|
||||
|
||||
await tester.tap(find.byType(CupertinoCheckbox));
|
||||
await tester.pumpAndSettle();
|
||||
expect(checkBoxValue, false);
|
||||
|
||||
await tester.tap(find.byType(CupertinoCheckbox));
|
||||
await tester.pumpAndSettle();
|
||||
expect(checkBoxValue, true);
|
||||
|
||||
await tester.tap(find.byType(CupertinoCheckbox));
|
||||
await tester.pumpAndSettle();
|
||||
expect(checkBoxValue, null);
|
||||
|
||||
checkBoxValue = true;
|
||||
await tester.pumpAndSettle();
|
||||
expect(checkBoxValue, true);
|
||||
|
||||
checkBoxValue = null;
|
||||
await tester.pumpAndSettle();
|
||||
expect(checkBoxValue, null);
|
||||
});
|
||||
|
||||
testWidgets('has semantics for tristate', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoCheckbox(
|
||||
tristate: true,
|
||||
value: null,
|
||||
onChanged: (bool? newValue) { },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics.nodesWith(
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasCheckedState,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
SemanticsFlag.isCheckStateMixed,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
), hasLength(1));
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoCheckbox(
|
||||
tristate: true,
|
||||
value: true,
|
||||
onChanged: (bool? newValue) { },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics.nodesWith(
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasCheckedState,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isChecked,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
), hasLength(1));
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoCheckbox(
|
||||
tristate: true,
|
||||
value: false,
|
||||
onChanged: (bool? newValue) { },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics.nodesWith(
|
||||
flags: <SemanticsFlag>[
|
||||
SemanticsFlag.hasCheckedState,
|
||||
SemanticsFlag.hasEnabledState,
|
||||
SemanticsFlag.isEnabled,
|
||||
SemanticsFlag.isFocusable,
|
||||
],
|
||||
actions: <SemanticsAction>[SemanticsAction.tap],
|
||||
), hasLength(1));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('has semantic events', (WidgetTester tester) async {
|
||||
dynamic semanticEvent;
|
||||
bool? checkboxValue = false;
|
||||
tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async {
|
||||
semanticEvent = message;
|
||||
});
|
||||
final SemanticsTester semanticsTester = SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return CupertinoCheckbox(
|
||||
value: checkboxValue,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
checkboxValue = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(CupertinoCheckbox));
|
||||
final RenderObject object = tester.firstRenderObject(find.byType(CupertinoCheckbox));
|
||||
|
||||
expect(checkboxValue, true);
|
||||
expect(semanticEvent, <String, dynamic>{
|
||||
'type': 'tap',
|
||||
'nodeId': object.debugSemantics!.id,
|
||||
'data': <String, dynamic>{},
|
||||
});
|
||||
expect(object.debugSemantics!.getSemanticsData().hasAction(SemanticsAction.tap), true);
|
||||
|
||||
tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null);
|
||||
semanticsTester.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Checkbox can be toggled by keyboard shortcuts', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
bool? value = true;
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return CupertinoApp(
|
||||
home: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return CupertinoCheckbox(
|
||||
value: value,
|
||||
onChanged: enabled ? (bool? newValue) {
|
||||
setState(() {
|
||||
value = newValue;
|
||||
});
|
||||
} : null,
|
||||
autofocus: true,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
// On web, switches don't respond to the enter key.
|
||||
expect(value, kIsWeb ? isTrue : isFalse);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isTrue);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isFalse);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('Checkbox respects shape and side', (WidgetTester tester) async {
|
||||
const RoundedRectangleBorder roundedRectangleBorder =
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5)));
|
||||
|
||||
const BorderSide side = BorderSide(
|
||||
width: 4,
|
||||
color: Color(0xfff44336),
|
||||
);
|
||||
|
||||
Widget buildApp() {
|
||||
return CupertinoApp(
|
||||
home: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return CupertinoCheckbox(
|
||||
value: false,
|
||||
onChanged: (bool? newValue) {},
|
||||
shape: roundedRectangleBorder,
|
||||
side: side,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.widget<CupertinoCheckbox>(find.byType(CupertinoCheckbox)).shape, roundedRectangleBorder);
|
||||
expect(tester.widget<CupertinoCheckbox>(find.byType(CupertinoCheckbox)).side, side);
|
||||
expect(
|
||||
find.byType(CupertinoCheckbox),
|
||||
paints
|
||||
..drrect(
|
||||
color: const Color(0xfff44336),
|
||||
outer: RRect.fromLTRBR(13.0, 13.0, 31.0, 31.0, const Radius.circular(5)),
|
||||
inner: RRect.fromLTRBR(17.0, 17.0, 27.0, 27.0, const Radius.circular(1)),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user