| // 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); |
| case true: |
| onChanged!(tristate ? null : false); |
| case null: |
| onChanged!(false); |
| } |
| 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); |
| } |