| // Copyright 2015 The Chromium 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 'dart:math' as math; |
| |
| 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 'theme.dart'; |
| import 'typography.dart'; |
| |
| /// A material design slider. |
| /// |
| /// Used to select from a range of values. |
| /// |
| /// A slider can be used to select from either a continuous or a discrete set of |
| /// values. The default is use a continuous range of values from [min] to [max]. |
| /// To use discrete values, use a non-null value for [divisions], which |
| /// indicates the number of discrete intervals. For example, if [min] is 0.0 and |
| /// [max] is 50.0 and [divisions] is 5, then the slider can take on the values |
| /// discrete values 0.0, 10.0, 20.0, 30.0, 40.0, and 50.0. |
| /// |
| /// The slider will be disabled if [onChanged] is null or if the range given by |
| /// [min]..[max] is empty (i.e. if [min] is equal to [max]). |
| /// |
| /// The slider itself does not maintain any state. Instead, when the state of |
| /// the slider changes, the widget calls the [onChanged] callback. Most widgets |
| /// that use a slider will listen for the [onChanged] callback and rebuild the |
| /// slider with a new [value] to update the visual appearance of the slider. |
| /// |
| /// By default, a slider will be as wide as possible, centered vertically. When |
| /// given unbounded constraints, it will attempt to make the track 144 pixels |
| /// wide (with margins on each side) and will shrink-wrap vertically. |
| /// |
| /// Requires one of its ancestors to be a [Material] widget. |
| /// |
| /// See also: |
| /// |
| /// * [Radio], for selecting among a set of explicit values. |
| /// * [Checkbox] and [Switch], for toggling a particular value on or off. |
| /// * <https://material.google.com/components/sliders.html> |
| class Slider extends StatefulWidget { |
| /// Creates a material design slider. |
| /// |
| /// The slider itself does not maintain any state. Instead, when the state of |
| /// the slider changes, the widget calls the [onChanged] callback. Most widgets |
| /// that use a slider will listen for the [onChanged] callback and rebuild the |
| /// slider with a new [value] to update the visual appearance of the slider. |
| /// |
| /// * [value] determines currently selected value for this slider. |
| /// * [onChanged] is called when the user selects a new value for the slider. |
| const Slider({ |
| Key key, |
| @required this.value, |
| @required this.onChanged, |
| this.min: 0.0, |
| this.max: 1.0, |
| this.divisions, |
| this.label, |
| this.activeColor, |
| this.inactiveColor, |
| this.thumbOpenAtMin: false, |
| }) : assert(value != null), |
| assert(min != null), |
| assert(max != null), |
| assert(min <= max), |
| assert(value >= min && value <= max), |
| assert(divisions == null || divisions > 0), |
| assert(thumbOpenAtMin != null), |
| super(key: key); |
| |
| /// The currently selected value for this slider. |
| /// |
| /// The slider's thumb is drawn at a position that corresponds to this value. |
| final double value; |
| |
| /// Called when the user selects a new value for the slider. |
| /// |
| /// The slider passes the new value to the callback but does not actually |
| /// change state until the parent widget rebuilds the slider with the new |
| /// value. |
| /// |
| /// If null, the slider 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 |
| /// new Slider( |
| /// value: _duelCommandment.toDouble(), |
| /// min: 1.0, |
| /// max: 10.0, |
| /// divisions: 10, |
| /// label: '$_duelCommandment', |
| /// onChanged: (double newValue) { |
| /// setState(() { |
| /// _duelCommandment = newValue.round(); |
| /// }); |
| /// }, |
| /// ) |
| /// ``` |
| final ValueChanged<double> onChanged; |
| |
| /// The minimum value the user can select. |
| /// |
| /// Defaults to 0.0. Must be less than or equal to [max]. |
| /// |
| /// If the [max] is equal to the [min], then the slider is disabled. |
| final double min; |
| |
| /// The maximum value the user can select. |
| /// |
| /// Defaults to 1.0. Must be greater than or equal to [min]. |
| /// |
| /// If the [max] is equal to the [min], then the slider is disabled. |
| final double max; |
| |
| /// The number of discrete divisions. |
| /// |
| /// Typically used with [label] to show the current discrete value. |
| /// |
| /// If null, the slider is continuous. |
| final int divisions; |
| |
| /// A label to show above the slider when the slider is active. |
| /// |
| /// Typically used to display the value of a discrete slider. |
| final String label; |
| |
| /// The color to use for the portion of the slider that has been selected. |
| /// |
| /// Defaults to accent color of the current [Theme]. |
| final Color activeColor; |
| |
| /// The color for the unselected portion of the slider. |
| /// |
| /// Defaults to the unselected widget color of the current [Theme]. |
| final Color inactiveColor; |
| |
| /// Whether the thumb should be an open circle when the slider is at its minimum position. |
| /// |
| /// When this property is false, the thumb does not change when it the slider |
| /// reaches its minimum position. |
| /// |
| /// This property is useful, for example, when the minimum value represents a |
| /// qualitatively different state. For a slider that controls the volume of |
| /// a sound, for example, the minimum value represents "no sound at all," |
| /// which is qualitatively different from even a very soft sound. |
| /// |
| /// Defaults to false. |
| final bool thumbOpenAtMin; |
| |
| @override |
| _SliderState createState() => new _SliderState(); |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder description) { |
| super.debugFillProperties(description); |
| description.add(new DoubleProperty('value', value)); |
| description.add(new DoubleProperty('min', min)); |
| description.add(new DoubleProperty('max', max)); |
| } |
| } |
| |
| class _SliderState extends State<Slider> with TickerProviderStateMixin { |
| _SliderState() { |
| _reactionController = new AnimationController( |
| duration: kRadialReactionDuration, |
| vsync: this, |
| ); |
| } |
| |
| void _handleChanged(double value) { |
| assert(widget.onChanged != null); |
| widget.onChanged(value * (widget.max - widget.min) + widget.min); |
| } |
| |
| @override |
| void dispose() { |
| _reactionController?.dispose(); |
| super.dispose(); |
| } |
| |
| // Have to keep the reaction controller here so that we may dispose of it |
| // properly. |
| AnimationController _reactionController; |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasMaterial(context)); |
| final ThemeData theme = Theme.of(context); |
| return new _SliderRenderObjectWidget( |
| value: widget.max > widget.min ? (widget.value - widget.min) / (widget.max - widget.min) : 0.0, |
| divisions: widget.divisions, |
| label: widget.label, |
| activeColor: widget.activeColor ?? theme.accentColor, |
| inactiveColor: widget.inactiveColor ?? theme.unselectedWidgetColor, |
| thumbOpenAtMin: widget.thumbOpenAtMin, |
| textTheme: theme.accentTextTheme, |
| textScaleFactor: MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0, |
| onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, |
| vsync: this, |
| reactionController: _reactionController, |
| ); |
| } |
| } |
| |
| class _SliderRenderObjectWidget extends LeafRenderObjectWidget { |
| const _SliderRenderObjectWidget({ |
| Key key, |
| this.value, |
| this.divisions, |
| this.label, |
| this.activeColor, |
| this.inactiveColor, |
| this.thumbOpenAtMin, |
| this.textTheme, |
| this.textScaleFactor, |
| this.onChanged, |
| this.vsync, |
| this.reactionController, |
| }) : super(key: key); |
| |
| final double value; |
| final int divisions; |
| final String label; |
| final Color activeColor; |
| final Color inactiveColor; |
| final bool thumbOpenAtMin; |
| final TextTheme textTheme; |
| final double textScaleFactor; |
| final ValueChanged<double> onChanged; |
| final TickerProvider vsync; |
| final AnimationController reactionController; |
| |
| @override |
| _RenderSlider createRenderObject(BuildContext context) { |
| return new _RenderSlider( |
| value: value, |
| divisions: divisions, |
| label: label, |
| activeColor: activeColor, |
| inactiveColor: inactiveColor, |
| thumbOpenAtMin: thumbOpenAtMin, |
| textTheme: textTheme, |
| textScaleFactor: textScaleFactor, |
| onChanged: onChanged, |
| vsync: vsync, |
| reactionController: reactionController, |
| textDirection: Directionality.of(context), |
| ); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderSlider renderObject) { |
| renderObject |
| ..value = value |
| ..divisions = divisions |
| ..label = label |
| ..activeColor = activeColor |
| ..inactiveColor = inactiveColor |
| ..thumbOpenAtMin = thumbOpenAtMin |
| ..textTheme = textTheme |
| ..textScaleFactor = textScaleFactor |
| ..onChanged = onChanged |
| ..textDirection = Directionality.of(context); |
| // Ticker provider cannot change since there's a 1:1 relationship between |
| // the _SliderRenderObjectWidget object and the _SliderState object. |
| } |
| } |
| |
| const double _kThumbRadius = 6.0; |
| const double _kActiveThumbRadius = 9.0; |
| const double _kDisabledThumbRadius = 4.0; |
| const double _kReactionRadius = 16.0; |
| const double _kPreferredTrackWidth = 144.0; |
| const double _kMinimumTrackWidth = _kActiveThumbRadius; // biggest of the thumb radii |
| const double _kPreferredTotalWidth = _kPreferredTrackWidth + 2 * _kReactionRadius; |
| const double _kMinimumTotalWidth = _kMinimumTrackWidth + 2 * _kReactionRadius; |
| |
| final Color _kActiveTrackColor = Colors.grey; |
| final Tween<double> _kReactionRadiusTween = new Tween<double>(begin: _kThumbRadius, end: _kReactionRadius); |
| final Tween<double> _kThumbRadiusTween = new Tween<double>(begin: _kThumbRadius, end: _kActiveThumbRadius); |
| final ColorTween _kTickColorTween = new ColorTween(begin: Colors.transparent, end: Colors.black54); |
| final Duration _kDiscreteTransitionDuration = const Duration(milliseconds: 500); |
| |
| const double _kLabelBalloonRadius = 14.0; |
| final Tween<double> _kLabelBalloonCenterTween = new Tween<double>(begin: 0.0, end: -_kLabelBalloonRadius * 2.0); |
| final Tween<double> _kLabelBalloonRadiusTween = new Tween<double>(begin: _kThumbRadius, end: _kLabelBalloonRadius); |
| final Tween<double> _kLabelBalloonTipTween = new Tween<double>(begin: 0.0, end: -8.0); |
| final double _kLabelBalloonTipAttachmentRatio = math.sin(math.PI / 4.0); |
| |
| const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider. |
| |
| double _getAdditionalHeightForLabel(String label) { |
| return label == null ? 0.0 : _kLabelBalloonRadius * 2.0; |
| } |
| |
| double _getPreferredTotalHeight(String label) { |
| return 2 * _kReactionRadius + _getAdditionalHeightForLabel(label); |
| } |
| |
| class _RenderSlider extends RenderBox { |
| _RenderSlider({ |
| @required double value, |
| int divisions, |
| String label, |
| Color activeColor, |
| Color inactiveColor, |
| bool thumbOpenAtMin, |
| TextTheme textTheme, |
| double textScaleFactor, |
| ValueChanged<double> onChanged, |
| TickerProvider vsync, |
| @required TextDirection textDirection, |
| @required AnimationController reactionController, |
| }) : assert(value != null && value >= 0.0 && value <= 1.0), |
| assert(textDirection != null), |
| _label = label, |
| _value = value, |
| _divisions = divisions, |
| _activeColor = activeColor, |
| _inactiveColor = inactiveColor, |
| _thumbOpenAtMin = thumbOpenAtMin, |
| _textTheme = textTheme, |
| _textScaleFactor = textScaleFactor, |
| _onChanged = onChanged, |
| _textDirection = textDirection { |
| _updateLabelPainter(); |
| final GestureArenaTeam team = new GestureArenaTeam(); |
| _drag = new HorizontalDragGestureRecognizer() |
| ..team = team |
| ..onStart = _handleDragStart |
| ..onUpdate = _handleDragUpdate |
| ..onEnd = _handleDragEnd; |
| _tap = new TapGestureRecognizer() |
| ..team = team |
| ..onTapUp = _handleTapUp; |
| _reactionController = reactionController; |
| _reaction = new CurvedAnimation( |
| parent: _reactionController, |
| curve: Curves.fastOutSlowIn |
| )..addListener(markNeedsPaint); |
| _position = new AnimationController( |
| value: value, |
| duration: _kDiscreteTransitionDuration, |
| vsync: vsync, |
| )..addListener(markNeedsPaint); |
| } |
| |
| double get value => _value; |
| double _value; |
| set value(double newValue) { |
| assert(newValue != null && newValue >= 0.0 && newValue <= 1.0); |
| if (newValue == _value) |
| return; |
| _value = newValue; |
| if (divisions != null) |
| _position.animateTo(newValue, curve: Curves.fastOutSlowIn); |
| else |
| _position.value = newValue; |
| } |
| |
| int get divisions => _divisions; |
| int _divisions; |
| set divisions(int value) { |
| if (value == _divisions) |
| return; |
| _divisions = value; |
| markNeedsPaint(); |
| } |
| |
| String get label => _label; |
| String _label; |
| set label(String value) { |
| if (value == _label) |
| return; |
| _label = value; |
| _updateLabelPainter(); |
| } |
| |
| Color get activeColor => _activeColor; |
| Color _activeColor; |
| set activeColor(Color value) { |
| if (value == _activeColor) |
| return; |
| _activeColor = value; |
| markNeedsPaint(); |
| } |
| |
| Color get inactiveColor => _inactiveColor; |
| Color _inactiveColor; |
| set inactiveColor(Color value) { |
| if (value == _inactiveColor) |
| return; |
| _inactiveColor = value; |
| markNeedsPaint(); |
| } |
| |
| bool get thumbOpenAtMin => _thumbOpenAtMin; |
| bool _thumbOpenAtMin; |
| set thumbOpenAtMin(bool value) { |
| if (value == _thumbOpenAtMin) |
| return; |
| _thumbOpenAtMin = value; |
| markNeedsPaint(); |
| } |
| |
| TextTheme get textTheme => _textTheme; |
| TextTheme _textTheme; |
| set textTheme(TextTheme value) { |
| if (value == _textTheme) |
| return; |
| _textTheme = value; |
| markNeedsPaint(); |
| } |
| |
| double get textScaleFactor => _textScaleFactor; |
| double _textScaleFactor; |
| set textScaleFactor(double value) { |
| if (value == _textScaleFactor) |
| return; |
| _textScaleFactor = value; |
| _updateLabelPainter(); |
| markNeedsPaint(); |
| } |
| |
| ValueChanged<double> get onChanged => _onChanged; |
| ValueChanged<double> _onChanged; |
| set onChanged(ValueChanged<double> value) { |
| if (value == _onChanged) |
| return; |
| final bool wasInteractive = isInteractive; |
| _onChanged = value; |
| if (wasInteractive != isInteractive) { |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| } |
| |
| TextDirection get textDirection => _textDirection; |
| TextDirection _textDirection; |
| set textDirection(TextDirection value) { |
| assert(value != null); |
| if (value == _textDirection) |
| return; |
| _textDirection = value; |
| _updateLabelPainter(); |
| } |
| |
| void _updateLabelPainter() { |
| if (label != null) { |
| _labelPainter |
| ..text = new TextSpan( |
| style: _textTheme.body1.copyWith(fontSize: 10.0 * _textScaleFactor), |
| text: label, |
| ) |
| ..textDirection = textDirection |
| ..layout(); |
| } else { |
| _labelPainter.text = null; |
| } |
| // Changing the textDirection can result in the layout changing, because the |
| // bidi algorithm might line up the glyphs differently which can result in |
| // different ligatures, different shapes, etc. So we always markNeedsLayout. |
| markNeedsLayout(); |
| } |
| |
| double get _trackLength => size.width - 2.0 * _kReactionRadius; |
| |
| Animation<double> _reaction; |
| AnimationController _reactionController; |
| |
| AnimationController _position; |
| final TextPainter _labelPainter = new TextPainter(); |
| |
| HorizontalDragGestureRecognizer _drag; |
| TapGestureRecognizer _tap; |
| bool _active = false; |
| double _currentDragValue = 0.0; |
| |
| bool get isInteractive => onChanged != null; |
| |
| double _getValueFromVisualPosition(double visualPosition) { |
| switch (textDirection) { |
| case TextDirection.rtl: |
| return 1.0 - visualPosition; |
| case TextDirection.ltr: |
| return visualPosition; |
| } |
| return null; |
| } |
| |
| double _getValueFromGlobalPosition(Offset globalPosition) { |
| final double visualPosition = (globalToLocal(globalPosition).dx - _kReactionRadius) / _trackLength; |
| return _getValueFromVisualPosition(visualPosition); |
| } |
| |
| double _discretize(double value) { |
| double result = value.clamp(0.0, 1.0); |
| if (divisions != null) |
| result = (result * divisions).round() / divisions; |
| return result; |
| } |
| |
| void _handleDragStart(DragStartDetails details) { |
| if (isInteractive) { |
| _active = true; |
| _currentDragValue = _getValueFromGlobalPosition(details.globalPosition); |
| onChanged(_discretize(_currentDragValue)); |
| _reactionController.forward(); |
| } |
| } |
| |
| void _handleDragUpdate(DragUpdateDetails details) { |
| if (isInteractive) { |
| final double valueDelta = details.primaryDelta / _trackLength; |
| switch (textDirection) { |
| case TextDirection.rtl: |
| _currentDragValue -= valueDelta; |
| break; |
| case TextDirection.ltr: |
| _currentDragValue += valueDelta; |
| break; |
| } |
| onChanged(_discretize(_currentDragValue)); |
| } |
| } |
| |
| void _handleDragEnd(DragEndDetails details) { |
| if (_active) { |
| _active = false; |
| _currentDragValue = 0.0; |
| _reactionController.reverse(); |
| } |
| } |
| |
| void _handleTapUp(TapUpDetails details) { |
| if (isInteractive && !_active) |
| onChanged(_discretize(_getValueFromGlobalPosition(details.globalPosition))); |
| } |
| |
| @override |
| bool hitTestSelf(Offset position) => true; |
| |
| @override |
| void handleEvent(PointerEvent event, BoxHitTestEntry entry) { |
| assert(debugHandleEvent(event, entry)); |
| if (event is PointerDownEvent && isInteractive) { |
| // We need to add the drag first so that it has priority. |
| _drag.addPointer(event); |
| _tap.addPointer(event); |
| } |
| } |
| |
| @override |
| double computeMinIntrinsicWidth(double height) { |
| return _kMinimumTotalWidth; |
| } |
| |
| @override |
| double computeMaxIntrinsicWidth(double height) { |
| // This doesn't quite match the definition of computeMaxIntrinsicWidth, |
| // but it seems within the spirit... |
| return _kPreferredTotalWidth; |
| } |
| |
| @override |
| double computeMinIntrinsicHeight(double width) { |
| return _getPreferredTotalHeight(label); |
| } |
| |
| @override |
| double computeMaxIntrinsicHeight(double width) { |
| return _getPreferredTotalHeight(label); |
| } |
| |
| @override |
| bool get sizedByParent => true; |
| |
| @override |
| void performResize() { |
| size = new Size( |
| constraints.hasBoundedWidth ? constraints.maxWidth : _kPreferredTotalWidth, |
| constraints.hasBoundedHeight ? constraints.maxHeight : _getPreferredTotalHeight(label), |
| ); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| final Canvas canvas = context.canvas; |
| |
| final double trackLength = size.width - 2 * _kReactionRadius; |
| final bool enabled = isInteractive; |
| final double value = _position.value; |
| final bool thumbAtMin = value == 0.0; |
| |
| final Paint primaryPaint = new Paint()..color = enabled ? _activeColor : _inactiveColor; |
| final Paint trackPaint = new Paint()..color = _inactiveColor; |
| |
| double visualPosition; |
| Paint leftPaint; |
| Paint rightPaint; |
| switch (textDirection) { |
| case TextDirection.rtl: |
| visualPosition = 1.0 - value; |
| leftPaint = trackPaint; |
| rightPaint = primaryPaint; |
| break; |
| case TextDirection.ltr: |
| visualPosition = value; |
| leftPaint = primaryPaint; |
| rightPaint = trackPaint; |
| break; |
| } |
| |
| final double additionalHeightForLabel = _getAdditionalHeightForLabel(label); |
| final double trackCenter = offset.dy + (size.height - additionalHeightForLabel) / 2.0 + additionalHeightForLabel; |
| final double trackLeft = offset.dx + _kReactionRadius; |
| final double trackTop = trackCenter - 1.0; |
| final double trackBottom = trackCenter + 1.0; |
| final double trackRight = trackLeft + trackLength; |
| final double trackActive = trackLeft + trackLength * visualPosition; |
| |
| final Offset thumbCenter = new Offset(trackActive, trackCenter); |
| final double thumbRadius = enabled ? _kThumbRadiusTween.evaluate(_reaction) : _kDisabledThumbRadius; |
| |
| if (enabled) { |
| if (visualPosition > 0.0) |
| canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive, trackBottom), leftPaint); |
| if (visualPosition < 1.0) { |
| final bool hasBalloon = _reaction.status != AnimationStatus.dismissed && label != null; |
| final double trackActiveDelta = hasBalloon ? 0.0 : thumbRadius - 1.0; |
| canvas.drawRect(new Rect.fromLTRB(trackActive + trackActiveDelta, trackTop, trackRight, trackBottom), rightPaint); |
| } |
| } else { |
| if (visualPosition > 0.0) |
| canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive - _kDisabledThumbRadius - 2, trackBottom), trackPaint); |
| if (visualPosition < 1.0) |
| canvas.drawRect(new Rect.fromLTRB(trackActive + _kDisabledThumbRadius + 2, trackTop, trackRight, trackBottom), trackPaint); |
| } |
| |
| if (_reaction.status != AnimationStatus.dismissed) { |
| final int divisions = this.divisions; |
| if (divisions != null) { |
| const double tickWidth = 2.0; |
| final double dx = (trackLength - tickWidth) / divisions; |
| // If the ticks would be too dense, don't bother painting them. |
| if (dx >= 3 * tickWidth) { |
| final Paint tickPaint = new Paint()..color = _kTickColorTween.evaluate(_reaction); |
| for (int i = 0; i <= divisions; i += 1) { |
| final double left = trackLeft + i * dx; |
| canvas.drawRect(new Rect.fromLTRB(left, trackTop, left + tickWidth, trackBottom), tickPaint); |
| } |
| } |
| } |
| |
| if (label != null) { |
| final Offset center = new Offset( |
| trackActive, |
| _kLabelBalloonCenterTween.evaluate(_reaction) * textScaleFactor + trackCenter |
| ); |
| final double radius = _kLabelBalloonRadiusTween.evaluate(_reaction) * textScaleFactor; |
| final Offset tip = new Offset( |
| trackActive, |
| _kLabelBalloonTipTween.evaluate(_reaction) * textScaleFactor + trackCenter |
| ); |
| final double tipAttachment = _kLabelBalloonTipAttachmentRatio * radius; |
| |
| canvas.drawCircle(center, radius, primaryPaint); |
| final Path path = new Path() |
| ..moveTo(tip.dx, tip.dy) |
| ..lineTo(center.dx - tipAttachment, center.dy + tipAttachment) |
| ..lineTo(center.dx + tipAttachment, center.dy + tipAttachment) |
| ..close(); |
| canvas.drawPath(path, primaryPaint); |
| final Offset labelOffset = new Offset( |
| center.dx - _labelPainter.width / 2.0, |
| center.dy - _labelPainter.height / 2.0 |
| ); |
| _labelPainter.paint(canvas, labelOffset); |
| return; |
| } else { |
| final Color reactionBaseColor = thumbAtMin ? _kActiveTrackColor : _activeColor; |
| final Paint reactionPaint = new Paint()..color = reactionBaseColor.withAlpha(kRadialReactionAlpha); |
| canvas.drawCircle(thumbCenter, _kReactionRadiusTween.evaluate(_reaction), reactionPaint); |
| } |
| } |
| |
| Paint thumbPaint = primaryPaint; |
| double thumbRadiusDelta = 0.0; |
| if (thumbAtMin && thumbOpenAtMin) { |
| thumbPaint = trackPaint; |
| // This is destructive to trackPaint. |
| thumbPaint |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 2.0; |
| thumbRadiusDelta = -1.0; |
| } |
| canvas.drawCircle(thumbCenter, thumbRadius + thumbRadiusDelta, thumbPaint); |
| } |
| |
| @override |
| void describeSemanticsConfiguration(SemanticsConfiguration config) { |
| super.describeSemanticsConfiguration(config); |
| |
| config.isSemanticBoundary = isInteractive; |
| if (isInteractive) { |
| config.addAction(SemanticsAction.increase, _increaseAction); |
| config.addAction(SemanticsAction.decrease, _decreaseAction); |
| } |
| } |
| |
| double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _kAdjustmentUnit; |
| |
| void _increaseAction() { |
| if (isInteractive) |
| onChanged((value + _semanticActionUnit).clamp(0.0, 1.0)); |
| } |
| |
| void _decreaseAction() { |
| if (isInteractive) |
| onChanged((value - _semanticActionUnit).clamp(0.0, 1.0)); |
| } |
| } |