mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
801 lines
26 KiB
Dart
801 lines
26 KiB
Dart
// 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/gestures.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import 'colors.dart';
|
|
import 'constants.dart';
|
|
import 'debug.dart';
|
|
import 'shadows.dart';
|
|
import 'theme.dart';
|
|
import 'theme_data.dart';
|
|
import 'toggleable.dart';
|
|
|
|
const double _kTrackHeight = 14.0;
|
|
const double _kTrackWidth = 33.0;
|
|
const double _kTrackRadius = _kTrackHeight / 2.0;
|
|
const double _kThumbRadius = 10.0;
|
|
const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius;
|
|
const double _kSwitchHeight = 2 * kRadialReactionRadius + 8.0;
|
|
const double _kSwitchHeightCollapsed = 2 * kRadialReactionRadius;
|
|
|
|
enum _SwitchType { material, adaptive }
|
|
|
|
/// A material design switch.
|
|
///
|
|
/// Used to toggle the on/off state of a single setting.
|
|
///
|
|
/// The switch itself does not maintain any state. Instead, when the state of
|
|
/// the switch changes, the widget calls the [onChanged] callback. Most widgets
|
|
/// that use a switch will listen for the [onChanged] callback and rebuild the
|
|
/// switch with a new [value] to update the visual appearance of the switch.
|
|
///
|
|
/// If the [onChanged] callback is null, then the switch will be disabled (it
|
|
/// will not respond to input). A disabled switch's thumb and track are rendered
|
|
/// in shades of grey by default. The default appearance of a disabled switch
|
|
/// can be overridden with [inactiveThumbColor] and [inactiveTrackColor].
|
|
///
|
|
/// Requires one of its ancestors to be a [Material] widget.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [SwitchListTile], which combines this widget with a [ListTile] so that
|
|
/// you can give the switch a label.
|
|
/// * [Checkbox], another widget with similar semantics.
|
|
/// * [Radio], for selecting among a set of explicit values.
|
|
/// * [Slider], for selecting a value in a range.
|
|
/// * <https://material.io/design/components/selection-controls.html#switches>
|
|
class Switch extends StatefulWidget {
|
|
/// Creates a material design switch.
|
|
///
|
|
/// The switch itself does not maintain any state. Instead, when the state of
|
|
/// the switch changes, the widget calls the [onChanged] callback. Most widgets
|
|
/// that use a switch will listen for the [onChanged] callback and rebuild the
|
|
/// switch with a new [value] to update the visual appearance of the switch.
|
|
///
|
|
/// The following arguments are required:
|
|
///
|
|
/// * [value] determines whether this switch is on or off.
|
|
/// * [onChanged] is called when the user toggles the switch on or off.
|
|
const Switch({
|
|
Key key,
|
|
@required this.value,
|
|
@required this.onChanged,
|
|
this.activeColor,
|
|
this.activeTrackColor,
|
|
this.inactiveThumbColor,
|
|
this.inactiveTrackColor,
|
|
this.activeThumbImage,
|
|
this.onActiveThumbImageError,
|
|
this.inactiveThumbImage,
|
|
this.onInactiveThumbImageError,
|
|
this.materialTapTargetSize,
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
this.focusColor,
|
|
this.hoverColor,
|
|
this.focusNode,
|
|
this.autofocus = false,
|
|
}) : _switchType = _SwitchType.material,
|
|
assert(dragStartBehavior != null),
|
|
assert(activeThumbImage != null || onActiveThumbImageError == null),
|
|
assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
|
|
super(key: key);
|
|
|
|
/// Creates a [CupertinoSwitch] if the target platform is iOS, creates a
|
|
/// material design switch otherwise.
|
|
///
|
|
/// If a [CupertinoSwitch] is created, the following parameters are
|
|
/// ignored: [activeTrackColor], [inactiveThumbColor], [inactiveTrackColor],
|
|
/// [activeThumbImage], [onActiveThumbImageError], [inactiveThumbImage],
|
|
/// [onInactiveImageThumbError], [materialTapTargetSize].
|
|
///
|
|
/// The target platform is based on the current [Theme]: [ThemeData.platform].
|
|
const Switch.adaptive({
|
|
Key key,
|
|
@required this.value,
|
|
@required this.onChanged,
|
|
this.activeColor,
|
|
this.activeTrackColor,
|
|
this.inactiveThumbColor,
|
|
this.inactiveTrackColor,
|
|
this.activeThumbImage,
|
|
this.onActiveThumbImageError,
|
|
this.inactiveThumbImage,
|
|
this.onInactiveThumbImageError,
|
|
this.materialTapTargetSize,
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
this.focusColor,
|
|
this.hoverColor,
|
|
this.focusNode,
|
|
this.autofocus = false,
|
|
}) : assert(autofocus != null),
|
|
assert(activeThumbImage != null || onActiveThumbImageError == null),
|
|
assert(inactiveThumbImage != null || onInactiveThumbImageError == null),
|
|
_switchType = _SwitchType.adaptive,
|
|
super(key: key);
|
|
|
|
/// Whether this switch is on or off.
|
|
///
|
|
/// This property must not be null.
|
|
final bool value;
|
|
|
|
/// Called when the user toggles the switch on or off.
|
|
///
|
|
/// The switch passes the new value to the callback but does not actually
|
|
/// change state until the parent widget rebuilds the switch with the new
|
|
/// value.
|
|
///
|
|
/// If null, the switch will be displayed as disabled.
|
|
///
|
|
/// 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
|
|
/// Switch(
|
|
/// value: _giveVerse,
|
|
/// onChanged: (bool newValue) {
|
|
/// setState(() {
|
|
/// _giveVerse = newValue;
|
|
/// });
|
|
/// },
|
|
/// )
|
|
/// ```
|
|
final ValueChanged<bool> onChanged;
|
|
|
|
/// The color to use when this switch is on.
|
|
///
|
|
/// Defaults to [ThemeData.toggleableActiveColor].
|
|
final Color activeColor;
|
|
|
|
/// The color to use on the track when this switch is on.
|
|
///
|
|
/// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%.
|
|
///
|
|
/// Ignored if this switch is created with [Switch.adaptive].
|
|
final Color activeTrackColor;
|
|
|
|
/// The color to use on the thumb when this switch is off.
|
|
///
|
|
/// Defaults to the colors described in the Material design specification.
|
|
///
|
|
/// Ignored if this switch is created with [Switch.adaptive].
|
|
final Color inactiveThumbColor;
|
|
|
|
/// The color to use on the track when this switch is off.
|
|
///
|
|
/// Defaults to the colors described in the Material design specification.
|
|
///
|
|
/// Ignored if this switch is created with [Switch.adaptive].
|
|
final Color inactiveTrackColor;
|
|
|
|
/// An image to use on the thumb of this switch when the switch is on.
|
|
///
|
|
/// Ignored if this switch is created with [Switch.adaptive].
|
|
final ImageProvider activeThumbImage;
|
|
|
|
/// An optional error callback for errors emitted when loading
|
|
/// [activeThumbImage].
|
|
final ImageErrorListener onActiveThumbImageError;
|
|
|
|
/// An image to use on the thumb of this switch when the switch is off.
|
|
///
|
|
/// Ignored if this switch is created with [Switch.adaptive].
|
|
final ImageProvider inactiveThumbImage;
|
|
|
|
/// An optional error callback for errors emitted when loading
|
|
/// [inactiveThumbImage].
|
|
final ImageErrorListener onInactiveThumbImageError;
|
|
|
|
/// Configures the minimum size of the tap target.
|
|
///
|
|
/// Defaults to [ThemeData.materialTapTargetSize].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
|
|
final MaterialTapTargetSize materialTapTargetSize;
|
|
|
|
final _SwitchType _switchType;
|
|
|
|
/// {@macro flutter.cupertino.switch.dragStartBehavior}
|
|
final DragStartBehavior dragStartBehavior;
|
|
|
|
/// The color for the button's [Material] when it has the input focus.
|
|
final Color focusColor;
|
|
|
|
/// The color for the button's [Material] when a pointer is hovering over it.
|
|
final Color hoverColor;
|
|
|
|
/// {@macro flutter.widgets.Focus.focusNode}
|
|
final FocusNode focusNode;
|
|
|
|
/// {@macro flutter.widgets.Focus.autofocus}
|
|
final bool autofocus;
|
|
|
|
@override
|
|
_SwitchState createState() => _SwitchState();
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
|
|
properties.add(ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
|
|
}
|
|
}
|
|
|
|
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
|
Map<LocalKey, ActionFactory> _actionMap;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_actionMap = <LocalKey, ActionFactory>{
|
|
ActivateAction.key: _createAction,
|
|
};
|
|
}
|
|
|
|
void _actionHandler(FocusNode node, Intent intent){
|
|
if (widget.onChanged != null) {
|
|
widget.onChanged(!widget.value);
|
|
}
|
|
final RenderObject renderObject = node.context.findRenderObject();
|
|
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
|
}
|
|
|
|
Action _createAction() {
|
|
return CallbackAction(
|
|
ActivateAction.key,
|
|
onInvoke: _actionHandler,
|
|
);
|
|
}
|
|
|
|
bool _focused = false;
|
|
void _handleFocusHighlightChanged(bool focused) {
|
|
if (focused != _focused) {
|
|
setState(() { _focused = focused; });
|
|
}
|
|
}
|
|
|
|
bool _hovering = false;
|
|
void _handleHoverChanged(bool hovering) {
|
|
if (hovering != _hovering) {
|
|
setState(() { _hovering = hovering; });
|
|
}
|
|
}
|
|
|
|
Size getSwitchSize(ThemeData theme) {
|
|
switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
|
|
case MaterialTapTargetSize.padded:
|
|
return const Size(_kSwitchWidth, _kSwitchHeight);
|
|
break;
|
|
case MaterialTapTargetSize.shrinkWrap:
|
|
return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
|
|
break;
|
|
}
|
|
assert(false);
|
|
return null;
|
|
}
|
|
|
|
bool get enabled => widget.onChanged != null;
|
|
|
|
void _didFinishDragging() {
|
|
// The user has finished dragging the thumb of this switch. Rebuild the switch
|
|
// to update the animation.
|
|
setState(() {});
|
|
}
|
|
|
|
Widget buildMaterialSwitch(BuildContext context) {
|
|
assert(debugCheckHasMaterial(context));
|
|
final ThemeData theme = Theme.of(context);
|
|
final bool isDark = theme.brightness == Brightness.dark;
|
|
|
|
final Color activeThumbColor = widget.activeColor ?? theme.toggleableActiveColor;
|
|
final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80);
|
|
final Color hoverColor = widget.hoverColor ?? theme.hoverColor;
|
|
final Color focusColor = widget.focusColor ?? theme.focusColor;
|
|
|
|
Color inactiveThumbColor;
|
|
Color inactiveTrackColor;
|
|
if (enabled) {
|
|
const Color black32 = Color(0x52000000); // Black with 32% opacity
|
|
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50);
|
|
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : black32);
|
|
} else {
|
|
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
|
|
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
|
|
}
|
|
|
|
return FocusableActionDetector(
|
|
actions: _actionMap,
|
|
focusNode: widget.focusNode,
|
|
autofocus: widget.autofocus,
|
|
enabled: enabled,
|
|
onShowFocusHighlight: _handleFocusHighlightChanged,
|
|
onShowHoverHighlight: _handleHoverChanged,
|
|
child: Builder(
|
|
builder: (BuildContext context) {
|
|
return _SwitchRenderObjectWidget(
|
|
dragStartBehavior: widget.dragStartBehavior,
|
|
value: widget.value,
|
|
activeColor: activeThumbColor,
|
|
inactiveColor: inactiveThumbColor,
|
|
hoverColor: hoverColor,
|
|
focusColor: focusColor,
|
|
activeThumbImage: widget.activeThumbImage,
|
|
onActiveThumbImageError: widget.onActiveThumbImageError,
|
|
inactiveThumbImage: widget.inactiveThumbImage,
|
|
onInactiveThumbImageError: widget.onInactiveThumbImageError,
|
|
activeTrackColor: activeTrackColor,
|
|
inactiveTrackColor: inactiveTrackColor,
|
|
configuration: createLocalImageConfiguration(context),
|
|
onChanged: widget.onChanged,
|
|
additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)),
|
|
hasFocus: _focused,
|
|
hovering: _hovering,
|
|
state: this,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildCupertinoSwitch(BuildContext context) {
|
|
final Size size = getSwitchSize(Theme.of(context));
|
|
return Focus(
|
|
focusNode: widget.focusNode,
|
|
autofocus: widget.autofocus,
|
|
child: Container(
|
|
width: size.width, // Same size as the Material switch.
|
|
height: size.height,
|
|
alignment: Alignment.center,
|
|
child: CupertinoSwitch(
|
|
dragStartBehavior: widget.dragStartBehavior,
|
|
value: widget.value,
|
|
onChanged: widget.onChanged,
|
|
activeColor: widget.activeColor,
|
|
trackColor: widget.inactiveTrackColor
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
switch (widget._switchType) {
|
|
case _SwitchType.material:
|
|
return buildMaterialSwitch(context);
|
|
|
|
case _SwitchType.adaptive: {
|
|
final ThemeData theme = Theme.of(context);
|
|
assert(theme.platform != null);
|
|
switch (theme.platform) {
|
|
case TargetPlatform.android:
|
|
case TargetPlatform.fuchsia:
|
|
case TargetPlatform.linux:
|
|
case TargetPlatform.windows:
|
|
return buildMaterialSwitch(context);
|
|
case TargetPlatform.iOS:
|
|
case TargetPlatform.macOS:
|
|
return buildCupertinoSwitch(context);
|
|
}
|
|
}
|
|
}
|
|
assert(false);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
|
|
const _SwitchRenderObjectWidget({
|
|
Key key,
|
|
this.value,
|
|
this.activeColor,
|
|
this.inactiveColor,
|
|
this.hoverColor,
|
|
this.focusColor,
|
|
this.activeThumbImage,
|
|
this.onActiveThumbImageError,
|
|
this.inactiveThumbImage,
|
|
this.onInactiveThumbImageError,
|
|
this.activeTrackColor,
|
|
this.inactiveTrackColor,
|
|
this.configuration,
|
|
this.onChanged,
|
|
this.additionalConstraints,
|
|
this.dragStartBehavior,
|
|
this.hasFocus,
|
|
this.hovering,
|
|
this.state,
|
|
}) : super(key: key);
|
|
|
|
final bool value;
|
|
final Color activeColor;
|
|
final Color inactiveColor;
|
|
final Color hoverColor;
|
|
final Color focusColor;
|
|
final ImageProvider activeThumbImage;
|
|
final ImageErrorListener onActiveThumbImageError;
|
|
final ImageProvider inactiveThumbImage;
|
|
final ImageErrorListener onInactiveThumbImageError;
|
|
final Color activeTrackColor;
|
|
final Color inactiveTrackColor;
|
|
final ImageConfiguration configuration;
|
|
final ValueChanged<bool> onChanged;
|
|
final BoxConstraints additionalConstraints;
|
|
final DragStartBehavior dragStartBehavior;
|
|
final bool hasFocus;
|
|
final bool hovering;
|
|
final _SwitchState state;
|
|
|
|
@override
|
|
_RenderSwitch createRenderObject(BuildContext context) {
|
|
return _RenderSwitch(
|
|
dragStartBehavior: dragStartBehavior,
|
|
value: value,
|
|
activeColor: activeColor,
|
|
inactiveColor: inactiveColor,
|
|
hoverColor: hoverColor,
|
|
focusColor: focusColor,
|
|
activeThumbImage: activeThumbImage,
|
|
onActiveThumbImageError: onActiveThumbImageError,
|
|
inactiveThumbImage: inactiveThumbImage,
|
|
onInactiveThumbImageError: onInactiveThumbImageError,
|
|
activeTrackColor: activeTrackColor,
|
|
inactiveTrackColor: inactiveTrackColor,
|
|
configuration: configuration,
|
|
onChanged: onChanged,
|
|
textDirection: Directionality.of(context),
|
|
additionalConstraints: additionalConstraints,
|
|
hasFocus: hasFocus,
|
|
hovering: hovering,
|
|
state: state,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
|
|
renderObject
|
|
..value = value
|
|
..activeColor = activeColor
|
|
..inactiveColor = inactiveColor
|
|
..hoverColor = hoverColor
|
|
..focusColor = focusColor
|
|
..activeThumbImage = activeThumbImage
|
|
..onActiveThumbImageError = onActiveThumbImageError
|
|
..inactiveThumbImage = inactiveThumbImage
|
|
..onInactiveThumbImageError = onInactiveThumbImageError
|
|
..activeTrackColor = activeTrackColor
|
|
..inactiveTrackColor = inactiveTrackColor
|
|
..configuration = configuration
|
|
..onChanged = onChanged
|
|
..textDirection = Directionality.of(context)
|
|
..additionalConstraints = additionalConstraints
|
|
..dragStartBehavior = dragStartBehavior
|
|
..hasFocus = hasFocus
|
|
..hovering = hovering
|
|
..vsync = state;
|
|
}
|
|
}
|
|
|
|
class _RenderSwitch extends RenderToggleable {
|
|
_RenderSwitch({
|
|
bool value,
|
|
Color activeColor,
|
|
Color inactiveColor,
|
|
Color hoverColor,
|
|
Color focusColor,
|
|
ImageProvider activeThumbImage,
|
|
ImageErrorListener onActiveThumbImageError,
|
|
ImageProvider inactiveThumbImage,
|
|
ImageErrorListener onInactiveThumbImageError,
|
|
Color activeTrackColor,
|
|
Color inactiveTrackColor,
|
|
ImageConfiguration configuration,
|
|
BoxConstraints additionalConstraints,
|
|
@required TextDirection textDirection,
|
|
ValueChanged<bool> onChanged,
|
|
DragStartBehavior dragStartBehavior,
|
|
bool hasFocus,
|
|
bool hovering,
|
|
@required this.state,
|
|
}) : assert(textDirection != null),
|
|
_activeThumbImage = activeThumbImage,
|
|
_onActiveThumbImageError = onActiveThumbImageError,
|
|
_inactiveThumbImage = inactiveThumbImage,
|
|
_onInactiveThumbImageError = onInactiveThumbImageError,
|
|
_activeTrackColor = activeTrackColor,
|
|
_inactiveTrackColor = inactiveTrackColor,
|
|
_configuration = configuration,
|
|
_textDirection = textDirection,
|
|
super(
|
|
value: value,
|
|
tristate: false,
|
|
activeColor: activeColor,
|
|
inactiveColor: inactiveColor,
|
|
hoverColor: hoverColor,
|
|
focusColor: focusColor,
|
|
onChanged: onChanged,
|
|
additionalConstraints: additionalConstraints,
|
|
hasFocus: hasFocus,
|
|
hovering: hovering,
|
|
vsync: state,
|
|
) {
|
|
_drag = HorizontalDragGestureRecognizer()
|
|
..onStart = _handleDragStart
|
|
..onUpdate = _handleDragUpdate
|
|
..onEnd = _handleDragEnd
|
|
..dragStartBehavior = dragStartBehavior;
|
|
}
|
|
|
|
ImageProvider get activeThumbImage => _activeThumbImage;
|
|
ImageProvider _activeThumbImage;
|
|
set activeThumbImage(ImageProvider value) {
|
|
if (value == _activeThumbImage)
|
|
return;
|
|
_activeThumbImage = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
ImageErrorListener get onActiveThumbImageError => _onActiveThumbImageError;
|
|
ImageErrorListener _onActiveThumbImageError;
|
|
set onActiveThumbImageError(ImageErrorListener value) {
|
|
if (value == _onActiveThumbImageError) {
|
|
return;
|
|
}
|
|
_onActiveThumbImageError = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
ImageProvider get inactiveThumbImage => _inactiveThumbImage;
|
|
ImageProvider _inactiveThumbImage;
|
|
set inactiveThumbImage(ImageProvider value) {
|
|
if (value == _inactiveThumbImage)
|
|
return;
|
|
_inactiveThumbImage = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
ImageErrorListener get onInactiveThumbImageError => _onInactiveThumbImageError;
|
|
ImageErrorListener _onInactiveThumbImageError;
|
|
set onInactiveThumbImageError(ImageErrorListener value) {
|
|
if (value == _onInactiveThumbImageError) {
|
|
return;
|
|
}
|
|
_onInactiveThumbImageError = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
Color get activeTrackColor => _activeTrackColor;
|
|
Color _activeTrackColor;
|
|
set activeTrackColor(Color value) {
|
|
assert(value != null);
|
|
if (value == _activeTrackColor)
|
|
return;
|
|
_activeTrackColor = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
Color get inactiveTrackColor => _inactiveTrackColor;
|
|
Color _inactiveTrackColor;
|
|
set inactiveTrackColor(Color value) {
|
|
assert(value != null);
|
|
if (value == _inactiveTrackColor)
|
|
return;
|
|
_inactiveTrackColor = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
ImageConfiguration get configuration => _configuration;
|
|
ImageConfiguration _configuration;
|
|
set configuration(ImageConfiguration value) {
|
|
assert(value != null);
|
|
if (value == _configuration)
|
|
return;
|
|
_configuration = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
TextDirection get textDirection => _textDirection;
|
|
TextDirection _textDirection;
|
|
set textDirection(TextDirection value) {
|
|
assert(value != null);
|
|
if (_textDirection == value)
|
|
return;
|
|
_textDirection = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
DragStartBehavior get dragStartBehavior => _drag.dragStartBehavior;
|
|
set dragStartBehavior(DragStartBehavior value) {
|
|
assert(value != null);
|
|
if (_drag.dragStartBehavior == value)
|
|
return;
|
|
_drag.dragStartBehavior = value;
|
|
}
|
|
|
|
_SwitchState state;
|
|
|
|
@override
|
|
set value(bool newValue) {
|
|
assert(value != null);
|
|
super.value = newValue;
|
|
// The widget is rebuilt and we have pending position animation to play.
|
|
if (_needsPositionAnimation) {
|
|
_needsPositionAnimation = false;
|
|
position
|
|
..curve = null
|
|
..reverseCurve = null;
|
|
if (newValue)
|
|
positionController.forward();
|
|
else
|
|
positionController.reverse();
|
|
}
|
|
}
|
|
|
|
|
|
@override
|
|
void detach() {
|
|
_cachedThumbPainter?.dispose();
|
|
_cachedThumbPainter = null;
|
|
super.detach();
|
|
}
|
|
|
|
double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius;
|
|
|
|
HorizontalDragGestureRecognizer _drag;
|
|
|
|
bool _needsPositionAnimation = false;
|
|
|
|
void _handleDragStart(DragStartDetails details) {
|
|
if (isInteractive)
|
|
reactionController.forward();
|
|
}
|
|
|
|
void _handleDragUpdate(DragUpdateDetails details) {
|
|
if (isInteractive) {
|
|
position
|
|
..curve = null
|
|
..reverseCurve = null;
|
|
final double delta = details.primaryDelta / _trackInnerLength;
|
|
switch (textDirection) {
|
|
case TextDirection.rtl:
|
|
positionController.value -= delta;
|
|
break;
|
|
case TextDirection.ltr:
|
|
positionController.value += delta;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _handleDragEnd(DragEndDetails details) {
|
|
_needsPositionAnimation = true;
|
|
|
|
if (position.value >= 0.5 != value)
|
|
onChanged(!value);
|
|
reactionController.reverse();
|
|
state._didFinishDragging();
|
|
}
|
|
|
|
@override
|
|
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
|
|
assert(debugHandleEvent(event, entry));
|
|
if (event is PointerDownEvent && onChanged != null)
|
|
_drag.addPointer(event);
|
|
super.handleEvent(event, entry);
|
|
}
|
|
|
|
Color _cachedThumbColor;
|
|
ImageProvider _cachedThumbImage;
|
|
ImageErrorListener _cachedThumbErrorListener;
|
|
BoxPainter _cachedThumbPainter;
|
|
|
|
BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image, ImageErrorListener errorListener) {
|
|
return BoxDecoration(
|
|
color: color,
|
|
image: image == null ? null : DecorationImage(image: image, onError: errorListener),
|
|
shape: BoxShape.circle,
|
|
boxShadow: kElevationToShadow[1],
|
|
);
|
|
}
|
|
|
|
bool _isPainting = false;
|
|
|
|
void _handleDecorationChanged() {
|
|
// If the image decoration is available synchronously, we'll get called here
|
|
// during paint. There's no reason to mark ourselves as needing paint if we
|
|
// are already in the middle of painting. (In fact, doing so would trigger
|
|
// an assert).
|
|
if (!_isPainting)
|
|
markNeedsPaint();
|
|
}
|
|
|
|
@override
|
|
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
|
super.describeSemanticsConfiguration(config);
|
|
config.isToggled = value == true;
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
final Canvas canvas = context.canvas;
|
|
final bool isEnabled = onChanged != null;
|
|
final double currentValue = position.value;
|
|
|
|
double visualPosition;
|
|
switch (textDirection) {
|
|
case TextDirection.rtl:
|
|
visualPosition = 1.0 - currentValue;
|
|
break;
|
|
case TextDirection.ltr:
|
|
visualPosition = currentValue;
|
|
break;
|
|
}
|
|
|
|
final Color trackColor = isEnabled
|
|
? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)
|
|
: inactiveTrackColor;
|
|
|
|
final Color thumbColor = isEnabled
|
|
? Color.lerp(inactiveColor, activeColor, currentValue)
|
|
: inactiveColor;
|
|
|
|
final ImageProvider thumbImage = isEnabled
|
|
? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
|
|
: inactiveThumbImage;
|
|
|
|
final ImageErrorListener thumbErrorListener = isEnabled
|
|
? (currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError)
|
|
: onInactiveThumbImageError;
|
|
|
|
// Paint the track
|
|
final Paint paint = Paint()
|
|
..color = trackColor;
|
|
const double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
|
|
final Rect trackRect = Rect.fromLTWH(
|
|
offset.dx + trackHorizontalPadding,
|
|
offset.dy + (size.height - _kTrackHeight) / 2.0,
|
|
size.width - 2.0 * trackHorizontalPadding,
|
|
_kTrackHeight,
|
|
);
|
|
final RRect trackRRect = RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
|
|
canvas.drawRRect(trackRRect, paint);
|
|
|
|
final Offset thumbPosition = Offset(
|
|
kRadialReactionRadius + visualPosition * _trackInnerLength,
|
|
size.height / 2.0,
|
|
);
|
|
|
|
paintRadialReaction(canvas, offset, thumbPosition);
|
|
|
|
try {
|
|
_isPainting = true;
|
|
BoxPainter thumbPainter;
|
|
if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage || thumbErrorListener != _cachedThumbErrorListener) {
|
|
_cachedThumbColor = thumbColor;
|
|
_cachedThumbImage = thumbImage;
|
|
_cachedThumbErrorListener = thumbErrorListener;
|
|
_cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage, thumbErrorListener).createBoxPainter(_handleDecorationChanged);
|
|
}
|
|
thumbPainter = _cachedThumbPainter;
|
|
|
|
// The thumb contracts slightly during the animation
|
|
final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
|
|
final double radius = _kThumbRadius - inset;
|
|
thumbPainter.paint(
|
|
canvas,
|
|
thumbPosition + offset - Offset(radius, radius),
|
|
configuration.copyWith(size: Size.fromRadius(radius)),
|
|
);
|
|
} finally {
|
|
_isPainting = false;
|
|
}
|
|
}
|
|
}
|