diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index 18648fe4593..2d4f8444cc9 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -277,6 +277,7 @@ class SliderThemeData with Diagnosticable { this.disabledThumbColor, this.overlayColor, this.valueIndicatorColor, + this.valueIndicatorStrokeColor, this.overlayShape, this.tickMarkShape, this.thumbShape, @@ -344,6 +345,7 @@ class SliderThemeData with Diagnosticable { disabledThumbColor: primaryColorDark.withAlpha(disabledThumbAlpha), overlayColor: primaryColor.withAlpha(overlayAlpha), valueIndicatorColor: primaryColor.withAlpha(valueIndicatorAlpha), + valueIndicatorStrokeColor: primaryColor.withAlpha(valueIndicatorAlpha), overlayShape: const RoundSliderOverlayShape(), tickMarkShape: const RoundSliderTickMarkShape(), thumbShape: const RoundSliderThumbShape(), @@ -423,6 +425,8 @@ class SliderThemeData with Diagnosticable { /// The color given to the [valueIndicatorShape] to draw itself with. final Color? valueIndicatorColor; + /// The color given to the [valueIndicatorShape] stroke. + final Color? valueIndicatorStrokeColor; /// The shape that will be used to draw the [Slider]'s overlay. /// @@ -600,6 +604,7 @@ class SliderThemeData with Diagnosticable { Color? disabledThumbColor, Color? overlayColor, Color? valueIndicatorColor, + Color? valueIndicatorStrokeColor, SliderComponentShape? overlayShape, SliderTickMarkShape? tickMarkShape, SliderComponentShape? thumbShape, @@ -633,6 +638,7 @@ class SliderThemeData with Diagnosticable { disabledThumbColor: disabledThumbColor ?? this.disabledThumbColor, overlayColor: overlayColor ?? this.overlayColor, valueIndicatorColor: valueIndicatorColor ?? this.valueIndicatorColor, + valueIndicatorStrokeColor: valueIndicatorStrokeColor ?? this.valueIndicatorStrokeColor, overlayShape: overlayShape ?? this.overlayShape, tickMarkShape: tickMarkShape ?? this.tickMarkShape, thumbShape: thumbShape ?? this.thumbShape, @@ -675,6 +681,7 @@ class SliderThemeData with Diagnosticable { disabledThumbColor: Color.lerp(a.disabledThumbColor, b.disabledThumbColor, t), overlayColor: Color.lerp(a.overlayColor, b.overlayColor, t), valueIndicatorColor: Color.lerp(a.valueIndicatorColor, b.valueIndicatorColor, t), + valueIndicatorStrokeColor: Color.lerp(a.valueIndicatorStrokeColor, b.valueIndicatorStrokeColor, t), overlayShape: t < 0.5 ? a.overlayShape : b.overlayShape, tickMarkShape: t < 0.5 ? a.tickMarkShape : b.tickMarkShape, thumbShape: t < 0.5 ? a.thumbShape : b.thumbShape, @@ -755,6 +762,7 @@ class SliderThemeData with Diagnosticable { && other.disabledThumbColor == disabledThumbColor && other.overlayColor == overlayColor && other.valueIndicatorColor == valueIndicatorColor + && other.valueIndicatorStrokeColor == valueIndicatorStrokeColor && other.overlayShape == overlayShape && other.tickMarkShape == tickMarkShape && other.thumbShape == thumbShape @@ -792,6 +800,7 @@ class SliderThemeData with Diagnosticable { properties.add(ColorProperty('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor)); properties.add(ColorProperty('overlayColor', overlayColor, defaultValue: defaultData.overlayColor)); properties.add(ColorProperty('valueIndicatorColor', valueIndicatorColor, defaultValue: defaultData.valueIndicatorColor)); + properties.add(ColorProperty('valueIndicatorStrokeColor', valueIndicatorStrokeColor, defaultValue: defaultData.valueIndicatorStrokeColor)); properties.add(DiagnosticsProperty('overlayShape', overlayShape, defaultValue: defaultData.overlayShape)); properties.add(DiagnosticsProperty('tickMarkShape', tickMarkShape, defaultValue: defaultData.tickMarkShape)); properties.add(DiagnosticsProperty('thumbShape', thumbShape, defaultValue: defaultData.thumbShape)); @@ -2625,6 +2634,7 @@ class RectangularSliderValueIndicatorShape extends SliderComponentShape { textScaleFactor: textScaleFactor, sizeWithOverflow: sizeWithOverflow, backgroundPaintColor: sliderTheme.valueIndicatorColor!, + strokePaintColor: sliderTheme.valueIndicatorStrokeColor, ); } } @@ -2704,7 +2714,7 @@ class RectangularRangeSliderValueIndicatorShape textScaleFactor: textScaleFactor!, sizeWithOverflow: sizeWithOverflow!, backgroundPaintColor: sliderTheme!.valueIndicatorColor!, - strokePaintColor: isOnTop! ? sliderTheme.overlappingShapeStrokeColor : null, + strokePaintColor: isOnTop! ? sliderTheme.overlappingShapeStrokeColor : sliderTheme.valueIndicatorStrokeColor, ); } } @@ -2892,7 +2902,7 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { labelPainter, textScaleFactor, sizeWithOverflow, - null, + sliderTheme.valueIndicatorStrokeColor, ); } } @@ -2974,7 +2984,7 @@ class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShap labelPainter, textScaleFactor!, sizeWithOverflow!, - isOnTop ? sliderTheme.overlappingShapeStrokeColor : null, + isOnTop ? sliderTheme.overlappingShapeStrokeColor : sliderTheme.valueIndicatorStrokeColor, ); } } @@ -3422,6 +3432,7 @@ class DropSliderValueIndicatorShape extends SliderComponentShape { textScaleFactor: textScaleFactor, sizeWithOverflow: sizeWithOverflow, backgroundPaintColor: sliderTheme.valueIndicatorColor!, + strokePaintColor: sliderTheme.valueIndicatorStrokeColor, ); } } @@ -3538,8 +3549,17 @@ class _DropSliderValueIndicatorPathPainter { ..lineTo(-_triangleHeight, -_triangleHeight) ..lineTo(_triangleHeight, -_triangleHeight) ..close(); + trianglePath.addRRect(borderRect); + + if (strokePaintColor != null) { + final Paint strokePaint = Paint() + ..color = strokePaintColor + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke; + canvas.drawPath(trianglePath, strokePaint); + } + canvas.drawPath(trianglePath, fillPaint); - canvas.drawRRect(borderRect, fillPaint); // The label text is centered within the value indicator. final double bottomTipToUpperRectTranslateY = -_preferredHalfHeight / 2 - upperRect.height; diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart index 06f10503f7b..62b0168c786 100644 --- a/packages/flutter/test/material/slider_theme_test.dart +++ b/packages/flutter/test/material/slider_theme_test.dart @@ -50,6 +50,7 @@ void main() { disabledThumbColor: Color(0xFF000013), overlayColor: Color(0xFF000014), valueIndicatorColor: Color(0xFF000015), + valueIndicatorStrokeColor: Color(0xFF000015), overlayShape: RoundSliderOverlayShape(), tickMarkShape: RoundSliderTickMarkShape(), thumbShape: RoundSliderThumbShape(), @@ -87,6 +88,7 @@ void main() { 'disabledThumbColor: Color(0xff000013)', 'overlayColor: Color(0xff000014)', 'valueIndicatorColor: Color(0xff000015)', + 'valueIndicatorStrokeColor: Color(0xff000015)', "overlayShape: Instance of 'RoundSliderOverlayShape'", "tickMarkShape: Instance of 'RoundSliderTickMarkShape'", "thumbShape: Instance of 'RoundSliderThumbShape'", @@ -630,6 +632,7 @@ void main() { expect(sliderTheme.disabledThumbColor, equals(customColor2.withAlpha(0x52))); expect(sliderTheme.overlayColor, equals(customColor1.withAlpha(0x1f))); expect(sliderTheme.valueIndicatorColor, equals(customColor1.withAlpha(0xff))); + expect(sliderTheme.valueIndicatorStrokeColor, equals(customColor1.withAlpha(0xff))); expect(sliderTheme.valueIndicatorTextStyle!.color, equals(customColor4)); }); @@ -688,6 +691,7 @@ void main() { expect(lerp.disabledThumbColor, equals(middleGrey.withAlpha(0x52))); expect(lerp.overlayColor, equals(middleGrey.withAlpha(0x1f))); expect(lerp.valueIndicatorColor, equals(middleGrey.withAlpha(0xff))); + expect(lerp.valueIndicatorStrokeColor, equals(middleGrey.withAlpha(0xff))); expect(lerp.valueIndicatorTextStyle!.color, equals(middleGrey.withAlpha(0xff))); }); @@ -2078,9 +2082,6 @@ void main() { ..rrect(color: const Color(0xff6750a4)) ..rrect(color: const Color(0xffe7e0ec)) ..path(color: Color(theme.colorScheme.primary.value)) - ..rrect( - color: Color(theme.colorScheme.primary.value), - ) ); // Finish gesture to release resources. @@ -2091,6 +2092,349 @@ void main() { } }); + testWidgetsWithLeakTracking('RectangularSliderValueIndicatorShape supports SliderTheme.valueIndicatorStrokeColor', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + sliderTheme: const SliderThemeData( + showValueIndicator: ShowValueIndicator.always, + valueIndicatorShape: RectangularSliderValueIndicatorShape(), + valueIndicatorColor: Color(0xff000001), + valueIndicatorStrokeColor: Color(0xff000002), + ), + ); + + const double value = 0.5; + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Slider( + value: value, + label: '$value', + onChanged: (double newValue) {}, + ), + ), + ), + ), + ); + + final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); + + final Offset center = tester.getCenter(find.byType(Slider)); + await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + + expect( + valueIndicatorBox, + paints + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.sliderTheme.valueIndicatorStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor), + ); + }); + + testWidgetsWithLeakTracking('PaddleSliderValueIndicatorShape supports SliderTheme.valueIndicatorStrokeColor', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + sliderTheme: const SliderThemeData( + showValueIndicator: ShowValueIndicator.always, + valueIndicatorShape: PaddleSliderValueIndicatorShape(), + valueIndicatorColor: Color(0xff000001), + valueIndicatorStrokeColor: Color(0xff000002), + ), + ); + + const double value = 0.5; + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Slider( + value: value, + label: '$value', + onChanged: (double newValue) {}, + ), + ), + ), + ), + ); + + final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); + + final Offset center = tester.getCenter(find.byType(Slider)); + await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + + expect( + valueIndicatorBox, + paints + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.sliderTheme.valueIndicatorStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor), + ); + }); + + testWidgetsWithLeakTracking('DropSliderValueIndicatorShape supports SliderTheme.valueIndicatorStrokeColor', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + sliderTheme: const SliderThemeData( + showValueIndicator: ShowValueIndicator.always, + valueIndicatorShape: DropSliderValueIndicatorShape(), + valueIndicatorColor: Color(0xff000001), + valueIndicatorStrokeColor: Color(0xff000002), + ), + ); + + const double value = 0.5; + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Center( + child: Slider( + value: value, + label: '$value', + onChanged: (double newValue) {}, + ), + ), + ), + ), + ); + + final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); + + final Offset center = tester.getCenter(find.byType(Slider)); + await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + + expect( + valueIndicatorBox, + paints + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.sliderTheme.valueIndicatorStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor), + ); + }); + + testWidgetsWithLeakTracking('RectangularRangeSliderValueIndicatorShape supports SliderTheme.valueIndicatorStrokeColor', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + sliderTheme: const SliderThemeData( + showValueIndicator: ShowValueIndicator.always, + rangeValueIndicatorShape: RectangularRangeSliderValueIndicatorShape(), + valueIndicatorColor: Color(0xff000001), + valueIndicatorStrokeColor: Color(0xff000002), + ) + ); + + RangeValues values = const RangeValues(0, 0.5); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Center( + child: RangeSlider( + values: values, + labels: RangeLabels( + values.start.toString(), + values.end.toString(), + ), + onChanged: (RangeValues val) { + values = val; + }, + ), + ), + ), + ), + ); + + final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); + + final Offset center = tester.getCenter(find.byType(RangeSlider)); + final TestGesture gesture = await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + + expect( + valueIndicatorBox, + paints + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.sliderTheme.valueIndicatorStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor) + ..path(color: theme.sliderTheme.valueIndicatorStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor) + ); + + await gesture.up(); + }); + + testWidgetsWithLeakTracking('RectangularRangeSliderValueIndicatorShape supports SliderTheme.valueIndicatorStrokeColor on overlapping indicator', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + sliderTheme: const SliderThemeData( + showValueIndicator: ShowValueIndicator.always, + rangeValueIndicatorShape: RectangularRangeSliderValueIndicatorShape(), + valueIndicatorColor: Color(0xff000001), + valueIndicatorStrokeColor: Color(0xff000002), + overlappingShapeStrokeColor: Color(0xff000003), + ) + ); + + RangeValues values = const RangeValues(0.0, 0.0); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Center( + child: RangeSlider( + values: values, + labels: RangeLabels( + values.start.toString(), + values.end.toString(), + ), + onChanged: (RangeValues val) { + values = val; + }, + ), + ), + ), + ), + ); + + final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); + + final Offset center = tester.getCenter(find.byType(RangeSlider)); + final TestGesture gesture = await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + + expect( + valueIndicatorBox, + paints + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.sliderTheme.valueIndicatorStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor) + ..path(color: theme.sliderTheme.overlappingShapeStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor) + ); + + await gesture.up(); + }); + + testWidgetsWithLeakTracking('PaddleRangeSliderValueIndicatorShape supports SliderTheme.valueIndicatorStrokeColor', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + sliderTheme: const SliderThemeData( + showValueIndicator: ShowValueIndicator.always, + rangeValueIndicatorShape: PaddleRangeSliderValueIndicatorShape(), + valueIndicatorColor: Color(0xff000001), + valueIndicatorStrokeColor: Color(0xff000002), + ) + ); + + RangeValues values = const RangeValues(0, 0.5); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Center( + child: RangeSlider( + values: values, + labels: RangeLabels( + values.start.toString(), + values.end.toString(), + ), + onChanged: (RangeValues val) { + values = val; + }, + ), + ), + ), + ), + ); + + final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); + + final Offset center = tester.getCenter(find.byType(RangeSlider)); + final TestGesture gesture = await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + + expect( + valueIndicatorBox, + paints + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.sliderTheme.valueIndicatorStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor) + ..path(color: theme.sliderTheme.valueIndicatorStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor) + ); + + await gesture.up(); + }); + + testWidgetsWithLeakTracking('PaddleRangeSliderValueIndicatorShape supports SliderTheme.valueIndicatorStrokeColor on overlapping indicator', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + sliderTheme: const SliderThemeData( + showValueIndicator: ShowValueIndicator.always, + rangeValueIndicatorShape: PaddleRangeSliderValueIndicatorShape(), + valueIndicatorColor: Color(0xff000001), + valueIndicatorStrokeColor: Color(0xff000002), + overlappingShapeStrokeColor: Color(0xff000003), + ) + ); + + RangeValues values = const RangeValues(0, 0); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Center( + child: RangeSlider( + values: values, + labels: RangeLabels( + values.start.toString(), + values.end.toString(), + ), + onChanged: (RangeValues val) { + values = val; + }, + ), + ), + ), + ), + ); + + final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); + + final Offset center = tester.getCenter(find.byType(RangeSlider)); + final TestGesture gesture = await tester.startGesture(center); + // Wait for value indicator animation to finish. + await tester.pumpAndSettle(); + + expect( + valueIndicatorBox, + paints + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.colorScheme.shadow) // shadow + ..path(color: theme.sliderTheme.valueIndicatorStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor) + ..path(color: theme.sliderTheme.overlappingShapeStrokeColor) + ..path(color: theme.sliderTheme.valueIndicatorColor) + ); + + await gesture.up(); + }); + group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests