blob: dacdab762b50c1312e018df06bd3bdb33d4ee4fa [file] [log] [blame]
// 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);
}