Skip to content

Commit ac06523

Browse files
authored
Add Material 3 support for Slider - Part 2 (#114624)
* Add Material 3 support for Slider - Part 2 * Kick tests * Update drawing order to fix html renderer bug * Update test
1 parent 0344407 commit ac06523

File tree

5 files changed

+525
-17
lines changed

5 files changed

+525
-17
lines changed

dev/tools/gen_defaults/lib/slider_template.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ class _${blockName}DefaultsM3 extends SliderThemeData {
7171
7272
return Colors.transparent;
7373
});
74+
75+
@override
76+
TextStyle? get valueIndicatorTextStyle => ${textStyle('$tokenGroup.label.label-text')}!.copyWith(
77+
color: ${componentColor('$tokenGroup.label.label-text')},
78+
);
79+
80+
@override
81+
SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
7482
}
7583
''';
7684

packages/flutter/lib/src/material/slider.dart

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
759759
const SliderTickMarkShape defaultTickMarkShape = RoundSliderTickMarkShape();
760760
const SliderComponentShape defaultOverlayShape = RoundSliderOverlayShape();
761761
const SliderComponentShape defaultThumbShape = RoundSliderThumbShape();
762-
const SliderComponentShape defaultValueIndicatorShape = RectangularSliderValueIndicatorShape();
762+
final SliderComponentShape defaultValueIndicatorShape = defaults.valueIndicatorShape!;
763763
const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
764764

765765
final Set<MaterialState> states = <MaterialState>{
@@ -810,9 +810,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
810810
overlayShape: sliderTheme.overlayShape ?? defaultOverlayShape,
811811
valueIndicatorShape: valueIndicatorShape,
812812
showValueIndicator: sliderTheme.showValueIndicator ?? defaultShowValueIndicator,
813-
valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.bodyLarge!.copyWith(
814-
color: theme.colorScheme.onPrimary,
815-
),
813+
valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? defaults.valueIndicatorTextStyle,
816814
);
817815
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
818816
?? sliderTheme.mouseCursor?.resolve(states)
@@ -851,6 +849,14 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
851849
break;
852850
}
853851

852+
final double textScaleFactor = theme.useMaterial3
853+
// TODO(tahatesser): This is an eye-balled value.
854+
// This needs to be updated when accessibility
855+
// guidelines are available on the material specs page
856+
// https://m3.material.io/components/sliders/accessibility.
857+
? math.min(MediaQuery.of(context).textScaleFactor, 1.3)
858+
: MediaQuery.of(context).textScaleFactor;
859+
854860
return Semantics(
855861
container: true,
856862
slider: true,
@@ -873,7 +879,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
873879
divisions: widget.divisions,
874880
label: widget.label,
875881
sliderTheme: sliderTheme,
876-
textScaleFactor: MediaQuery.of(context).textScaleFactor,
882+
textScaleFactor: textScaleFactor,
877883
screenSize: screenSize(),
878884
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
879885
onChangeStart: _handleDragStart,
@@ -1858,6 +1864,14 @@ class _SliderDefaultsM2 extends SliderThemeData {
18581864

18591865
@override
18601866
Color? get overlayColor => _colors.primary.withOpacity(0.12);
1867+
1868+
@override
1869+
TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.bodyLarge!.copyWith(
1870+
color: _colors.onPrimary,
1871+
);
1872+
1873+
@override
1874+
SliderComponentShape? get valueIndicatorShape => const RectangularSliderValueIndicatorShape();
18611875
}
18621876

18631877
// BEGIN GENERATED TOKEN PROPERTIES - Slider
@@ -1927,6 +1941,14 @@ class _SliderDefaultsM3 extends SliderThemeData {
19271941

19281942
return Colors.transparent;
19291943
});
1944+
1945+
@override
1946+
TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.labelMedium!.copyWith(
1947+
color: _colors.onPrimary,
1948+
);
1949+
1950+
@override
1951+
SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
19301952
}
19311953

19321954
// END GENERATED TOKEN PROPERTIES - Slider

packages/flutter/lib/src/material/slider_theme.dart

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3483,3 +3483,185 @@ void _debugDrawShadow(Canvas canvas, Path path, double elevation) {
34833483
);
34843484
}
34853485
}
3486+
3487+
/// The default shape of a Material 3 [Slider]'s value indicator.
3488+
///
3489+
/// See also:
3490+
///
3491+
/// * [Slider], which includes a value indicator defined by this shape.
3492+
/// * [SliderTheme], which can be used to configure the slider value indicator
3493+
/// of all sliders in a widget subtree.
3494+
class DropSliderValueIndicatorShape extends SliderComponentShape {
3495+
/// Create a slider value indicator that resembles a drop shape.
3496+
const DropSliderValueIndicatorShape();
3497+
3498+
static const _DropSliderValueIndicatorPathPainter _pathPainter = _DropSliderValueIndicatorPathPainter();
3499+
3500+
@override
3501+
Size getPreferredSize(
3502+
bool isEnabled,
3503+
bool isDiscrete, {
3504+
TextPainter? labelPainter,
3505+
double? textScaleFactor,
3506+
}) {
3507+
assert(labelPainter != null);
3508+
assert(textScaleFactor != null && textScaleFactor >= 0);
3509+
return _pathPainter.getPreferredSize(labelPainter!, textScaleFactor!);
3510+
}
3511+
3512+
@override
3513+
void paint(
3514+
PaintingContext context,
3515+
Offset center, {
3516+
required Animation<double> activationAnimation,
3517+
required Animation<double> enableAnimation,
3518+
required bool isDiscrete,
3519+
required TextPainter labelPainter,
3520+
required RenderBox parentBox,
3521+
required SliderThemeData sliderTheme,
3522+
required TextDirection textDirection,
3523+
required double value,
3524+
required double textScaleFactor,
3525+
required Size sizeWithOverflow,
3526+
}) {
3527+
final Canvas canvas = context.canvas;
3528+
final double scale = activationAnimation.value;
3529+
_pathPainter.paint(
3530+
parentBox: parentBox,
3531+
canvas: canvas,
3532+
center: center,
3533+
scale: scale,
3534+
labelPainter: labelPainter,
3535+
textScaleFactor: textScaleFactor,
3536+
sizeWithOverflow: sizeWithOverflow,
3537+
backgroundPaintColor: sliderTheme.valueIndicatorColor!,
3538+
);
3539+
}
3540+
}
3541+
3542+
class _DropSliderValueIndicatorPathPainter {
3543+
const _DropSliderValueIndicatorPathPainter();
3544+
3545+
static const double _triangleHeight = 10.0;
3546+
static const double _labelPadding = 8.0;
3547+
static const double _preferredHeight = 32.0;
3548+
static const double _minLabelWidth = 20.0;
3549+
static const double _minRectHeight = 28.0;
3550+
static const double _rectYOffset = 6.0;
3551+
static const double _bottomTipYOffset = 16.0;
3552+
static const double _preferredHalfHeight = _preferredHeight / 2;
3553+
static const double _upperRectRadius = 4;
3554+
3555+
Size getPreferredSize(
3556+
TextPainter labelPainter,
3557+
double textScaleFactor,
3558+
) {
3559+
assert(labelPainter != null);
3560+
final double width = math.max(_minLabelWidth, labelPainter.width) + _labelPadding * 2 * textScaleFactor;
3561+
return Size(width, _preferredHeight * textScaleFactor);
3562+
}
3563+
3564+
double getHorizontalShift({
3565+
required RenderBox parentBox,
3566+
required Offset center,
3567+
required TextPainter labelPainter,
3568+
required double textScaleFactor,
3569+
required Size sizeWithOverflow,
3570+
required double scale,
3571+
}) {
3572+
assert(!sizeWithOverflow.isEmpty);
3573+
3574+
const double edgePadding = 8.0;
3575+
final double rectangleWidth = _upperRectangleWidth(labelPainter, scale);
3576+
/// Value indicator draws on the Overlay and by using the global Offset
3577+
/// we are making sure we use the bounds of the Overlay instead of the Slider.
3578+
final Offset globalCenter = parentBox.localToGlobal(center);
3579+
3580+
// The rectangle must be shifted towards the center so that it minimizes the
3581+
// chance of it rendering outside the bounds of the render box. If the shift
3582+
// is negative, then the lobe is shifted from right to left, and if it is
3583+
// positive, then the lobe is shifted from left to right.
3584+
final double overflowLeft = math.max(0, rectangleWidth / 2 - globalCenter.dx + edgePadding);
3585+
final double overflowRight = math.max(0, rectangleWidth / 2 - (sizeWithOverflow.width - globalCenter.dx - edgePadding));
3586+
3587+
if (rectangleWidth < sizeWithOverflow.width) {
3588+
return overflowLeft - overflowRight;
3589+
} else if (overflowLeft - overflowRight > 0) {
3590+
return overflowLeft - (edgePadding * textScaleFactor);
3591+
} else {
3592+
return -overflowRight + (edgePadding * textScaleFactor);
3593+
}
3594+
}
3595+
3596+
double _upperRectangleWidth(TextPainter labelPainter, double scale) {
3597+
final double unscaledWidth = math.max(_minLabelWidth, labelPainter.width) + _labelPadding;
3598+
return unscaledWidth * scale;
3599+
}
3600+
3601+
BorderRadius _adjustBorderRadius(Rect rect) {
3602+
const double rectness = 0.0;
3603+
return BorderRadius.lerp(
3604+
BorderRadius.circular(_upperRectRadius),
3605+
BorderRadius.all(Radius.circular(rect.shortestSide / 2.0)),
3606+
1.0 - rectness,
3607+
)!;
3608+
}
3609+
3610+
void paint({
3611+
required RenderBox parentBox,
3612+
required Canvas canvas,
3613+
required Offset center,
3614+
required double scale,
3615+
required TextPainter labelPainter,
3616+
required double textScaleFactor,
3617+
required Size sizeWithOverflow,
3618+
required Color backgroundPaintColor,
3619+
Color? strokePaintColor,
3620+
}) {
3621+
if (scale == 0.0) {
3622+
// Zero scale essentially means "do not draw anything", so it's safe to just return.
3623+
return;
3624+
}
3625+
assert(!sizeWithOverflow.isEmpty);
3626+
3627+
final double rectangleWidth = _upperRectangleWidth(labelPainter, scale);
3628+
final double horizontalShift = getHorizontalShift(
3629+
parentBox: parentBox,
3630+
center: center,
3631+
labelPainter: labelPainter,
3632+
textScaleFactor: textScaleFactor,
3633+
sizeWithOverflow: sizeWithOverflow,
3634+
scale: scale,
3635+
);
3636+
final Rect upperRect = Rect.fromLTWH(
3637+
-rectangleWidth / 2 + horizontalShift,
3638+
-_rectYOffset - _minRectHeight,
3639+
rectangleWidth,
3640+
_minRectHeight,
3641+
);
3642+
3643+
final Paint fillPaint = Paint()..color = backgroundPaintColor;
3644+
3645+
canvas.save();
3646+
canvas.translate(center.dx, center.dy - _bottomTipYOffset);
3647+
canvas.scale(scale, scale);
3648+
3649+
final BorderRadius adjustedBorderRadius = _adjustBorderRadius(upperRect);
3650+
final RRect borderRect = adjustedBorderRadius.resolve(labelPainter.textDirection).toRRect(upperRect);
3651+
final Path trianglePath = Path()
3652+
..lineTo(-_triangleHeight, -_triangleHeight)
3653+
..lineTo(_triangleHeight, -_triangleHeight)
3654+
..close();
3655+
canvas.drawPath(trianglePath, fillPaint);
3656+
canvas.drawRRect(borderRect, fillPaint);
3657+
3658+
// The label text is centered within the value indicator.
3659+
final double bottomTipToUpperRectTranslateY = -_preferredHalfHeight / 2 - upperRect.height;
3660+
canvas.translate(0, bottomTipToUpperRectTranslateY);
3661+
final Offset boxCenter = Offset(horizontalShift, upperRect.height / 1.75);
3662+
final Offset halfLabelPainterOffset = Offset(labelPainter.width / 2, labelPainter.height / 2);
3663+
final Offset labelOffset = boxCenter - halfLabelPainterOffset;
3664+
labelPainter.paint(canvas, labelOffset);
3665+
canvas.restore();
3666+
}
3667+
}

0 commit comments

Comments
 (0)