Unverified Commit 629395f7 authored by Pierre-Louis's avatar Pierre-Louis Committed by GitHub

Add per thumb Range Slider semantics (#61439)

parent 6d303af9
...@@ -365,13 +365,13 @@ class RangeSlider extends StatefulWidget { ...@@ -365,13 +365,13 @@ class RangeSlider extends StatefulWidget {
/// _dollarsRange = newValues; /// _dollarsRange = newValues;
/// }); /// });
/// }, /// },
/// semanticFormatterCallback: (RangeValues rangeValues) { /// semanticFormatterCallback: (double newValue) {
/// return '${rangeValues.start.round()} - ${rangeValues.end.round()} dollars'; /// return '${newValue.round()} dollars';
/// } /// }
/// ) /// )
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
final RangeSemanticFormatterCallback semanticFormatterCallback; final SemanticFormatterCallback semanticFormatterCallback;
// Touch width for the tap boundary of the slider thumbs. // Touch width for the tap boundary of the slider thumbs.
static const double _minTouchTargetWidth = kMinInteractiveDimension; static const double _minTouchTargetWidth = kMinInteractiveDimension;
...@@ -394,7 +394,7 @@ class RangeSlider extends StatefulWidget { ...@@ -394,7 +394,7 @@ class RangeSlider extends StatefulWidget {
properties.add(StringProperty('labelEnd', labels?.end)); properties.add(StringProperty('labelEnd', labels?.end));
properties.add(ColorProperty('activeColor', activeColor)); properties.add(ColorProperty('activeColor', activeColor));
properties.add(ColorProperty('inactiveColor', inactiveColor)); properties.add(ColorProperty('inactiveColor', inactiveColor));
properties.add(ObjectFlagProperty<ValueChanged<RangeValues>>.has('semanticFormatterCallback', semanticFormatterCallback)); properties.add(ObjectFlagProperty<ValueChanged<double>>.has('semanticFormatterCallback', semanticFormatterCallback));
} }
} }
...@@ -703,7 +703,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -703,7 +703,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
final ValueChanged<RangeValues> onChanged; final ValueChanged<RangeValues> onChanged;
final ValueChanged<RangeValues> onChangeStart; final ValueChanged<RangeValues> onChangeStart;
final ValueChanged<RangeValues> onChangeEnd; final ValueChanged<RangeValues> onChangeEnd;
final RangeSemanticFormatterCallback semanticFormatterCallback; final SemanticFormatterCallback semanticFormatterCallback;
final _RangeSliderState state; final _RangeSliderState state;
@override @override
...@@ -756,7 +756,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -756,7 +756,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
Size screenSize, Size screenSize,
TargetPlatform platform, TargetPlatform platform,
ValueChanged<RangeValues> onChanged, ValueChanged<RangeValues> onChanged,
RangeSemanticFormatterCallback semanticFormatterCallback, SemanticFormatterCallback semanticFormatterCallback,
this.onChangeStart, this.onChangeStart,
this.onChangeEnd, this.onChangeEnd,
@required _RangeSliderState state, @required _RangeSliderState state,
...@@ -858,6 +858,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -858,6 +858,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
bool get isDiscrete => divisions != null && divisions > 0; bool get isDiscrete => divisions != null && divisions > 0;
double get _minThumbSeparationValue => isDiscrete ? 0 : sliderTheme.minThumbSeparation / _trackRect.width;
RangeValues get values => _values; RangeValues get values => _values;
RangeValues _values; RangeValues _values;
set values(RangeValues newValues) { set values(RangeValues newValues) {
...@@ -897,9 +899,9 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -897,9 +899,9 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
RangeSemanticFormatterCallback _semanticFormatterCallback; SemanticFormatterCallback _semanticFormatterCallback;
RangeSemanticFormatterCallback get semanticFormatterCallback => _semanticFormatterCallback; SemanticFormatterCallback get semanticFormatterCallback => _semanticFormatterCallback;
set semanticFormatterCallback(RangeSemanticFormatterCallback value) { set semanticFormatterCallback(SemanticFormatterCallback value) {
if (_semanticFormatterCallback == value) if (_semanticFormatterCallback == value)
return; return;
_semanticFormatterCallback = value; _semanticFormatterCallback = value;
...@@ -1189,11 +1191,10 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1189,11 +1191,10 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
} }
final double currentDragValue = _discretize(dragValue); final double currentDragValue = _discretize(dragValue);
final double minThumbSeparationValue = isDiscrete ? 0 : sliderTheme.minThumbSeparation / _trackRect.width;
if (_lastThumbSelection == Thumb.start) { if (_lastThumbSelection == Thumb.start) {
_newValues = RangeValues(math.min(currentDragValue, currentValues.end - minThumbSeparationValue), currentValues.end); _newValues = RangeValues(math.min(currentDragValue, currentValues.end - _minThumbSeparationValue), currentValues.end);
} else if (_lastThumbSelection == Thumb.end) { } else if (_lastThumbSelection == Thumb.end) {
_newValues = RangeValues(currentValues.start, math.max(currentDragValue, currentValues.start + minThumbSeparationValue)); _newValues = RangeValues(currentValues.start, math.max(currentDragValue, currentValues.start + _minThumbSeparationValue));
} }
onChanged(_newValues); onChanged(_newValues);
} }
...@@ -1513,64 +1514,149 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1513,64 +1514,149 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
); );
} }
@override /// Describe the semantics of the start thumb.
void describeSemanticsConfiguration(SemanticsConfiguration config) { SemanticsNode _startSemanticsNode = SemanticsNode();
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = isEnabled; /// Describe the semantics of the end thumb.
if (isEnabled) { SemanticsNode _endSemanticsNode = SemanticsNode();
// Create the semantics configuration for a single value.
SemanticsConfiguration _createSemanticsConfiguration(
double value,
double increasedValue,
double decreasedValue,
String label,
VoidCallback increaseAction,
VoidCallback decreaseAction,
) {
final SemanticsConfiguration config = SemanticsConfiguration();
config.isEnabled = isEnabled;
config.textDirection = textDirection; config.textDirection = textDirection;
config.customSemanticsActions = <CustomSemanticsAction, VoidCallback>{ if (isEnabled) {
_decreaseStart: _decreaseStartAction, config.onIncrease = increaseAction;
_increaseStart: _increaseStartAction, config.onDecrease = decreaseAction;
_decreaseEnd: _decreaseEndAction, }
_increaseEnd: _increaseEndAction, config.label = label ?? '';
};
if (semanticFormatterCallback != null) { if (semanticFormatterCallback != null) {
config.value = semanticFormatterCallback(_state._lerpRangeValues(values)); config.value = semanticFormatterCallback(_state._lerp(value));
config.increasedValue = semanticFormatterCallback(_state._lerp(increasedValue));
config.decreasedValue = semanticFormatterCallback(_state._lerp(decreasedValue));
} else { } else {
config.value = values.toString(); config.value = '${(value * 100).round()}%';
config.increasedValue = '${(increasedValue * 100).round()}%';
config.decreasedValue = '${(decreasedValue * 100).round()}%';
}
return config;
}
@override
void assembleSemanticsNode(
SemanticsNode node,
SemanticsConfiguration config,
Iterable<SemanticsNode> children,
) {
assert(children.isEmpty);
final SemanticsConfiguration startSemanticsConfiguration = _createSemanticsConfiguration(
values.start,
_increasedStartValue,
_decreasedStartValue,
labels?.start,
_increaseStartAction,
_decreaseStartAction,
);
final SemanticsConfiguration endSemanticsConfiguration = _createSemanticsConfiguration(
values.end,
_increasedEndValue,
_decreasedEndValue,
labels?.end,
_increaseEndAction,
_decreaseEndAction,
);
// Split the semantics node area between the start and end nodes.
final Rect leftRect = Rect.fromPoints(node.rect.topLeft, node.rect.bottomCenter);
final Rect rightRect = Rect.fromPoints(node.rect.topCenter, node.rect.bottomRight);
switch (textDirection) {
case TextDirection.ltr:
_startSemanticsNode.rect = leftRect;
_endSemanticsNode.rect = rightRect;
break;
case TextDirection.rtl:
_startSemanticsNode.rect = rightRect;
_endSemanticsNode.rect = leftRect;
break;
} }
_startSemanticsNode.updateWith(config: startSemanticsConfiguration);
_endSemanticsNode.updateWith(config: endSemanticsConfiguration);
final List<SemanticsNode> finalChildren = <SemanticsNode>[
_startSemanticsNode,
_endSemanticsNode,
];
node.updateWith(config: config, childrenInInversePaintOrder: finalChildren);
} }
@override
void clearSemantics() {
super.clearSemantics();
_startSemanticsNode = null;
_endSemanticsNode = null;
} }
final CustomSemanticsAction _decreaseStart = const CustomSemanticsAction(label: 'Decrease Min'); @override
final CustomSemanticsAction _increaseStart = const CustomSemanticsAction(label: 'Increase Min'); void describeSemanticsConfiguration(SemanticsConfiguration config) {
final CustomSemanticsAction _decreaseEnd = const CustomSemanticsAction(label: 'Decrease Max'); super.describeSemanticsConfiguration(config);
final CustomSemanticsAction _increaseEnd = const CustomSemanticsAction(label: 'Increase Max'); config.isSemanticBoundary = true;
}
double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _adjustmentUnit; double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _adjustmentUnit;
void _increaseStartAction() { void _increaseStartAction() {
if (isEnabled) { if (isEnabled) {
onChanged(RangeValues(_increaseValue(values.start), values.end)); onChanged(RangeValues(_increasedStartValue, values.end));
} }
} }
void _decreaseStartAction() { void _decreaseStartAction() {
if (isEnabled) { if (isEnabled) {
onChanged(RangeValues(_decreaseValue(values.start), values.end)); onChanged(RangeValues(_decreasedStartValue, values.end));
} }
} }
void _increaseEndAction() { void _increaseEndAction() {
if (isEnabled) { if (isEnabled) {
onChanged(RangeValues(values.start, _increaseValue(values.end))); onChanged(RangeValues(values.start, _increasedEndValue));
} }
} }
void _decreaseEndAction() { void _decreaseEndAction() {
if (isEnabled) { if (isEnabled) {
onChanged(RangeValues(values.start, _decreaseValue(values.end))); onChanged(RangeValues(values.start, _decreasedEndValue));
} }
} }
double _increaseValue(double value) { double get _increasedStartValue {
return (value + _semanticActionUnit).clamp(0.0, 1.0) as double; // Due to floating-point operations, this value can actually be greater than
// expected (e.g. 0.4 + 0.2 = 0.600000000001), so we limit to 2 decimal points.
final double increasedStartValue = double.parse((values.start + _semanticActionUnit).toStringAsFixed(2));
return increasedStartValue <= values.end - _minThumbSeparationValue ? increasedStartValue : values.start;
}
double get _decreasedStartValue {
return (values.start - _semanticActionUnit).clamp(0.0, 1.0) as double;
}
double get _increasedEndValue {
return (values.end + _semanticActionUnit).clamp(0.0, 1.0) as double;
} }
double _decreaseValue(double value) { double get _decreasedEndValue {
return (value - _semanticActionUnit).clamp(0.0, 1.0) as double; final double decreasedEndValue = values.end - _semanticActionUnit;
return decreasedEndValue >= values.start + _minThumbSeparationValue ? decreasedEndValue : values.end;
} }
} }
......
...@@ -28,13 +28,6 @@ import 'theme.dart'; ...@@ -28,13 +28,6 @@ import 'theme.dart';
// int _duelCommandment = 1; // int _duelCommandment = 1;
// void setState(VoidCallback fn) { } // void setState(VoidCallback fn) { }
/// A callback that formats a numeric value from a [Slider] widget.
///
/// See also:
///
/// * [Slider.semanticFormatterCallback], which shows an example use case.
typedef SemanticFormatterCallback = String Function(double value);
/// [Slider] uses this callback to paint the value indicator on the overlay. /// [Slider] uses this callback to paint the value indicator on the overlay.
/// ///
/// Since the value indicator is painted on the Overlay; this method paints the /// Since the value indicator is painted on the Overlay; this method paints the
......
...@@ -3307,12 +3307,13 @@ class _PaddleSliderValueIndicatorPathPainter { ...@@ -3307,12 +3307,13 @@ class _PaddleSliderValueIndicatorPathPainter {
} }
} }
/// A callback that formats the numeric values from a [RangeSlider] widget. /// A callback that formats a numeric value from a [Slider] or [RangerSlider] widget.
/// ///
/// See also: /// See also:
/// ///
/// * [Slider.semanticFormatterCallback], which shows an example use case.
/// * [RangeSlider.semanticFormatterCallback], which shows an example use case. /// * [RangeSlider.semanticFormatterCallback], which shows an example use case.
typedef RangeSemanticFormatterCallback = String Function(RangeValues values); typedef SemanticFormatterCallback = String Function(double value);
/// Decides which thumbs (if any) should be selected. /// Decides which thumbs (if any) should be selected.
/// ///
......
...@@ -1742,6 +1742,63 @@ void main() { ...@@ -1742,6 +1742,63 @@ void main() {
); );
}); });
testWidgets('Range Slider Semantics', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Theme(
data: ThemeData.light(),
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: RangeSlider(
values: const RangeValues(10.0, 12.0),
min: 0.0,
max: 100.0,
onChanged: (RangeValues v) { },
),
),
),
),
),
)
);
await tester.pumpAndSettle();
expect(
tester.getSemantics(find.byType(RangeSlider)),
matchesSemantics(
scopesRoute: true,
children:<Matcher>[
matchesSemantics(
children: <Matcher>[
matchesSemantics(
isEnabled: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '10%',
decreasedValue: '5%',
),
matchesSemantics(
isEnabled: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '12%',
increasedValue: '17%',
decreasedValue: '12%',
),
],
),
],
),
);
});
testWidgets('Range Slider implements debugFillProperties', (WidgetTester tester) async { testWidgets('Range Slider implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment