Add ability to customize the default Slider padding (#156143)

Fixes [Ability to change Sliders padding](https://github.com/flutter/flutter/issues/40098)

Add ability to override default padding so the Slider can fit better in a layout.

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  double _sliderValue = 0.5;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        sliderTheme: const SliderThemeData(
          padding: EdgeInsets.symmetric(vertical: 4.0),
          thumbColor: Colors.red,
          inactiveTrackColor: Colors.amber,
        ),
      ),
      home: Scaffold(
        body: Directionality(
          textDirection: TextDirection.ltr,
          child: Center(
            child: Card(
              shape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.all(Radius.circular(4.0)),
              ),
              color: Theme.of(context).colorScheme.surfaceContainerHighest,
              margin: const EdgeInsets.symmetric(horizontal: 16.0),
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const Placeholder(fallbackHeight: 100.0),
                    Slider(
                      value: _sliderValue,
                      onChanged: (double value) {
                        setState(() {
                          _sliderValue = value;
                        });
                      },
                    ),
                    const Placeholder(fallbackHeight: 100.0),

                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}
```

</details>

### Before
(Cannot adjust default `Slider` padding to fill the horizontal space in a `Column` and reduce the padded height)

<img width="717" alt="Screenshot 2024-10-03 at 15 45 18" src="https://github.com/user-attachments/assets/e9d9a4d1-3087-45b4-8607-b94411e2bd23">

### After 
Can adjust default `Slider` padding via `SliderTheme`)

<img width="717" alt="Screenshot 2024-10-03 at 15 46 25" src="https://github.com/user-attachments/assets/cd455881-6d52-46cb-8ac6-cc33f50a13ff">
This commit is contained in:
Taha Tesser 2024-10-30 12:16:23 +02:00 committed by GitHub
parent ec50578982
commit 40c22749f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 244 additions and 25 deletions

View File

@ -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<Slider> with TickerProviderStateMixin {
valueIndicatorShape: valueIndicatorShape,
showValueIndicator: sliderTheme.showValueIndicator ?? defaultShowValueIndicator,
valueIndicatorTextStyle: valueIndicatorTextStyle,
padding: widget.padding ?? sliderTheme.padding,
);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? sliderTheme.mouseCursor?.resolve(states)
@ -899,6 +910,36 @@ class _SliderState extends State<Slider> 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<Slider> 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<Size> get _sliderPartSizes => <Size>[
_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),
];

View File

@ -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?>? 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<RangeThumbSelector>('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
properties.add(EnumProperty<SliderInteraction>('allowedInteraction', allowedInteraction, defaultValue: defaultData.allowedInteraction));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('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);

View File

@ -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)),
),
);
});
}

View File

@ -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