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 {
/// _dollarsRange = newValues;
/// });
/// },
/// semanticFormatterCallback: (RangeValues rangeValues) {
/// return '${rangeValues.start.round()} - ${rangeValues.end.round()} dollars';
/// semanticFormatterCallback: (double newValue) {
/// return '${newValue.round()} dollars';
/// }
/// )
/// ```
/// {@end-tool}
final RangeSemanticFormatterCallback semanticFormatterCallback;
final SemanticFormatterCallback semanticFormatterCallback;
// Touch width for the tap boundary of the slider thumbs.
static const double _minTouchTargetWidth = kMinInteractiveDimension;
......@@ -394,7 +394,7 @@ class RangeSlider extends StatefulWidget {
properties.add(StringProperty('labelEnd', labels?.end));
properties.add(ColorProperty('activeColor', activeColor));
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 {
final ValueChanged<RangeValues> onChanged;
final ValueChanged<RangeValues> onChangeStart;
final ValueChanged<RangeValues> onChangeEnd;
final RangeSemanticFormatterCallback semanticFormatterCallback;
final SemanticFormatterCallback semanticFormatterCallback;
final _RangeSliderState state;
@override
......@@ -756,7 +756,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
Size screenSize,
TargetPlatform platform,
ValueChanged<RangeValues> onChanged,
RangeSemanticFormatterCallback semanticFormatterCallback,
SemanticFormatterCallback semanticFormatterCallback,
this.onChangeStart,
this.onChangeEnd,
@required _RangeSliderState state,
......@@ -858,6 +858,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
bool get isDiscrete => divisions != null && divisions > 0;
double get _minThumbSeparationValue => isDiscrete ? 0 : sliderTheme.minThumbSeparation / _trackRect.width;
RangeValues get values => _values;
RangeValues _values;
set values(RangeValues newValues) {
......@@ -897,9 +899,9 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
markNeedsSemanticsUpdate();
}
RangeSemanticFormatterCallback _semanticFormatterCallback;
RangeSemanticFormatterCallback get semanticFormatterCallback => _semanticFormatterCallback;
set semanticFormatterCallback(RangeSemanticFormatterCallback value) {
SemanticFormatterCallback _semanticFormatterCallback;
SemanticFormatterCallback get semanticFormatterCallback => _semanticFormatterCallback;
set semanticFormatterCallback(SemanticFormatterCallback value) {
if (_semanticFormatterCallback == value)
return;
_semanticFormatterCallback = value;
......@@ -1189,11 +1191,10 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
}
final double currentDragValue = _discretize(dragValue);
final double minThumbSeparationValue = isDiscrete ? 0 : sliderTheme.minThumbSeparation / _trackRect.width;
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) {
_newValues = RangeValues(currentValues.start, math.max(currentDragValue, currentValues.start + minThumbSeparationValue));
_newValues = RangeValues(currentValues.start, math.max(currentDragValue, currentValues.start + _minThumbSeparationValue));
}
onChanged(_newValues);
}
......@@ -1513,64 +1514,149 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
);
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
/// Describe the semantics of the start thumb.
SemanticsNode _startSemanticsNode = SemanticsNode();
config.isSemanticBoundary = isEnabled;
if (isEnabled) {
/// Describe the semantics of the end thumb.
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.customSemanticsActions = <CustomSemanticsAction, VoidCallback>{
_decreaseStart: _decreaseStartAction,
_increaseStart: _increaseStartAction,
_decreaseEnd: _decreaseEndAction,
_increaseEnd: _increaseEndAction,
};
if (isEnabled) {
config.onIncrease = increaseAction;
config.onDecrease = decreaseAction;
}
config.label = label ?? '';
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 {
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');
final CustomSemanticsAction _increaseStart = const CustomSemanticsAction(label: 'Increase Min');
final CustomSemanticsAction _decreaseEnd = const CustomSemanticsAction(label: 'Decrease Max');
final CustomSemanticsAction _increaseEnd = const CustomSemanticsAction(label: 'Increase Max');
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = true;
}
double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _adjustmentUnit;
void _increaseStartAction() {
if (isEnabled) {
onChanged(RangeValues(_increaseValue(values.start), values.end));
onChanged(RangeValues(_increasedStartValue, values.end));
}
}
void _decreaseStartAction() {
if (isEnabled) {
onChanged(RangeValues(_decreaseValue(values.start), values.end));
onChanged(RangeValues(_decreasedStartValue, values.end));
}
}
void _increaseEndAction() {
if (isEnabled) {
onChanged(RangeValues(values.start, _increaseValue(values.end)));
onChanged(RangeValues(values.start, _increasedEndValue));
}
}
void _decreaseEndAction() {
if (isEnabled) {
onChanged(RangeValues(values.start, _decreaseValue(values.end)));
onChanged(RangeValues(values.start, _decreasedEndValue));
}
}
double _increaseValue(double value) {
return (value + _semanticActionUnit).clamp(0.0, 1.0) as double;
double get _increasedStartValue {
// 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) {
return (value - _semanticActionUnit).clamp(0.0, 1.0) as double;
double get _decreasedEndValue {
final double decreasedEndValue = values.end - _semanticActionUnit;
return decreasedEndValue >= values.start + _minThumbSeparationValue ? decreasedEndValue : values.end;
}
}
......
......@@ -28,13 +28,6 @@ import 'theme.dart';
// int _duelCommandment = 1;
// 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.
///
/// Since the value indicator is painted on the Overlay; this method paints the
......
......@@ -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:
///
/// * [Slider.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.
///
......
......@@ -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 {
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