| // 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; |
| } |
| } |
| } |