From 9e88446f89c05659bc761c39519a407f89b25079 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Mon, 15 Jul 2024 10:09:09 +0300 Subject: [PATCH] Fix `Slider` thumb doesn't align with divisions, thumb padding, and rounded corners (#149594) fixes [[Slider] Thumb's center doesn't align with division's center](https://github.com/flutter/flutter/issues/62567) fixes [Slider thumb doesn't respect round slider track shape](https://github.com/flutter/flutter/issues/149591) fixes [`RoundedRectSliderTrackShape` corners are not rendered correctly](https://github.com/flutter/flutter/issues/149589) (Verified these behaviors with Android components implementation) ### Code sample
expand to view the code sample ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State { double _value = 5.0; @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( sliderTheme: const SliderThemeData( trackHeight: 32, thumbColor: Colors.green, activeTrackColor: Colors.deepPurple, inactiveTrackColor: Colors.amber, ), ), home: Scaffold( body: Slider( value: _value, // divisions: 10, // ignore: avoid_redundant_argument_values min: 0, max: 10, onChanged: (double value) { setState(() { _value = value; }); }, ), ), ); } } ```
### Description This PR fixes several core `Sliders` issues which are apparent in https://github.com/flutter/flutter/pull/147783. As a result, fixing the these bugs will unblock it. ### 1. Fixes the thumb doesn't align with `Slider` divisions. ![Group 8](https://github.com/flutter/flutter/assets/48603081/9aa138ae-9525-4af4-8fc7-3cea0692a6d7) ![Group 9](https://github.com/flutter/flutter/assets/48603081/e97940ae-a1c8-4b8b-9971-1cf417d32e40) ### 2. Fixes `RoundedRectSliderTrackShape` corners are not rendered correctly. ![Group 10](https://github.com/flutter/flutter/assets/48603081/ed20a6bb-d5c9-486b-a020-2c9ca7de55da) ### 3. Fixes round track shape corners when the thumb is at the start or end of the round track shape. ![Group 4](https://github.com/flutter/flutter/assets/48603081/37a2e820-402d-4964-a206-717ccf1c5c02) ![Group 3](https://github.com/flutter/flutter/assets/48603081/5d36d523-5fb7-466f-9d53-b6928963fcab) ![Group 7](https://github.com/flutter/flutter/assets/48603081/8f3b4c48-f04d-4681-a62f-a7ea5a3e19fa) --- packages/flutter/lib/src/material/slider.dart | 18 +- .../lib/src/material/slider_theme.dart | 72 ++++-- .../test/material/inherited_theme_test.dart | 4 +- .../flutter/test/material/slider_test.dart | 56 ++-- .../test/material/slider_theme_test.dart | 241 +++++++++++++----- 5 files changed, 275 insertions(+), 116 deletions(-) diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index b8dec8d7eaef6..b7528f4554e81 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -1650,7 +1650,22 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { sliderTheme: _sliderTheme, isDiscrete: isDiscrete, ); - final Offset thumbCenter = Offset(trackRect.left + visualPosition * trackRect.width, trackRect.center.dy); + final double padding = isDiscrete || _sliderTheme.trackShape!.isRounded ? trackRect.height : 0.0; + final double thumbPosition = isDiscrete + ? trackRect.left + visualPosition * (trackRect.width - padding) + padding / 2 + : trackRect.left + visualPosition * trackRect.width; + // Apply padding to trackRect.left and trackRect.right if the track height is + // greater than the thumb radius to ensure the thumb is drawn within the track. + final Size thumbSize = _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete); + final double thumbPadding = (padding > thumbSize.width / 2 ? padding / 2 : 0); + final Offset thumbCenter = Offset( + clampDouble( + thumbPosition, + trackRect.left + thumbPadding, + trackRect.right - thumbPadding, + ), + trackRect.center.dy, + ); if (isInteractive) { final Size overlaySize = sliderTheme.overlayShape!.getPreferredSize(isInteractive, false); overlayRect = Rect.fromCircle(center: thumbCenter, radius: overlaySize.width / 2.0); @@ -1692,7 +1707,6 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { isEnabled: isInteractive, sliderTheme: _sliderTheme, ).width; - final double padding = trackRect.height; final double adjustedTrackWidth = trackRect.width - padding; // If the tick marks would be too dense, don't bother painting them. if (adjustedTrackWidth / divisions! >= 3.0 * tickMarkWidth) { diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index aa3af71419396..1fd0219017a3b 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -1117,6 +1117,11 @@ abstract class SliderTrackShape { bool isDiscrete, required TextDirection textDirection, }); + + /// Whether the track shape is rounded. + /// + /// This is used to determine the correct position of the thumb in relation to the track. + bool get isRounded => false; } /// Base class for [RangeSlider] thumb shapes. @@ -1534,6 +1539,10 @@ mixin BaseSliderTrackShape { // If the parentBox's size less than slider's size the trackRight will be less than trackLeft, so switch them. return Rect.fromLTRB(math.min(trackLeft, trackRight), trackTop, math.max(trackLeft, trackRight), trackBottom); } + + /// Whether the track shape is rounded. This is used to determine the correct + /// position of the thumb in relation to the track. Defaults to false. + bool get isRounded => false; } /// A [Slider] track that's a simple rectangle. @@ -1714,39 +1723,45 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS ); final Radius trackRadius = Radius.circular(trackRect.height / 2); final Radius activeTrackRadius = Radius.circular((trackRect.height + additionalActiveTrackHeight) / 2); - - context.canvas.drawRRect( - RRect.fromLTRBAndCorners( - trackRect.left, - (textDirection == TextDirection.ltr) ? trackRect.top - (additionalActiveTrackHeight / 2): trackRect.top, - thumbCenter.dx, - (textDirection == TextDirection.ltr) ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom, - topLeft: (textDirection == TextDirection.ltr) ? activeTrackRadius : trackRadius, - bottomLeft: (textDirection == TextDirection.ltr) ? activeTrackRadius: trackRadius, - ), - leftTrackPaint, - ); - context.canvas.drawRRect( - RRect.fromLTRBAndCorners( - thumbCenter.dx, - (textDirection == TextDirection.rtl) ? trackRect.top - (additionalActiveTrackHeight / 2) : trackRect.top, - trackRect.right, - (textDirection == TextDirection.rtl) ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom, - topRight: (textDirection == TextDirection.rtl) ? activeTrackRadius : trackRadius, - bottomRight: (textDirection == TextDirection.rtl) ? activeTrackRadius : trackRadius, - ), - rightTrackPaint, - ); + final bool isLTR = textDirection == TextDirection.ltr; + final bool isRTL = textDirection == TextDirection.rtl; + + final bool drawInactiveTrack = thumbCenter.dx < (trackRect.right - (sliderTheme.trackHeight! / 2)); + if (drawInactiveTrack) { + // Draw the inactive track segment. + context.canvas.drawRRect( + RRect.fromLTRBR( + thumbCenter.dx - (sliderTheme.trackHeight! / 2), + isRTL ? trackRect.top - (additionalActiveTrackHeight / 2) : trackRect.top, + trackRect.right, + isRTL ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom, + isLTR ? trackRadius : activeTrackRadius, + ), + rightTrackPaint, + ); + } + final bool drawActiveTrack = thumbCenter.dx > (trackRect.left + (sliderTheme.trackHeight! / 2)); + if (drawActiveTrack) { + // Draw the active track segment. + context.canvas.drawRRect( + RRect.fromLTRBR( + trackRect.left, + isLTR ? trackRect.top - (additionalActiveTrackHeight / 2): trackRect.top, + thumbCenter.dx + (sliderTheme.trackHeight! / 2), + isLTR ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom, + isLTR ? activeTrackRadius : trackRadius, + ), + leftTrackPaint, + ); + } final bool showSecondaryTrack = (secondaryOffset != null) && - ((textDirection == TextDirection.ltr) - ? (secondaryOffset.dx > thumbCenter.dx) - : (secondaryOffset.dx < thumbCenter.dx)); + (isLTR ? (secondaryOffset.dx > thumbCenter.dx) : (secondaryOffset.dx < thumbCenter.dx)); if (showSecondaryTrack) { final ColorTween secondaryTrackColorTween = ColorTween(begin: sliderTheme.disabledSecondaryActiveTrackColor, end: sliderTheme.secondaryActiveTrackColor); final Paint secondaryTrackPaint = Paint()..color = secondaryTrackColorTween.evaluate(enableAnimation)!; - if (textDirection == TextDirection.ltr) { + if (isLTR) { context.canvas.drawRRect( RRect.fromLTRBAndCorners( thumbCenter.dx, @@ -1773,6 +1788,9 @@ class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackS } } } + + @override + bool get isRounded => true; } diff --git a/packages/flutter/test/material/inherited_theme_test.dart b/packages/flutter/test/material/inherited_theme_test.dart index a36a6fec1c557..c0b9145817c52 100644 --- a/packages/flutter/test/material/inherited_theme_test.dart +++ b/packages/flutter/test/material/inherited_theme_test.dart @@ -558,7 +558,7 @@ void main() { await tester.tap(find.text('push wrapped')); await tester.pumpAndSettle(); // route animation RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); - expect(sliderBox, paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor)); + expect(sliderBox, paints..rrect(color: inactiveTrackColor)..rrect(color: activeTrackColor)); expect(sliderBox, paints..circle(color: thumbColor)); Navigator.of(navigatorContext).pop(); @@ -567,7 +567,7 @@ void main() { await tester.tap(find.text('push unwrapped')); await tester.pumpAndSettle(); // route animation sliderBox = tester.firstRenderObject(find.byType(Slider)); - expect(sliderBox, isNot(paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor))); + expect(sliderBox, isNot(paints..rrect(color: inactiveTrackColor)..rrect(color: activeTrackColor))); expect(sliderBox, isNot(paints..circle(color: thumbColor))); }); diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart index ce1a110f41c04..65fc4f18a1cf6 100644 --- a/packages/flutter/test/material/slider_test.dart +++ b/packages/flutter/test/material/slider_test.dart @@ -167,7 +167,7 @@ void main() { expect(value, equals(0.20)); expect(log.length, 1); - expect(log[0], const Offset(212.0, 300.0)); + expect(log[0], const Offset(213.0, 300.0)); }); testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async { @@ -417,8 +417,8 @@ void main() { ); final List expectedLog = [ - const Offset(24.0, 300.0), - const Offset(24.0, 300.0), + const Offset(26.0, 300.0), + const Offset(26.0, 300.0), const Offset(400.0, 300.0), ]; final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); @@ -439,13 +439,13 @@ void main() { await tester.pump(const Duration(milliseconds: 10)); expect(value, equals(0.0)); expect(log.length, 7); - expect(log.last.dx, moreOrLessEquals(344.5, epsilon: 0.1)); + expect(log.last.dx, moreOrLessEquals(344.8, epsilon: 0.1)); // Final position. await tester.pump(const Duration(milliseconds: 80)); - expectedLog.add(const Offset(24.0, 300.0)); + expectedLog.add(const Offset(26.0, 300.0)); expect(value, equals(0.0)); expect(log.length, 8); - expect(log.last.dx, moreOrLessEquals(24.0, epsilon: 0.1)); + expect(log.last.dx, moreOrLessEquals(26.0, epsilon: 0.1)); await gesture.up(); }); @@ -490,7 +490,7 @@ void main() { expect(updates, equals(1)); }); - testWidgets('discrete Slider repaints when dragged', (WidgetTester tester) async { + testWidgets('Discrete Slider repaints when dragged', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; final List log = []; @@ -526,8 +526,8 @@ void main() { ); final List expectedLog = [ - const Offset(24.0, 300.0), - const Offset(24.0, 300.0), + const Offset(26.0, 300.0), + const Offset(26.0, 300.0), const Offset(400.0, 300.0), ]; final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); @@ -548,13 +548,13 @@ void main() { await tester.pump(const Duration(milliseconds: 10)); expect(value, equals(0.0)); expect(log.length, 7); - expect(log.last.dx, moreOrLessEquals(344.5, epsilon: 0.1)); + expect(log.last.dx, moreOrLessEquals(344.8, epsilon: 0.1)); // Final position. await tester.pump(const Duration(milliseconds: 80)); - expectedLog.add(const Offset(24.0, 300.0)); + expectedLog.add(const Offset(26.0, 300.0)); expect(value, equals(0.0)); expect(log.length, 8); - expect(log.last.dx, moreOrLessEquals(24.0, epsilon: 0.1)); + expect(log.last.dx, moreOrLessEquals(26.0, epsilon: 0.1)); await gesture.up(); }); @@ -1175,7 +1175,7 @@ void main() { ..circle(x: 400.0, y: 24.0, radius: 1.0) ..circle(x: 587.0, y: 24.0, radius: 1.0) ..circle(x: 774.0, y: 24.0, radius: 1.0) - ..circle(x: 24.0, y: 24.0, radius: 10.0), + ..circle(x: 26.0, y: 24.0, radius: 10.0), ); gesture = await tester.startGesture(center); @@ -1186,13 +1186,13 @@ void main() { expect( material, paints - ..circle(x: 111.20703125, y: 24.0, radius: 5.687664985656738) + ..circle(x: 112.7431640625, y: 24.0, radius: 5.687664985656738) ..circle(x: 26.0, y: 24.0, radius: 1.0) ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) ..circle(x: 587.0, y: 24.0, radius: 1.0) ..circle(x: 774.0, y: 24.0, radius: 1.0) - ..circle(x: 111.20703125, y: 24.0, radius: 10.0), + ..circle(x: 112.7431640625, y: 24.0, radius: 10.0), ); // Reparenting in the middle of an animation should do nothing. @@ -1206,13 +1206,13 @@ void main() { expect( material, paints - ..circle(x: 190.0135726928711, y: 24.0, radius: 12.0) + ..circle(x: 191.130521774292, y: 24.0, radius: 12.0) ..circle(x: 26.0, y: 24.0, radius: 1.0) ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) ..circle(x: 587.0, y: 24.0, radius: 1.0) ..circle(x: 774.0, y: 24.0, radius: 1.0) - ..circle(x: 190.0135726928711, y: 24.0, radius: 10.0), + ..circle(x: 191.130521774292, y: 24.0, radius: 10.0), ); // Wait for animations to finish. await tester.pumpAndSettle(); @@ -3249,11 +3249,11 @@ void main() { expect( renderObject, paints - // active track RRect - ..rrect(rrect: RRect.fromLTRBAndCorners(-14.0, 2.0, 5.0, 8.0, topLeft: const Radius.circular(3.0), bottomLeft: const Radius.circular(3.0))) - // inactive track RRect - ..rrect(rrect: RRect.fromLTRBAndCorners(5.0, 3.0, 24.0, 7.0, topRight: const Radius.circular(2.0), bottomRight: const Radius.circular(2.0))) - // thumb + // Inactive track RRect. + ..rrect(rrect: RRect.fromLTRBR(3.0, 3.0, 24.0, 7.0, const Radius.circular(2.0))) + // Active track RRect. + ..rrect(rrect: RRect.fromLTRBR(-14.0, 2.0, 7.0, 8.0, const Radius.circular(3.0))) + // Thumb. ..circle(x: 5.0, y: 5.0, radius: 10.0, ), ); }); @@ -3285,7 +3285,7 @@ void main() { await tester.pumpAndSettle(); // Finish the animation. late RRect activeTrackRRect; - expect(renderObject, paints..something((Symbol method, List arguments) { + expect(renderObject, paints..rrect()..something((Symbol method, List arguments) { if (method != #drawRRect) { return false; } @@ -3293,10 +3293,18 @@ void main() { return true; })); + const double padding = 4.0; // The thumb should at one-third(5 / 15) of the Slider. // The right of the active track shape is the position of the thumb. // 24.0 is the default margin, (800.0 - 24.0 - 24.0) is the slider's width. - expect(nearEqual(activeTrackRRect.right, (800.0 - 24.0 - 24.0) * (5 / 15) + 24.0, 0.01), true); + expect( + nearEqual( + activeTrackRRect.right, + (800.0 - 24.0 - 24.0 + (padding / 2)) * (5 / 15) + 24.0 + padding / 2, + 0.01, + ), + true, + ); }); testWidgets('Slider paints thumbColor', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart index ef7b6a8298d0d..386e220424abd 100644 --- a/packages/flutter/test/material/slider_theme_test.dart +++ b/packages/flutter/test/material/slider_theme_test.dart @@ -165,12 +165,20 @@ void main() { expect( material, paints - ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 297.0, 362.4, 303.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: activeTrackColor) - ..rrect(rrect: RRect.fromLTRBAndCorners(362.4, 298.0, 776.0, 302.0, topRight: radius, bottomRight: radius), color: inactiveTrackColor), + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(360.4, 298.0, 776.0, 302.0, radius), + color: inactiveTrackColor, + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(24.0, 297.0, 364.4, 303.0, activatedRadius), + color: activeTrackColor, + ), ); // Test default colors for enabled slider. - expect(material, paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor)..rrect(color: secondaryActiveTrackColor)); + expect(material, paints..rrect(color: inactiveTrackColor)..rrect(color: activeTrackColor)..rrect(color: secondaryActiveTrackColor)); expect(material, paints..shadow(color: shadowColor)); expect(material, paints..circle(color: thumbColor)); expect(material, isNot(paints..circle(color: disabledThumbColor))); @@ -182,7 +190,7 @@ void main() { // Test defaults colors for discrete slider. await tester.pumpWidget(buildApp(divisions: 3)); - expect(material, paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor)..rrect(color: secondaryActiveTrackColor)); + expect(material, paints..rrect(color: inactiveTrackColor)..rrect(color: activeTrackColor)..rrect(color: secondaryActiveTrackColor)); expect( material, paints @@ -204,8 +212,8 @@ void main() { expect( material, paints - ..rrect(color: disabledActiveTrackColor) ..rrect(color: disabledInactiveTrackColor) + ..rrect(color: disabledActiveTrackColor) ..rrect(color: disabledSecondaryActiveTrackColor), ); expect(material, paints..shadow(color: shadowColor)..circle(color: disabledThumbColor)); @@ -307,7 +315,7 @@ void main() { final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); // Check default theme for enabled widget. - expect(material, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.inactiveTrackColor)..rrect(color: sliderTheme.secondaryActiveTrackColor)); + expect(material, paints..rrect(color: sliderTheme.inactiveTrackColor)..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.secondaryActiveTrackColor)); expect(material, paints..shadow(color: const Color(0xff000000))); expect(material, paints..circle(color: sliderTheme.thumbColor)); expect(material, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); @@ -319,7 +327,7 @@ void main() { // Test setting only the activeColor. await tester.pumpWidget(buildApp(activeColor: customColor1)); - expect(material, paints..rrect(color: customColor1)..rrect(color: sliderTheme.inactiveTrackColor)..rrect(color: sliderTheme.secondaryActiveTrackColor)); + expect(material, paints..rrect(color: sliderTheme.inactiveTrackColor)..rrect(color: customColor1)..rrect(color: sliderTheme.secondaryActiveTrackColor)); expect(material, paints..shadow(color: Colors.black)); expect(material, paints..circle(color: customColor1)); expect(material, isNot(paints..circle(color: sliderTheme.thumbColor))); @@ -330,7 +338,7 @@ void main() { // Test setting only the inactiveColor. await tester.pumpWidget(buildApp(inactiveColor: customColor1)); - expect(material, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: customColor1)..rrect(color: sliderTheme.secondaryActiveTrackColor)); + expect(material, paints..rrect(color: customColor1)..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.secondaryActiveTrackColor)); expect(material, paints..shadow(color: Colors.black)); expect(material, paints..circle(color: sliderTheme.thumbColor)); expect(material, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); @@ -340,7 +348,7 @@ void main() { // Test setting only the secondaryActiveColor. await tester.pumpWidget(buildApp(secondaryActiveColor: customColor1)); - expect(material, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.inactiveTrackColor)..rrect(color: customColor1)); + expect(material, paints..rrect(color: sliderTheme.inactiveTrackColor)..rrect(color: sliderTheme.activeTrackColor)..rrect(color: customColor1)); expect(material, paints..shadow(color: Colors.black)); expect(material, paints..circle(color: sliderTheme.thumbColor)); expect(material, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); @@ -350,7 +358,7 @@ void main() { // Test setting both activeColor, inactiveColor, and secondaryActiveColor. await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2, secondaryActiveColor: customColor3)); - expect(material, paints..rrect(color: customColor1)..rrect(color: customColor2)..rrect(color: customColor3)); + expect(material, paints..rrect(color: customColor2)..rrect(color: customColor1)..rrect(color: customColor3)); expect(material, paints..shadow(color: Colors.black)); expect(material, paints..circle(color: customColor1)); expect(material, isNot(paints..circle(color: sliderTheme.thumbColor))); @@ -361,7 +369,7 @@ void main() { // Test colors for discrete slider. await tester.pumpWidget(buildApp(divisions: 3)); - expect(material, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.inactiveTrackColor)..rrect(color: sliderTheme.secondaryActiveTrackColor)); + expect(material, paints..rrect(color: sliderTheme.inactiveTrackColor)..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.secondaryActiveTrackColor)); expect( material, paints @@ -384,7 +392,7 @@ void main() { secondaryActiveColor: customColor3, divisions: 3, )); - expect(material, paints..rrect(color: customColor1)..rrect(color: customColor2)..rrect(color: customColor3)); + expect(material, paints..rrect(color: customColor2)..rrect(color: customColor1)..rrect(color: customColor3)); expect( material, paints @@ -409,8 +417,8 @@ void main() { expect( material, paints - ..rrect(color: sliderTheme.disabledActiveTrackColor) ..rrect(color: sliderTheme.disabledInactiveTrackColor) + ..rrect(color: sliderTheme.disabledActiveTrackColor) ..rrect(color: sliderTheme.disabledSecondaryActiveTrackColor), ); expect(material, paints..shadow(color: Colors.black)..circle(color: sliderTheme.disabledThumbColor)); @@ -443,8 +451,8 @@ void main() { expect( material, paints - ..rrect(color: sliderTheme.disabledActiveTrackColor) ..rrect(color: sliderTheme.disabledInactiveTrackColor) + ..rrect(color: sliderTheme.disabledActiveTrackColor) ..rrect(color: sliderTheme.disabledSecondaryActiveTrackColor), ); expect(material, paints..circle(color: sliderTheme.disabledThumbColor)); @@ -485,8 +493,8 @@ void main() { valueIndicatorBox, paints ..rrect(color: const Color(0xfffafafa)) - ..rrect(color: customColor1) // active track - ..rrect(color: customColor2) // inactive track + ..rrect(color: customColor2) // Inactive track + ..rrect(color: customColor1) // Active track ..circle(color: customColor1.withOpacity(0.12)) // overlay ..circle(color: customColor2) // 1st tick mark ..circle(color: customColor2) // 2nd tick mark @@ -549,7 +557,7 @@ void main() { final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // Test Slider parameters. - expect(material, paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor)..rrect(color: secondaryActiveTrackColor)); + expect(material, paints..rrect(color: inactiveTrackColor)..rrect(color: activeTrackColor)..rrect(color: secondaryActiveTrackColor)); expect(material, paints..circle(color: thumbColor)); } finally { debugDisableShadows = true; @@ -712,9 +720,23 @@ void main() { expect( material, paints - ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 297.0, 212.0, 303.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.activeTrackColor) - ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 776.0, 302.0, topRight: radius, bottomRight: radius), color: sliderTheme.inactiveTrackColor) - ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 400.0, 302.0, topRight: radius, bottomRight: radius), color: sliderTheme.secondaryActiveTrackColor), + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(210.0, 298.0, 776.0, 302.0, radius), + color: sliderTheme.inactiveTrackColor, + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(24.0, 297.0, 214.0, 303.0, activatedRadius), + color: sliderTheme.activeTrackColor, + ) + ..rrect( + rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 400.0, 302.0, + topRight: radius, + bottomRight: radius, + ), + color: sliderTheme.secondaryActiveTrackColor, + ), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, secondaryTrackValue: 0.5, enabled: false)); @@ -724,9 +746,23 @@ void main() { expect( material, paints - ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 297.0, 212.0, 303.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.disabledActiveTrackColor) - ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 776.0, 302.0, topRight: radius, bottomRight: radius), color: sliderTheme.disabledInactiveTrackColor) - ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 400.0, 302.0, topRight: radius, bottomRight: radius), color: sliderTheme.disabledSecondaryActiveTrackColor), + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(210.0, 298.0, 776.0, 302.0, radius), + color: sliderTheme.disabledInactiveTrackColor, + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(24.0, 297.0, 214.0, 303.0, activatedRadius), + color: sliderTheme.disabledActiveTrackColor, + ) + ..rrect( + rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 400.0, 302.0, + topRight: radius, + bottomRight: radius, + ), + color: sliderTheme.disabledSecondaryActiveTrackColor, + ), ); }); @@ -1276,8 +1312,16 @@ void main() { expect( material, paints - ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 291.0, 212.0, 309.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.activeTrackColor) - ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 292.0, 776.0, 308.0, topRight: radius, bottomRight: radius), color: sliderTheme.inactiveTrackColor), + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(204.0, 292.0, 776.0, 308.0, radius), + color: sliderTheme.inactiveTrackColor, + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(24.0, 291.0, 220.0, 309.0, activatedRadius), + color: sliderTheme.activeTrackColor, + ), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false)); @@ -1288,8 +1332,16 @@ void main() { expect( material, paints - ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 291.0, 212.0, 309.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.disabledActiveTrackColor) - ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 292.0, 776.0, 308.0, topRight: radius, bottomRight: radius), color: sliderTheme.disabledInactiveTrackColor), + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(204.0, 292.0, 776.0, 308.0, radius), + color: sliderTheme.disabledInactiveTrackColor, + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(24.0, 291.0, 220.0, 309.0, activatedRadius), + color: sliderTheme.disabledActiveTrackColor, + ), ); }); @@ -1422,23 +1474,21 @@ void main() { expect( material, paints - // active track RRect. Starts 10 pixels from left of screen. - ..rrect(rrect: RRect.fromLTRBAndCorners( - 10.0, - 297.0, - 400.0, - 303.0, - topLeft: const Radius.circular(3.0), - bottomLeft: const Radius.circular(3.0), - )) - // inactive track RRect. Ends 10 pixels from right of screen. - ..rrect(rrect: RRect.fromLTRBAndCorners( - 400.0, + // Inactive track RRect. Ends 10 pixels from right of screen. + ..rrect(rrect: RRect.fromLTRBR( + 398.0, 298.0, 790.0, 302.0, - topRight: const Radius.circular(2.0), - bottomRight: const Radius.circular(2.0), + const Radius.circular(2.0), + )) + // Active track RRect. Starts 10 pixels from left of screen. + ..rrect(rrect: RRect.fromLTRBR( + 10.0, + 297.0, + 402.0, + 303.0, + const Radius.circular(3.0), )) // The thumb. ..circle(x: 400.0, y: 300.0, radius: 10.0), @@ -1882,11 +1932,12 @@ void main() { testWidgets('activeTrackRadius is taken into account when painting the border of the active track', (WidgetTester tester) async { await tester.pumpWidget(_buildApp( + value: 0.5, ThemeData().sliderTheme.copyWith( trackShape: const RoundedRectSliderTrackShapeWithCustomAdditionalActiveTrackHeight( - additionalActiveTrackHeight: 10.0 - ) - ) + additionalActiveTrackHeight: 10.0, + ), + ), )); await tester.pumpAndSettle(); final Offset center = tester.getCenter(find.byType(Slider)); @@ -1894,16 +1945,10 @@ void main() { expect( find.byType(Slider), paints - ..rrect(rrect: RRect.fromLTRBAndCorners( - 24.0, 293.0, 24.0, 307.0, - topLeft: const Radius.circular(7.0), - bottomLeft: const Radius.circular(7.0), - )) - ..rrect(rrect: RRect.fromLTRBAndCorners( - 24.0, 298.0, 776.0, 302.0, - topRight: const Radius.circular(2.0), - bottomRight: const Radius.circular(2.0), - )), + // Inactive track. + ..rrect(rrect: RRect.fromLTRBR(398.0, 298.0, 776.0, 302.0, const Radius.circular(2.0))) + // Active track. + ..rrect(rrect: RRect.fromLTRBR(24.0, 293.0, 402.0, 307.0, const Radius.circular(7.0))), ); // Finish gesture to release resources. @@ -2078,8 +2123,8 @@ void main() { valueIndicatorBox, paints ..rrect(color: const Color(0xfffef7ff)) - ..rrect(color: const Color(0xff6750a4)) ..rrect(color: const Color(0xffe6e0e9)) + ..rrect(color: const Color(0xff6750a4)) ..path(color: Color(theme.colorScheme.primary.value)) ); @@ -2434,6 +2479,75 @@ void main() { await gesture.up(); }); + group('RoundedRectSliderTrackShape', () { + testWidgets('Only draw active track if thumb center is higher than trackRect.left and track radius', (WidgetTester tester) async { + const SliderThemeData sliderTheme = SliderThemeData(trackShape: RoundedRectSliderTrackShape()); + await tester.pumpWidget(_buildApp(sliderTheme)); + + MaterialInkController material = Material.of(tester.element(find.byType(Slider))); + expect( + material, + paints + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(22.0, 298.0, 776.0, 302.0, const Radius.circular(2.0)), + ), + ); + + await tester.pumpWidget(_buildApp(sliderTheme, value: 0.025)); + + material = Material.of(tester.element(find.byType(Slider))); + expect( + material, + paints + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(40.8, 298.0, 776.0, 302.0, const Radius.circular(2.0)), + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(24.0, 297.0, 44.8, 303.0, const Radius.circular(3.0)), + ), + ); + }); + + testWidgets('Only draw inactive track if thumb center is lower than trackRect.right and track radius', (WidgetTester tester) async { + const SliderThemeData sliderTheme = SliderThemeData(trackShape: RoundedRectSliderTrackShape()); + await tester.pumpWidget(_buildApp(sliderTheme, value: 1.0)); + + MaterialInkController material = Material.of(tester.element(find.byType(Slider))); + expect( + material, + paints + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(24.0, 297.0, 778.0, 303.0, const Radius.circular(3.0)), + ), + ); + + await tester.pumpWidget(_buildApp(sliderTheme, value: 0.975)); + + material = Material.of(tester.element(find.byType(Slider))); + expect( + material, + paints + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(755.2, 298.0, 776.0, 302.0, const Radius.circular(2.0)), + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(24.0, 297.0, 759.2, 303.0, const Radius.circular(3.0)), + ), + ); + }); + }); + + testWidgets('SliderTrackShape isRounded defaults', (WidgetTester tester) async { + expect(const RectangularSliderTrackShape().isRounded, isFalse); + expect(const RoundedRectSliderTrackShape().isRounded, isTrue); + }); + group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests @@ -2500,12 +2614,17 @@ void main() { expect( material, paints - ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 297.0, 362.4, 303.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: activeTrackColor) - ..rrect(rrect: RRect.fromLTRBAndCorners(362.4, 298.0, 776.0, 302.0, topRight: radius, bottomRight: radius), color: inactiveTrackColor), + ..rrect( + rrect: RRect.fromLTRBR(360.4, 298.0, 776.0, 302.0, radius), + color: inactiveTrackColor) + ..rrect( + rrect: RRect.fromLTRBR(24.0, 297.0, 364.4, 303.0, activatedRadius), + color: activeTrackColor, + ), ); // Test default colors for enabled slider. - expect(material, paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor)..rrect(color: secondaryActiveTrackColor)); + expect(material, paints..rrect(color: inactiveTrackColor)..rrect(color: activeTrackColor)..rrect(color: secondaryActiveTrackColor)); expect(material, paints..shadow(color: shadowColor)); expect(material, paints..circle(color: thumbColor)); expect(material, isNot(paints..circle(color: disabledThumbColor))); @@ -2517,7 +2636,7 @@ void main() { // Test defaults colors for discrete slider. await tester.pumpWidget(buildApp(divisions: 3)); - expect(material, paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor)..rrect(color: secondaryActiveTrackColor)); + expect(material, paints..rrect(color: inactiveTrackColor)..rrect(color: activeTrackColor)..rrect(color: secondaryActiveTrackColor)); expect( material, paints @@ -2539,8 +2658,8 @@ void main() { expect( material, paints - ..rrect(color: disabledActiveTrackColor) ..rrect(color: disabledInactiveTrackColor) + ..rrect(color: disabledActiveTrackColor) ..rrect(color: disabledSecondaryActiveTrackColor), ); expect(material, paints..shadow(color: Colors.black)..circle(color: disabledThumbColor)); @@ -2634,8 +2753,8 @@ void main() { valueIndicatorBox, paints ..rrect(color: const Color(0xfffafafa)) - ..rrect(color: const Color(0xff2196f3)) ..rrect(color: const Color(0x3d2196f3)) + ..rrect(color: const Color(0xff2196f3)) // Test that the value indicator text is painted with the correct color. ..path(color: const Color(0xf55f5f5f)) );