Skip to content

Commit

Permalink
Add Material 3 support for Slider - Part 2 (#114624)
Browse files Browse the repository at this point in the history
* Add Material 3 support for Slider - Part 2

* Kick tests

* Update drawing order to fix html renderer bug

* Update test
  • Loading branch information
TahaTesser authored Nov 17, 2022
1 parent 0344407 commit ac06523
Show file tree
Hide file tree
Showing 5 changed files with 525 additions and 17 deletions.
8 changes: 8 additions & 0 deletions dev/tools/gen_defaults/lib/slider_template.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ class _${blockName}DefaultsM3 extends SliderThemeData {
return Colors.transparent;
});
@override
TextStyle? get valueIndicatorTextStyle => ${textStyle('$tokenGroup.label.label-text')}!.copyWith(
color: ${componentColor('$tokenGroup.label.label-text')},
);
@override
SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
}
''';

Expand Down
32 changes: 27 additions & 5 deletions packages/flutter/lib/src/material/slider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
const SliderTickMarkShape defaultTickMarkShape = RoundSliderTickMarkShape();
const SliderComponentShape defaultOverlayShape = RoundSliderOverlayShape();
const SliderComponentShape defaultThumbShape = RoundSliderThumbShape();
const SliderComponentShape defaultValueIndicatorShape = RectangularSliderValueIndicatorShape();
final SliderComponentShape defaultValueIndicatorShape = defaults.valueIndicatorShape!;
const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;

final Set<MaterialState> states = <MaterialState>{
Expand Down Expand Up @@ -810,9 +810,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
overlayShape: sliderTheme.overlayShape ?? defaultOverlayShape,
valueIndicatorShape: valueIndicatorShape,
showValueIndicator: sliderTheme.showValueIndicator ?? defaultShowValueIndicator,
valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? theme.textTheme.bodyLarge!.copyWith(
color: theme.colorScheme.onPrimary,
),
valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? defaults.valueIndicatorTextStyle,
);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? sliderTheme.mouseCursor?.resolve(states)
Expand Down Expand Up @@ -851,6 +849,14 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
break;
}

final double textScaleFactor = theme.useMaterial3
// TODO(tahatesser): This is an eye-balled value.
// This needs to be updated when accessibility
// guidelines are available on the material specs page
// https://m3.material.io/components/sliders/accessibility.
? math.min(MediaQuery.of(context).textScaleFactor, 1.3)
: MediaQuery.of(context).textScaleFactor;

return Semantics(
container: true,
slider: true,
Expand All @@ -873,7 +879,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
divisions: widget.divisions,
label: widget.label,
sliderTheme: sliderTheme,
textScaleFactor: MediaQuery.of(context).textScaleFactor,
textScaleFactor: textScaleFactor,
screenSize: screenSize(),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
onChangeStart: _handleDragStart,
Expand Down Expand Up @@ -1858,6 +1864,14 @@ class _SliderDefaultsM2 extends SliderThemeData {

@override
Color? get overlayColor => _colors.primary.withOpacity(0.12);

@override
TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.bodyLarge!.copyWith(
color: _colors.onPrimary,
);

@override
SliderComponentShape? get valueIndicatorShape => const RectangularSliderValueIndicatorShape();
}

// BEGIN GENERATED TOKEN PROPERTIES - Slider
Expand Down Expand Up @@ -1927,6 +1941,14 @@ class _SliderDefaultsM3 extends SliderThemeData {

return Colors.transparent;
});

@override
TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.labelMedium!.copyWith(
color: _colors.onPrimary,
);

@override
SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
}

// END GENERATED TOKEN PROPERTIES - Slider
182 changes: 182 additions & 0 deletions packages/flutter/lib/src/material/slider_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3483,3 +3483,185 @@ void _debugDrawShadow(Canvas canvas, Path path, double elevation) {
);
}
}

/// The default shape of a Material 3 [Slider]'s value indicator.
///
/// See also:
///
/// * [Slider], which includes a value indicator defined by this shape.
/// * [SliderTheme], which can be used to configure the slider value indicator
/// of all sliders in a widget subtree.
class DropSliderValueIndicatorShape extends SliderComponentShape {
/// Create a slider value indicator that resembles a drop shape.
const DropSliderValueIndicatorShape();

static const _DropSliderValueIndicatorPathPainter _pathPainter = _DropSliderValueIndicatorPathPainter();

@override
Size getPreferredSize(
bool isEnabled,
bool isDiscrete, {
TextPainter? labelPainter,
double? textScaleFactor,
}) {
assert(labelPainter != null);
assert(textScaleFactor != null && textScaleFactor >= 0);
return _pathPainter.getPreferredSize(labelPainter!, textScaleFactor!);
}

@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
final double scale = activationAnimation.value;
_pathPainter.paint(
parentBox: parentBox,
canvas: canvas,
center: center,
scale: scale,
labelPainter: labelPainter,
textScaleFactor: textScaleFactor,
sizeWithOverflow: sizeWithOverflow,
backgroundPaintColor: sliderTheme.valueIndicatorColor!,
);
}
}

class _DropSliderValueIndicatorPathPainter {
const _DropSliderValueIndicatorPathPainter();

static const double _triangleHeight = 10.0;
static const double _labelPadding = 8.0;
static const double _preferredHeight = 32.0;
static const double _minLabelWidth = 20.0;
static const double _minRectHeight = 28.0;
static const double _rectYOffset = 6.0;
static const double _bottomTipYOffset = 16.0;
static const double _preferredHalfHeight = _preferredHeight / 2;
static const double _upperRectRadius = 4;

Size getPreferredSize(
TextPainter labelPainter,
double textScaleFactor,
) {
assert(labelPainter != null);
final double width = math.max(_minLabelWidth, labelPainter.width) + _labelPadding * 2 * textScaleFactor;
return Size(width, _preferredHeight * textScaleFactor);
}

double getHorizontalShift({
required RenderBox parentBox,
required Offset center,
required TextPainter labelPainter,
required double textScaleFactor,
required Size sizeWithOverflow,
required double scale,
}) {
assert(!sizeWithOverflow.isEmpty);

const double edgePadding = 8.0;
final double rectangleWidth = _upperRectangleWidth(labelPainter, scale);
/// Value indicator draws on the Overlay and by using the global Offset
/// we are making sure we use the bounds of the Overlay instead of the Slider.
final Offset globalCenter = parentBox.localToGlobal(center);

// The rectangle must be shifted towards the center so that it minimizes the
// chance of it rendering outside the bounds of the render box. If the shift
// is negative, then the lobe is shifted from right to left, and if it is
// positive, then the lobe is shifted from left to right.
final double overflowLeft = math.max(0, rectangleWidth / 2 - globalCenter.dx + edgePadding);
final double overflowRight = math.max(0, rectangleWidth / 2 - (sizeWithOverflow.width - globalCenter.dx - edgePadding));

if (rectangleWidth < sizeWithOverflow.width) {
return overflowLeft - overflowRight;
} else if (overflowLeft - overflowRight > 0) {
return overflowLeft - (edgePadding * textScaleFactor);
} else {
return -overflowRight + (edgePadding * textScaleFactor);
}
}

double _upperRectangleWidth(TextPainter labelPainter, double scale) {
final double unscaledWidth = math.max(_minLabelWidth, labelPainter.width) + _labelPadding;
return unscaledWidth * scale;
}

BorderRadius _adjustBorderRadius(Rect rect) {
const double rectness = 0.0;
return BorderRadius.lerp(
BorderRadius.circular(_upperRectRadius),
BorderRadius.all(Radius.circular(rect.shortestSide / 2.0)),
1.0 - rectness,
)!;
}

void paint({
required RenderBox parentBox,
required Canvas canvas,
required Offset center,
required double scale,
required TextPainter labelPainter,
required double textScaleFactor,
required Size sizeWithOverflow,
required Color backgroundPaintColor,
Color? strokePaintColor,
}) {
if (scale == 0.0) {
// Zero scale essentially means "do not draw anything", so it's safe to just return.
return;
}
assert(!sizeWithOverflow.isEmpty);

final double rectangleWidth = _upperRectangleWidth(labelPainter, scale);
final double horizontalShift = getHorizontalShift(
parentBox: parentBox,
center: center,
labelPainter: labelPainter,
textScaleFactor: textScaleFactor,
sizeWithOverflow: sizeWithOverflow,
scale: scale,
);
final Rect upperRect = Rect.fromLTWH(
-rectangleWidth / 2 + horizontalShift,
-_rectYOffset - _minRectHeight,
rectangleWidth,
_minRectHeight,
);

final Paint fillPaint = Paint()..color = backgroundPaintColor;

canvas.save();
canvas.translate(center.dx, center.dy - _bottomTipYOffset);
canvas.scale(scale, scale);

final BorderRadius adjustedBorderRadius = _adjustBorderRadius(upperRect);
final RRect borderRect = adjustedBorderRadius.resolve(labelPainter.textDirection).toRRect(upperRect);
final Path trianglePath = Path()
..lineTo(-_triangleHeight, -_triangleHeight)
..lineTo(_triangleHeight, -_triangleHeight)
..close();
canvas.drawPath(trianglePath, fillPaint);
canvas.drawRRect(borderRect, fillPaint);

// The label text is centered within the value indicator.
final double bottomTipToUpperRectTranslateY = -_preferredHalfHeight / 2 - upperRect.height;
canvas.translate(0, bottomTipToUpperRectTranslateY);
final Offset boxCenter = Offset(horizontalShift, upperRect.height / 1.75);
final Offset halfLabelPainterOffset = Offset(labelPainter.width / 2, labelPainter.height / 2);
final Offset labelOffset = boxCenter - halfLabelPainterOffset;
labelPainter.paint(canvas, labelOffset);
canvas.restore();
}
}
Loading

0 comments on commit ac06523

Please sign in to comment.