diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index 0d4843bd93..045cc2e06e 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -196,6 +196,7 @@ class Slider extends StatefulWidget { this.focusNode, this.autofocus = false, this.allowedInteraction, + this.padding, }) : _sliderType = _SliderType.material, assert(min <= max), assert(value >= min && value <= max, @@ -238,6 +239,7 @@ class Slider extends StatefulWidget { this.autofocus = false, this.allowedInteraction, }) : _sliderType = _SliderType.adaptive, + padding = null, assert(min <= max), assert(value >= min && value <= max, 'Value $value is not between minimum $min and maximum $max'), @@ -550,6 +552,14 @@ class Slider extends StatefulWidget { /// Defaults to [SliderInteraction.tapAndSlide]. final SliderInteraction? allowedInteraction; + /// Determines the padding around the [Slider]. + /// + /// If specified, this padding overrides the default vertical padding of + /// the [Slider], defaults to the height of the overlay shape, and the + /// horizontal padding, defaults to the width of the thumb shape or + /// overlay shape, whichever is larger. + final EdgeInsetsGeometry? padding; + final _SliderType _sliderType ; @override @@ -853,6 +863,7 @@ class _SliderState extends State with TickerProviderStateMixin { valueIndicatorShape: valueIndicatorShape, showValueIndicator: sliderTheme.showValueIndicator ?? defaultShowValueIndicator, valueIndicatorTextStyle: valueIndicatorTextStyle, + padding: widget.padding ?? sliderTheme.padding, ); final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs(widget.mouseCursor, states) ?? sliderTheme.mouseCursor?.resolve(states) @@ -899,6 +910,36 @@ class _SliderState extends State with TickerProviderStateMixin { : MediaQuery.textScalerOf(context); final double effectiveTextScale = textScaler.scale(fontSizeToScale) / fontSizeToScale; + Widget result = CompositedTransformTarget( + link: _layerLink, + child: _SliderRenderObjectWidget( + key: _renderObjectKey, + value: _convert(widget.value), + secondaryTrackValue: (widget.secondaryTrackValue != null) ? _convert(widget.secondaryTrackValue!) : null, + divisions: widget.divisions, + label: widget.label, + sliderTheme: sliderTheme, + textScaleFactor: effectiveTextScale, + screenSize: screenSize(), + onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, + onChangeStart: _handleDragStart, + onChangeEnd: _handleDragEnd, + state: this, + semanticFormatterCallback: widget.semanticFormatterCallback, + hasFocus: _focused, + hovering: _hovering, + allowedInteraction: effectiveAllowedInteraction, + ), + ); + + final EdgeInsetsGeometry? padding = widget.padding ?? sliderTheme.padding; + if (padding != null) { + result = Padding( + padding: padding, + child: result, + ); + } + return Semantics( container: true, slider: true, @@ -912,27 +953,7 @@ class _SliderState extends State with TickerProviderStateMixin { onShowFocusHighlight: _handleFocusHighlightChanged, onShowHoverHighlight: _handleHoverChanged, mouseCursor: effectiveMouseCursor, - child: CompositedTransformTarget( - link: _layerLink, - child: _SliderRenderObjectWidget( - key: _renderObjectKey, - value: _convert(widget.value), - secondaryTrackValue: (widget.secondaryTrackValue != null) ? _convert(widget.secondaryTrackValue!) : null, - divisions: widget.divisions, - label: widget.label, - sliderTheme: sliderTheme, - textScaleFactor: effectiveTextScale, - screenSize: screenSize(), - onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, - onChangeStart: _handleDragStart, - onChangeEnd: _handleDragEnd, - state: this, - semanticFormatterCallback: widget.semanticFormatterCallback, - hasFocus: _focused, - hovering: _hovering, - allowedInteraction: effectiveAllowedInteraction, - ), - ), + child: result, ), ); } @@ -1145,8 +1166,13 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { // centered on the track. double get _maxSliderPartWidth => _sliderPartSizes.map((Size size) => size.width).reduce(math.max); double get _maxSliderPartHeight => _sliderPartSizes.map((Size size) => size.height).reduce(math.max); + double get _thumbSizeHeight => _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete).height; + double get _overlayHeight => _sliderTheme.overlayShape!.getPreferredSize(isInteractive, isDiscrete).height; List get _sliderPartSizes => [ - _sliderTheme.overlayShape!.getPreferredSize(isInteractive, isDiscrete), + Size( + _sliderTheme.overlayShape!.getPreferredSize(isInteractive, isDiscrete).width, + _sliderTheme.padding != null ? _thumbSizeHeight : _overlayHeight + ), _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete), _sliderTheme.tickMarkShape!.getPreferredSize(isEnabled: isInteractive, sliderTheme: sliderTheme), ]; diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index e988ed09d5..db1004076b 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -296,6 +296,7 @@ class SliderThemeData with Diagnosticable { this.thumbSelector, this.mouseCursor, this.allowedInteraction, + this.padding, }); /// Generates a SliderThemeData from three main colors. @@ -588,6 +589,14 @@ class SliderThemeData with Diagnosticable { /// If specified, overrides the default value of [Slider.allowedInteraction]. final SliderInteraction? allowedInteraction; + /// Determines the padding around the [Slider]. + /// + /// If specified, this padding overrides the default vertical padding of + /// the [Slider], defaults to the height of the overlay shape, and the + /// horizontal padding, defaults to the width of the thumb shape or + /// overlay shape, whichever is larger. + final EdgeInsetsGeometry? padding; + /// Creates a copy of this object but with the given fields replaced with the /// new values. SliderThemeData copyWith({ @@ -623,6 +632,7 @@ class SliderThemeData with Diagnosticable { RangeThumbSelector? thumbSelector, MaterialStateProperty? mouseCursor, SliderInteraction? allowedInteraction, + EdgeInsetsGeometry? padding, }) { return SliderThemeData( trackHeight: trackHeight ?? this.trackHeight, @@ -657,6 +667,7 @@ class SliderThemeData with Diagnosticable { thumbSelector: thumbSelector ?? this.thumbSelector, mouseCursor: mouseCursor ?? this.mouseCursor, allowedInteraction: allowedInteraction ?? this.allowedInteraction, + padding: padding ?? this.padding, ); } @@ -700,6 +711,7 @@ class SliderThemeData with Diagnosticable { thumbSelector: t < 0.5 ? a.thumbSelector : b.thumbSelector, mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor, allowedInteraction: t < 0.5 ? a.allowedInteraction : b.allowedInteraction, + padding: EdgeInsetsGeometry.lerp(a.padding, b.padding, t), ); } @@ -737,6 +749,7 @@ class SliderThemeData with Diagnosticable { thumbSelector, mouseCursor, allowedInteraction, + padding, ), ); @@ -780,7 +793,8 @@ class SliderThemeData with Diagnosticable { && other.minThumbSeparation == minThumbSeparation && other.thumbSelector == thumbSelector && other.mouseCursor == mouseCursor - && other.allowedInteraction == allowedInteraction; + && other.allowedInteraction == allowedInteraction + && other.padding == padding; } @override @@ -819,6 +833,7 @@ class SliderThemeData with Diagnosticable { properties.add(DiagnosticsProperty('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector)); properties.add(DiagnosticsProperty>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor)); properties.add(EnumProperty('allowedInteraction', allowedInteraction, defaultValue: defaultData.allowedInteraction)); + properties.add(DiagnosticsProperty('padding', padding, defaultValue: defaultData.padding)); } } @@ -1535,9 +1550,9 @@ mixin BaseSliderTrackShape { assert(overlayWidth >= 0); assert(trackHeight >= 0); - final double trackLeft = offset.dx + math.max(overlayWidth / 2, thumbWidth / 2); + final double trackLeft = offset.dx + (sliderTheme.padding == null ? math.max(overlayWidth / 2, thumbWidth / 2) : 0); final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2; - final double trackRight = trackLeft + parentBox.size.width - math.max(thumbWidth, overlayWidth); + final double trackRight = trackLeft + parentBox.size.width - (sliderTheme.padding == null ? math.max(thumbWidth, overlayWidth) : 0); final double trackBottom = trackTop + trackHeight; // 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); diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart index 036b9e932a..a1be488cbd 100644 --- a/packages/flutter/test/material/slider_test.dart +++ b/packages/flutter/test/material/slider_test.dart @@ -4598,4 +4598,93 @@ void main() { paints..path(color: const Color(0xff000000))..paragraph(), ); }, variant: TargetPlatformVariant.desktop()); + + testWidgets('Slider.padding can override the default Slider padding', (WidgetTester tester) async { + Widget buildSlider({ EdgeInsetsGeometry? padding }) { + return MaterialApp( + home: Material( + child: Center( + child: IntrinsicHeight( + child: Slider( + padding: padding, + value: 0.5, + onChanged: (double value) {}, + ), + ), + ), + ), + ); + } + + RenderBox sliderRenderBox() { + return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderSlider') as RenderBox; + } + + // Test Slider height and tracks spacing with zero padding. + await tester.pumpWidget(buildSlider(padding: EdgeInsets.zero)); + await tester.pumpAndSettle(); + + // The height equals to the default thumb height. + expect(sliderRenderBox().size, const Size(800, 20)); + expect( + find.byType(Slider), + paints + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(398.0, 8.0, 800.0, 12.0, const Radius.circular(2.0)), + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 7.0, 402.0, 13.0, const Radius.circular(3.0)), + ), + ); + + // Test Slider height and tracks spacing with directional padding. + const double startPadding = 100; + const double endPadding = 20; + await tester.pumpWidget(buildSlider( + padding: const EdgeInsetsDirectional.only( + start: startPadding, + end: endPadding, + ), + )); + await tester.pumpAndSettle(); + + expect(sliderRenderBox().size, const Size(800 - startPadding - endPadding, 20)); + expect( + find.byType(Slider), + paints + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(338.0, 8.0, 680.0, 12.0, const Radius.circular(2.0)), + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 7.0, 342.0, 13.0, const Radius.circular(3.0)), + ), + ); + + + // Test Slider height and tracks spacing with top and bottom padding. + const double topPadding = 100; + const double bottomPadding = 20; + const double trackHeight = 20; + await tester.pumpWidget(buildSlider(padding: const EdgeInsetsDirectional.only(top: topPadding, bottom: bottomPadding))); + await tester.pumpAndSettle(); + + expect(tester.getSize(find.byType(Slider)), const Size(800, topPadding + trackHeight + bottomPadding)); + expect(sliderRenderBox().size, const Size(800, 20)); + expect( + find.byType(Slider), + paints + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(398.0, 8.0, 800.0, 12.0, const Radius.circular(2.0)), + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 7.0, 402.0, 13.0, const Radius.circular(3.0)), + ), + ); + }); } diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart index 9cdc48064c..01a5c65e93 100644 --- a/packages/flutter/test/material/slider_theme_test.dart +++ b/packages/flutter/test/material/slider_theme_test.dart @@ -2551,6 +2551,95 @@ void main() { expect(const RoundedRectSliderTrackShape().isRounded, isTrue); }); + testWidgets('SliderThemeData.padding can override the default Slider padding', (WidgetTester tester) async { + Widget buildSlider({ EdgeInsetsGeometry? padding }) { + return MaterialApp( + theme: ThemeData(sliderTheme: SliderThemeData(padding: padding)), + home: Material( + child: Center( + child: IntrinsicHeight( + child: Slider( + value: 0.5, + onChanged: (double value) {}, + ), + ), + ), + ), + ); + } + + RenderBox sliderRenderBox() { + return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderSlider') as RenderBox; + } + + // Test Slider height and tracks spacing with zero padding. + await tester.pumpWidget(buildSlider(padding: EdgeInsets.zero)); + await tester.pumpAndSettle(); + + // The height equals to the default thumb height. + expect(sliderRenderBox().size, const Size(800, 20)); + expect( + find.byType(Slider), + paints + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(398.0, 8.0, 800.0, 12.0, const Radius.circular(2.0)), + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 7.0, 402.0, 13.0, const Radius.circular(3.0)), + ), + ); + + // Test Slider height and tracks spacing with directional padding. + const double startPadding = 100; + const double endPadding = 20; + await tester.pumpWidget(buildSlider( + padding: const EdgeInsetsDirectional.only( + start: startPadding, + end: endPadding, + ), + )); + await tester.pumpAndSettle(); + + expect(sliderRenderBox().size, const Size(800 - startPadding - endPadding, 20)); + expect( + find.byType(Slider), + paints + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(338.0, 8.0, 680.0, 12.0, const Radius.circular(2.0)), + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 7.0, 342.0, 13.0, const Radius.circular(3.0)), + ), + ); + + + // Test Slider height and tracks spacing with top and bottom padding. + const double topPadding = 100; + const double bottomPadding = 20; + const double trackHeight = 20; + await tester.pumpWidget(buildSlider(padding: const EdgeInsetsDirectional.only(top: topPadding, bottom: bottomPadding))); + await tester.pumpAndSettle(); + + expect(tester.getSize(find.byType(Slider)), const Size(800, topPadding + trackHeight + bottomPadding)); + expect(sliderRenderBox().size, const Size(800, 20)); + expect( + find.byType(Slider), + paints + // Inactive track. + ..rrect( + rrect: RRect.fromLTRBR(398.0, 8.0, 800.0, 12.0, const Radius.circular(2.0)), + ) + // Active track. + ..rrect( + rrect: RRect.fromLTRBR(0.0, 7.0, 402.0, 13.0, const Radius.circular(3.0)), + ), + ); + }); + group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests