Add per thumb Range Slider semantics (#61439)
This commit is contained in:
parent
6d303af97e
commit
629395f7ad
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/// Describe the semantics of the start thumb.
|
||||
SemanticsNode _startSemanticsNode = SemanticsNode();
|
||||
|
||||
/// 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;
|
||||
if (isEnabled) {
|
||||
config.onIncrease = increaseAction;
|
||||
config.onDecrease = decreaseAction;
|
||||
}
|
||||
config.label = label ?? '';
|
||||
if (semanticFormatterCallback != null) {
|
||||
config.value = semanticFormatterCallback(_state._lerp(value));
|
||||
config.increasedValue = semanticFormatterCallback(_state._lerp(increasedValue));
|
||||
config.decreasedValue = semanticFormatterCallback(_state._lerp(decreasedValue));
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
|
||||
config.isSemanticBoundary = isEnabled;
|
||||
if (isEnabled) {
|
||||
config.textDirection = textDirection;
|
||||
config.customSemanticsActions = <CustomSemanticsAction, VoidCallback>{
|
||||
_decreaseStart: _decreaseStartAction,
|
||||
_increaseStart: _increaseStartAction,
|
||||
_decreaseEnd: _decreaseEndAction,
|
||||
_increaseEnd: _increaseEndAction,
|
||||
};
|
||||
if (semanticFormatterCallback != null) {
|
||||
config.value = semanticFormatterCallback(_state._lerpRangeValues(values));
|
||||
} else {
|
||||
config.value = values.toString();
|
||||
}
|
||||
}
|
||||
config.isSemanticBoundary = true;
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
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 _decreaseValue(double value) {
|
||||
return (value - _semanticActionUnit).clamp(0.0, 1.0) as double;
|
||||
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 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();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user