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:
parent
ec50578982
commit
40c22749f5
@ -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),
|
||||
];
|
||||
|
@ -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);
|
||||
|
@ -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)),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user