Unverified Commit aae50f54 authored by Anthony's avatar Anthony Committed by GitHub

[Material] Text scale and wide label fixes for Slider and Range Slider value...

[Material] Text scale and wide label fixes for Slider and Range Slider value indicator shape (#35496)

Various bug fixes for Slider and Range Slider value indicator around accessibility and wide labels.
parent a76e39f9
......@@ -1241,8 +1241,9 @@ class _RenderRangeSlider extends RenderBox {
final TextPainter topLabelPainter = isLastThumbStart ? _startLabelPainter : _endLabelPainter;
final double bottomValue = isLastThumbStart ? endValue : startValue;
final double topValue = isLastThumbStart ? startValue : endValue;
final bool shouldPaintValueIndicators = isEnabled && labels != null && !_valueIndicatorAnimation.isDismissed && showValueIndicator;
if (isEnabled && labels != null && !_valueIndicatorAnimation.isDismissed && showValueIndicator) {
if (shouldPaintValueIndicators) {
_sliderTheme.rangeValueIndicatorShape.paint(
context,
bottomThumbCenter,
......@@ -1257,13 +1258,54 @@ class _RenderRangeSlider extends RenderBox {
thumb: bottomThumb,
value: bottomValue,
);
}
_sliderTheme.rangeThumbShape.paint(
context,
bottomThumbCenter,
activationAnimation: _valueIndicatorAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
isOnTop: false,
textDirection: textDirection,
sliderTheme: _sliderTheme,
thumb: bottomThumb,
);
if (shouldPaintValueIndicators) {
final double startOffset = sliderTheme.rangeValueIndicatorShape.getHorizontalShift(
parentBox: this,
center: startThumbCenter,
labelPainter: _startLabelPainter,
activationAnimation: _valueIndicatorAnimation,
);
final double endOffset = sliderTheme.rangeValueIndicatorShape.getHorizontalShift(
parentBox: this,
center: endThumbCenter,
labelPainter: _endLabelPainter,
activationAnimation: _valueIndicatorAnimation,
);
final double startHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(isEnabled, isDiscrete, labelPainter: _startLabelPainter).width / 2;
final double endHalfWidth = sliderTheme.rangeValueIndicatorShape.getPreferredSize(isEnabled, isDiscrete, labelPainter: _endLabelPainter).width / 2;
double innerOverflow = startHalfWidth + endHalfWidth;
switch (textDirection) {
case TextDirection.ltr:
innerOverflow += startOffset;
innerOverflow -= endOffset;
break;
case TextDirection.rtl:
innerOverflow -= startOffset;
innerOverflow += endOffset;
break;
}
_sliderTheme.rangeValueIndicatorShape.paint(
context,
topThumbCenter,
activationAnimation: _valueIndicatorAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
isOnTop: thumbDelta < sliderTheme.rangeValueIndicatorShape.getPreferredSize(isEnabled, isDiscrete, labelPainter: topLabelPainter).width,
isOnTop: thumbDelta < innerOverflow,
labelPainter: topLabelPainter,
parentBox: this,
sliderTheme: _sliderTheme,
......@@ -1273,17 +1315,6 @@ class _RenderRangeSlider extends RenderBox {
);
}
_sliderTheme.rangeThumbShape.paint(
context,
bottomThumbCenter,
activationAnimation: _valueIndicatorAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
isOnTop: false,
textDirection: textDirection,
sliderTheme: _sliderTheme,
thumb: bottomThumb,
);
_sliderTheme.rangeThumbShape.paint(
context,
topThumbCenter,
......
......@@ -1236,6 +1236,19 @@ abstract class RangeSliderValueIndicatorShape {
/// width because it is derived from a formatted string.
Size getPreferredSize(bool isEnabled, bool isDiscrete, { TextPainter labelPainter });
/// Determines the best offset to keep this shape on the screen.
///
/// Override this method when the center of the value indicator should be
/// shifted from the vertical center of the thumb.
double getHorizontalShift({
RenderBox parentBox,
Offset center,
TextPainter labelPainter,
Animation<double> activationAnimation,
}) {
return 0;
}
/// Paints the value indicator shape based on the state passed to it.
///
/// {@macro flutter.material.rangeSlider.shape.context}
......@@ -2335,18 +2348,11 @@ class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
break;
}
final Paint strokePaint = Paint()
..color = sliderTheme.overlappingShapeStrokeColor
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
if (showValueIndicator && activationAnimation.value > 0) {
// If there is a value indicator, then it is already stroked and a gap
// needs to be left on the thumb where the value indicator intersects
// the thumb.
const double startAngle = -math.pi / 2 + 0.2;
const double sweepAngle = math.pi * 2 - 0.4;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, false, strokePaint);
} else {
if (!showValueIndicator || activationAnimation.value == 0) {
final Paint strokePaint = Paint()
..color = sliderTheme.overlappingShapeStrokeColor
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
canvas.drawCircle(center, radius, strokePaint);
}
}
......@@ -2499,6 +2505,21 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap
return _pathPainter.getPreferredSize(isEnabled, isDiscrete, labelPainter);
}
@override
double getHorizontalShift({
RenderBox parentBox,
Offset center,
TextPainter labelPainter,
Animation<double> activationAnimation,
}) {
return _pathPainter.getHorizontalShift(
parentBox: parentBox,
center: center,
labelPainter: labelPainter,
scale: activationAnimation.value,
);
}
@override
void paint(
PaintingContext context,
......@@ -2553,18 +2574,18 @@ class _PaddleSliderTrackShapePathPainter {
// was designed to contain. We scale it from here to fit other sizes.
static const double _labelTextDesignSize = 14.0;
// Radius of the bottom lobe of the value indicator.
static const double _bottomLobeRadius = 6.0;
// The starting angle for the bottom lobe. Picked to get the desired
// thickness for the neck.
static const double _bottomLobeStartAngle = -1.1 * math.pi / 4.0;
// The ending angle for the bottom lobe. Picked to get the desired
// thickness for the neck.
static const double _bottomLobeEndAngle = 1.1 * 5 * math.pi / 4.0;
// The padding on either side of the label.
static const double _bottomLobeRadius = 10.0;
static const double _labelPadding = 8.0;
static const double _distanceBetweenTopBottomCenters = 40.0;
static const double _middleNeckWidth = 2.0;
static const double _bottomNeckRadius = 4.5;
// The base of the triangle between the top lobe center and the centers of
// the two top neck arcs.
static const double _neckTriangleBase = _topNeckRadius + _middleNeckWidth / 2;
static const double _rightBottomNeckCenterX = _middleNeckWidth / 2 + _bottomNeckRadius;
static const double _rightBottomNeckAngleStart = math.pi;
static const Offset _topLobeCenter = Offset(0.0, -_distanceBetweenTopBottomCenters);
static const double _topNeckRadius = 14.0;
static const double _topNeckRadius = 13.0;
// The length of the hypotenuse of the triangle formed by the center
// of the left top lobe arc and the center of the top left neck arc.
// Used to calculate the position of the center of the arc.
......@@ -2580,17 +2601,14 @@ class _PaddleSliderTrackShapePathPainter {
// be checked in while set to "true".
static const bool _debuggingLabelLocation = false;
static Path _bottomLobePath; // Initialized by _generateBottomLobe
static Offset _bottomLobeEnd; // Initialized by _generateBottomLobe
Size getPreferredSize(
bool isEnabled,
bool isDiscrete,
TextPainter labelPainter,
) {
assert(labelPainter != null);
final double labelHalfWidth = labelPainter.width / 2.0;
return Size((labelHalfWidth + _topLobeRadius) * 2, _preferredHeight);
final double textScaleFactor = labelPainter.height / _labelTextDesignSize;
return Size(labelPainter.width + 2 * _labelPadding * textScaleFactor, _preferredHeight * textScaleFactor);
}
// Adds an arc to the path that has the attributes passed in. This is
......@@ -2600,72 +2618,25 @@ class _PaddleSliderTrackShapePathPainter {
path.arcTo(arcRect, startAngle, endAngle - startAngle, false);
}
// Generates the bottom lobe path, which is the same for all instances of
// the value indicator, so we reuse it for each one.
static void _generateBottomLobe() {
const double bottomNeckRadius = 4.5;
const double bottomNeckStartAngle = _bottomLobeEndAngle - math.pi;
const double bottomNeckEndAngle = 0.0;
final Path path = Path();
final Offset bottomKnobStart = Offset(
_bottomLobeRadius * math.cos(_bottomLobeStartAngle),
_bottomLobeRadius * math.sin(_bottomLobeStartAngle) - 2,
);
final Offset bottomNeckRightCenter = bottomKnobStart +
Offset(
bottomNeckRadius * math.cos(bottomNeckStartAngle),
-bottomNeckRadius * math.sin(bottomNeckStartAngle),
);
final Offset bottomNeckLeftCenter = Offset(
-bottomNeckRightCenter.dx,
bottomNeckRightCenter.dy,
);
final Offset bottomNeckStartRight = Offset(
bottomNeckRightCenter.dx - bottomNeckRadius,
bottomNeckRightCenter.dy,
);
path.moveTo(bottomNeckStartRight.dx, bottomNeckStartRight.dy);
_addArc(
path,
bottomNeckRightCenter,
bottomNeckRadius,
math.pi - bottomNeckEndAngle,
math.pi - bottomNeckStartAngle,
);
_addArc(
path,
Offset.zero,
_bottomLobeRadius,
_bottomLobeStartAngle,
_bottomLobeEndAngle,
);
_addArc(
path,
bottomNeckLeftCenter,
bottomNeckRadius,
bottomNeckStartAngle,
bottomNeckEndAngle,
);
_bottomLobeEnd = Offset(
-bottomNeckStartRight.dx,
bottomNeckStartRight.dy,
double getHorizontalShift({
RenderBox parentBox,
Offset center,
TextPainter labelPainter,
double scale,
}) {
final double textScaleFactor = labelPainter.height / _labelTextDesignSize;
final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
final double labelHalfWidth = labelPainter.width / 2.0;
final double halfWidthNeeded = math.max(
0.0,
inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding),
);
_bottomLobePath = path;
final double shift = _getIdealOffset(parentBox, halfWidthNeeded, textScaleFactor * scale, center);
return shift * textScaleFactor;
}
Offset _addBottomLobe(Path path) {
if (_bottomLobePath == null || _bottomLobeEnd == null) {
// Generate this lazily so as to not slow down app startup.
_generateBottomLobe();
}
path.extendWithPath(_bottomLobePath, Offset.zero);
return _bottomLobeEnd;
}
// Determines the "best" offset to keep the bubble on the screen. The calling
// code will bound that with the available movement in the paddle shape.
// Determines the "best" offset to keep the bubble within the slider. The
// calling code will bound that with the available movement in the paddle shape.
double _getIdealOffset(
RenderBox parentBox,
double halfWidthNeeded,
......@@ -2684,13 +2655,25 @@ class _PaddleSliderTrackShapePathPainter {
final Offset topLeft = (topLobeRect.topLeft * scale) + center;
final Offset bottomRight = (topLobeRect.bottomRight * scale) + center;
double shift = 0.0;
if (topLeft.dx < edgeMargin) {
shift = edgeMargin - topLeft.dx;
final double startGlobal = parentBox.localToGlobal(Offset.zero).dx;
if (topLeft.dx < startGlobal + edgeMargin) {
shift = startGlobal + edgeMargin - topLeft.dx;
}
if (bottomRight.dx > parentBox.size.width - edgeMargin) {
shift = parentBox.size.width - bottomRight.dx - edgeMargin;
final double endGlobal = parentBox.localToGlobal(Offset(parentBox.size.width, parentBox.size.height)).dx;
if (bottomRight.dx > endGlobal - edgeMargin) {
shift = endGlobal - edgeMargin - bottomRight.dx;
}
shift = scale == 0.0 ? 0.0 : shift / scale;
if (shift < 0.0) {
// Shifting to the left.
shift = math.max(shift, -halfWidthNeeded);
} else {
// Shifting to the right.
shift = math.min(shift, halfWidthNeeded);
}
return shift;
}
......@@ -2703,16 +2686,43 @@ class _PaddleSliderTrackShapePathPainter {
TextPainter labelPainter,
Color strokePaintColor,
) {
canvas.save();
canvas.translate(center.dx, center.dy);
// The entire value indicator should scale with the size of the label,
// to keep it large enough to encompass the label text.
final double textScaleFactor = labelPainter.height / _labelTextDesignSize;
final double overallScale = scale * textScaleFactor;
canvas.scale(overallScale, overallScale);
final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
final double labelHalfWidth = labelPainter.width / 2.0;
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.scale(overallScale, overallScale);
final double bottomNeckTriangleHypotenuse = _bottomNeckRadius + _bottomLobeRadius / overallScale;
final double rightBottomNeckCenterY = -math.sqrt(math.pow(bottomNeckTriangleHypotenuse, 2) - math.pow(_rightBottomNeckCenterX, 2));
final double rightBottomNeckAngleEnd = math.pi + math.atan(rightBottomNeckCenterY / _rightBottomNeckCenterX);
final Path path = Path()..moveTo(_middleNeckWidth / 2, rightBottomNeckCenterY);
_addArc(
path,
Offset(_rightBottomNeckCenterX, rightBottomNeckCenterY),
_bottomNeckRadius,
_rightBottomNeckAngleStart,
rightBottomNeckAngleEnd,
);
_addArc(
path,
Offset.zero,
_bottomLobeRadius / overallScale,
rightBottomNeckAngleEnd - math.pi,
2 * math.pi - rightBottomNeckAngleEnd,
);
_addArc(
path,
Offset(-_rightBottomNeckCenterX, rightBottomNeckCenterY),
_bottomNeckRadius,
math.pi - rightBottomNeckAngleEnd,
0,
);
// This is the needed extra width for the label. It is only positive when
// the label exceeds the minimum size contained by the round top lobe.
final double halfWidthNeeded = math.max(
......@@ -2720,40 +2730,25 @@ class _PaddleSliderTrackShapePathPainter {
inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding),
);
double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center);
double leftWidthNeeded;
double rightWidthNeeded;
if (shift < 0.0) {
// shifting to the left
shift = math.max(shift, -halfWidthNeeded);
} else {
// shifting to the right
shift = math.min(shift, halfWidthNeeded);
}
rightWidthNeeded = halfWidthNeeded + shift;
leftWidthNeeded = halfWidthNeeded - shift;
final Path path = Path();
final Offset bottomLobeEnd = _addBottomLobe(path);
final double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center);
final double leftWidthNeeded = halfWidthNeeded - shift;
final double rightWidthNeeded = halfWidthNeeded + shift;
// The base of the triangle between the top lobe center and the centers of
// the two top neck arcs.
final double neckTriangleBase = _topNeckRadius - bottomLobeEnd.dx;
// The parameter that describes how far along the transition from round to
// stretched we are.
final double leftAmount = math.max(0.0, math.min(1.0, leftWidthNeeded / neckTriangleBase));
final double rightAmount = math.max(0.0, math.min(1.0, rightWidthNeeded / neckTriangleBase));
final double leftAmount = math.max(0.0, math.min(1.0, leftWidthNeeded / _neckTriangleBase));
final double rightAmount = math.max(0.0, math.min(1.0, rightWidthNeeded / _neckTriangleBase));
// The angle between the top neck arc's center and the top lobe's center
// and vertical.
final double leftTheta = (1.0 - leftAmount) * _thirtyDegrees;
final double rightTheta = (1.0 - rightAmount) * _thirtyDegrees;
// The center of the top left neck arc.
final Offset neckLeftCenter = Offset(
-neckTriangleBase,
final Offset leftTopNeckCenter = Offset(
-_neckTriangleBase,
_topLobeCenter.dy + math.cos(leftTheta) * _neckTriangleHypotenuse,
);
final Offset neckRightCenter = Offset(
neckTriangleBase,
_neckTriangleBase,
_topLobeCenter.dy + math.cos(rightTheta) * _neckTriangleHypotenuse,
);
final double leftNeckArcAngle = _ninetyDegrees - leftTheta;
......@@ -2761,7 +2756,7 @@ class _PaddleSliderTrackShapePathPainter {
// The distance between the end of the bottom neck arc and the beginning of
// the top neck arc. We use this to shrink/expand it based on the scale
// factor of the value indicator.
final double neckStretchBaseline = math.max(0.0, bottomLobeEnd.dy - math.max(neckLeftCenter.dy, neckRightCenter.dy));
final double neckStretchBaseline = math.max(0.0, rightBottomNeckCenterY - math.max(leftTopNeckCenter.dy, neckRightCenter.dy));
final double t = math.pow(inverseTextScale, 3.0);
final double stretch = (neckStretchBaseline * t).clamp(0.0, 10.0 * neckStretchBaseline);
final Offset neckStretch = Offset(0.0, neckStretchBaseline - stretch);
......@@ -2786,7 +2781,7 @@ class _PaddleSliderTrackShapePathPainter {
_addArc(
path,
neckLeftCenter + neckStretch,
leftTopNeckCenter + neckStretch,
_topNeckRadius,
0.0,
-leftNeckArcAngle,
......
......@@ -1459,5 +1459,79 @@ void main() {
await gesture.up();
});
testWidgets('Range Slider top value indicator gets stroked when overlapping with large text scale', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
valueIndicatorColor: Color(0xff000001),
overlappingShapeStrokeColor: Color(0xff000002),
showValueIndicator: ShowValueIndicator.always,
)
);
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window).copyWith(textScaleFactor: 2.0),
child: Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)),
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
),
),
);
},
),
),
);
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the the thumbs towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
await tester.pumpAndSettle();
expect(values.start, closeTo(0.5, 0.03));
expect(values.end, closeTo(0.5, 0.03));
final TestGesture gesture = await tester.startGesture(middle);
await tester.pumpAndSettle();
expect(
sliderBox,
paints
..path(color: sliderTheme.valueIndicatorColor)
..path(color: sliderTheme.overlappingShapeStrokeColor)
..path(color: sliderTheme.valueIndicatorColor),
);
await gesture.up();
});
}
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