Unverified Commit b356b94a authored by Anthony's avatar Anthony Committed by GitHub

[Material] Allow slider shapes to be easily resized (#27510)

* Add size configs for round default shapes, and add tests, for all shapes that can be sized without creating new custom shape painters
parent 3dbec840
......@@ -601,7 +601,7 @@ class _RenderSlider extends RenderBox {
_sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete),
_sliderTheme.tickMarkShape.getPreferredSize(isEnabled: isInteractive, sliderTheme: sliderTheme),
];
double get _minPreferredTrackHeight =>_sliderTheme.trackHeight;
double get _minPreferredTrackHeight => _sliderTheme.trackHeight;
_SliderState _state;
Animation<double> _overlayAnimation;
......
......@@ -846,9 +846,16 @@ abstract class SliderComponentShape {
/// * [SliderTrackShape] Base component for creating other custom track
/// shapes.
class RectangularSliderTrackShape extends SliderTrackShape {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const RectangularSliderTrackShape();
/// Create a slider track that draws 2 rectangles.
const RectangularSliderTrackShape({ this.disabledThumbGapWidth = 2.0 });
/// Horizontal spacing, or gap, between the disabled thumb and the track.
///
/// This is only used when the slider is disabled. There is no gap around
/// the thumb and any part of the track when the slider is enabled. The
/// Material spec defaults this gap width 2, which is half of the disabled
/// thumb radius.
final double disabledThumbGapWidth;
@override
Rect getPreferredRect({
......@@ -874,8 +881,6 @@ class RectangularSliderTrackShape extends SliderTrackShape {
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
// Spacing for disabled slider state.
static const double _thumbGap = 2.0;
@override
void paint(
......@@ -909,11 +914,15 @@ class RectangularSliderTrackShape extends SliderTrackShape {
}
// Used to create a gap around the thumb iff the slider is disabled.
// If the slider is enabled, the track can be drawn beneath the thumb
// without a gap. But when the slider is disabled, the track is shortened
// and this gap helps determine how much shorter it should be.
// TODO(clocksmith): The new Material spec has a gray circle in place of this gap.
double horizontalAdjustment = 0.0;
if (!isEnabled) {
final double thumbRadius = sliderTheme.thumbShape.getPreferredSize(isEnabled, isDiscrete).width / 2.0;
final double gap = _thumbGap * (1.0 - enableAnimation.value);
horizontalAdjustment = thumbRadius + gap;
final double disabledThumbRadius = sliderTheme.thumbShape.getPreferredSize(false, isDiscrete).width / 2.0;
final double gap = disabledThumbGapWidth * (1.0 - enableAnimation.value);
horizontalAdjustment = disabledThumbRadius + gap;
}
final Rect trackRect = getPreferredRect(
......@@ -949,16 +958,22 @@ class RectangularSliderTrackShape extends SliderTrackShape {
/// * [SliderTheme], which can be used to configure the tick mark shape of all
/// sliders in a widget subtree.
class RoundSliderTickMarkShape extends SliderTickMarkShape {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const RoundSliderTickMarkShape();
/// Create a slider tick mark that draws a circle.
const RoundSliderTickMarkShape({ this.tickMarkRadius });
/// The preferred radius of the round tick mark.
///
/// If it is not provided, then half of the track height is used.
final double tickMarkRadius;
@override
Size getPreferredSize({
bool isEnabled,
SliderThemeData sliderTheme,
}) {
return Size.fromRadius(sliderTheme.trackHeight / 2);
// The tick marks are tiny circles. If no radius is provided, then they are
// defaulted to be the same height as the track.
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 2);
}
@override
......@@ -991,7 +1006,10 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape {
final Paint paint = Paint()..color = ColorTween(begin: begin, end: end).evaluate(enableAnimation);
// The tick marks are tiny circles that are the same height as the track.
final double tickMarkRadius = sliderTheme.trackHeight / 2;
final double tickMarkRadius = getPreferredSize(
isEnabled: isEnabled,
sliderTheme: sliderTheme,
).width / 2;
context.canvas.drawCircle(center, tickMarkRadius, paint);
}
}
......@@ -1005,14 +1023,30 @@ class RoundSliderTickMarkShape extends SliderTickMarkShape {
/// sliders in a widget subtree.
class RoundSliderThumbShape extends SliderComponentShape {
/// Create a slider thumb that draws a circle.
const RoundSliderThumbShape();
// TODO(clocksmith): This needs to be changed to 10 according to spec.
const RoundSliderThumbShape({
this.enabledThumbRadius = 6.0,
this.disabledThumbRadius
});
static const double _thumbRadius = 6.0;
static const double _disabledThumbRadius = 4.0;
/// The preferred radius of the round thumb shape when the slider is enabled.
///
/// If it is not provided, then the material default is used.
final double enabledThumbRadius;
/// The preferred radius of the round thumb shape when the slider is disabled.
///
/// If no disabledRadius is provided, then it is is derived from the enabled
/// thumb radius and has the same ratio of enabled size to disabled size as
/// the Material spec. The default resolves to 4, which is 2 / 3 of the
/// default enabled thumb.
final double disabledThumbRadius;
// TODO(clocksmith): This needs to be updated once the thumb size is updated to the Material spec.
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius * 2 / 3;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled ? _thumbRadius : _disabledThumbRadius);
return Size.fromRadius(isEnabled ? enabledThumbRadius : _disabledThumbRadius);
}
@override
......@@ -1031,7 +1065,7 @@ class RoundSliderThumbShape extends SliderComponentShape {
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
end: _thumbRadius,
end: enabledThumbRadius,
);
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
......@@ -1062,13 +1096,17 @@ class RoundSliderThumbShape extends SliderComponentShape {
/// sliders in a widget subtree.
class RoundSliderOverlayShape extends SliderComponentShape {
/// Create a slider thumb overlay that draws a circle.
const RoundSliderOverlayShape();
// TODO(clocksmith): This needs to be changed to 24 according to spec.
const RoundSliderOverlayShape({ this.overlayRadius = 16.0 });
static const double _overlayRadius = 16.0;
/// The preferred radius of the round thumb shape when enabled.
///
/// If it is not provided, then half of the track height is used.
final double overlayRadius;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return const Size.fromRadius(_overlayRadius);
return Size.fromRadius(overlayRadius);
}
@override
......@@ -1087,7 +1125,7 @@ class RoundSliderOverlayShape extends SliderComponentShape {
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: 0.0,
end: _overlayRadius,
end: overlayRadius,
);
// TODO(gspencer): We don't really follow the spec here for overlays.
......
......@@ -30,32 +30,15 @@ void main() {
);
final SliderThemeData sliderTheme = theme.sliderTheme;
Widget buildSlider(SliderThemeData data) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Theme(
data: theme,
child: const Slider(
value: 0.5,
label: '0.5',
onChanged: null,
),
),
),
),
),
);
}
await tester.pumpWidget(buildSlider(sliderTheme));
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, enabled: false));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..rect(color: sliderTheme.disabledActiveTrackColor)..rect(color: sliderTheme.disabledInactiveTrackColor));
expect(
sliderBox,
paints
..rect(color: sliderTheme.disabledActiveTrackColor)
..rect(color: sliderTheme.disabledInactiveTrackColor),
);
});
testWidgets('Slider overrides ThemeData theme if SliderTheme present', (WidgetTester tester) async {
......@@ -69,35 +52,15 @@ void main() {
inactiveTrackColor: Colors.purple.withAlpha(0x3d),
);
Widget buildSlider(SliderThemeData data) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Theme(
data: theme,
child: SliderTheme(
data: customTheme,
child: const Slider(
value: 0.5,
label: '0.5',
onChanged: null,
),
),
),
),
),
),
);
}
await tester.pumpWidget(buildSlider(sliderTheme));
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, enabled: false));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..rect(color: customTheme.disabledActiveTrackColor)..rect(color: customTheme.disabledInactiveTrackColor));
expect(
sliderBox,
paints
..rect(color: customTheme.disabledActiveTrackColor)
..rect(color: customTheme.disabledInactiveTrackColor),
);
});
testWidgets('SliderThemeData assigns the correct default shapes', (WidgetTester tester) async {
......@@ -180,33 +143,12 @@ void main() {
primarySwatch: Colors.blue,
);
final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500);
double value = 0.25;
Widget buildApp({ bool enabled = true }) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: SliderTheme(
data: sliderTheme,
child: Slider(
value: value,
label: '$value',
onChanged: onChanged,
),
),
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
// The enabled slider thumb has track segments that extend to and from
// the center of the thumb.
expect(
sliderBox,
paints
......@@ -214,10 +156,15 @@ void main() {
..rect(rect: Rect.fromLTRB(208.0, 299.0, 784.0, 301.0), color: sliderTheme.inactiveTrackColor)
);
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false));
await tester.pumpAndSettle(); // wait for disable animation
// The disabled thumb is smaller so the track has to paint longer to get
// to the edge.
// The disabled slider thumb has a horizontal gap between itself and the
// track segments. Therefore, the track segments are shorter since they do
// not extend to the center of the thumb, but rather the outer edge of th
// gap. As a result, the `right` value of the first segment is less than it
// is above, and the `left` value of the second segment is more than it is
// above.
expect(
sliderBox,
paints
......@@ -232,31 +179,8 @@ void main() {
primarySwatch: Colors.blue,
);
final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500);
double value = 0.25;
Widget buildApp({ bool enabled = true }) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: SliderTheme(
data: sliderTheme,
child: Slider(
value: value,
label: '$value',
onChanged: onChanged,
),
),
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
// With no touch, paints only the thumb.
......@@ -296,6 +220,7 @@ void main() {
await gesture.up();
await tester.pumpAndSettle();
// After the gesture is up and complete, it again paints only the thumb.
expect(
sliderBox,
......@@ -315,45 +240,20 @@ void main() {
primarySwatch: Colors.blue,
);
final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500);
double value = 0.45;
Widget buildApp({
int divisions,
bool enabled = true,
}) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: SliderTheme(
data: sliderTheme,
child: Slider(
value: value,
label: '$value',
divisions: divisions,
onChanged: onChanged,
),
),
),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.45));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(sliderBox, paints..circle(color: sliderTheme.thumbColor, radius: 6.0));
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.45, enabled: false));
await tester.pumpAndSettle(); // wait for disable animation
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor, radius: 4.0));
await tester.pumpWidget(buildApp(divisions: 3));
await tester.pumpAndSettle(); // wait for disable animation
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.45, divisions: 3));
await tester.pumpAndSettle(); // wait for enable animation
expect(
sliderBox,
paints
......@@ -364,8 +264,9 @@ void main() {
..circle(color: sliderTheme.thumbColor, radius: 6.0)
);
await tester.pumpWidget(buildApp(divisions: 3, enabled: false));
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.45, divisions: 3, enabled: false));
await tester.pumpAndSettle(); // wait for disable animation
expect(
sliderBox,
paints
......@@ -548,4 +449,169 @@ void main() {
);
await gesture.up();
});
testWidgets('The slider track height can be overridden', (WidgetTester tester) async {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(trackHeight: 16);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
// Top and bottom are centerY (300) + and - trackRadius (8).
expect(
sliderBox,
paints
..rect(rect: Rect.fromLTRB(16.0, 292.0, 208.0, 308.0), color: sliderTheme.activeTrackColor)
..rect(rect: Rect.fromLTRB(208.0, 292.0, 784.0, 308.0), color: sliderTheme.inactiveTrackColor)
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false));
await tester.pumpAndSettle(); // wait for disable animation
// The disabled thumb is smaller so the active track has to paint longer to
// get to the edge.
expect(
sliderBox,
paints
..rect(rect: Rect.fromLTRB(16.0, 292.0, 202.0, 308.0), color: sliderTheme.disabledActiveTrackColor)
..rect(rect: Rect.fromLTRB(214.0, 292.0, 784.0, 308.0), color: sliderTheme.disabledInactiveTrackColor)
);
});
testWidgets('The default slider thumb shape sizes can be overridden', (WidgetTester tester) async {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 7,
disabledThumbRadius: 11,
),
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints..circle(x: 208, y: 300, radius: 7, color: sliderTheme.thumbColor)
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false));
await tester.pumpAndSettle(); // wait for disable animation
expect(
sliderBox,
paints..circle(x: 208, y: 300, radius: 11, color: sliderTheme.disabledThumbColor)
);
});
testWidgets('The default slider thumb shape disabled size can be inferred from the enabled size', (WidgetTester tester) async {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 9,
),
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints..circle(x: 208, y: 300, radius: 9, color: sliderTheme.thumbColor)
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false));
await tester.pumpAndSettle(); // wait for disable animation
// Radius should be 6, or 2/3 of 9. 2/3 because the default disabled thumb
// radius is 4 and the default enabled thumb radius is 6.
// TODO(clocksmith): This ratio will change once thumb sizes are updated to spec.
expect(
sliderBox,
paints..circle(x: 208, y: 300, radius: 6, color: sliderTheme.disabledThumbColor)
);
});
testWidgets('The default slider tick mark shape size can be overridden', (WidgetTester tester) async {
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
tickMarkShape: const RoundSliderTickMarkShape(
tickMarkRadius: 5
),
activeTickMarkColor: const Color(0xfadedead),
inactiveTickMarkColor: const Color(0xfadebeef),
disabledActiveTickMarkColor: const Color(0xfadecafe),
disabledInactiveTickMarkColor: const Color(0xfadeface),
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, divisions: 2));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints
..circle(x: 21, y: 300, radius: 5, color: sliderTheme.activeTickMarkColor)
..circle(x: 400, y: 300, radius: 5, color: sliderTheme.activeTickMarkColor)
..circle(x: 779, y: 300, radius: 5, color: sliderTheme.inactiveTickMarkColor)
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, divisions: 2, enabled: false));
await tester.pumpAndSettle();
expect(
sliderBox,
paints
..circle(x: 21, y: 300, radius: 5, color: sliderTheme.disabledActiveTickMarkColor)
..circle(x: 400, y: 300, radius: 5, color: sliderTheme.disabledActiveTickMarkColor)
..circle(x: 779, y: 300, radius: 5, color: sliderTheme.disabledInactiveTickMarkColor)
);
});
testWidgets('The default slider overlay shape size can be overridden', (WidgetTester tester) async {
const double uniqueOverlayRadius = 23;
final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(
overlayShape: const RoundSliderOverlayShape(
overlayRadius: uniqueOverlayRadius,
),
);
await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5));
// Tap center and wait for animation.
final Offset center = tester.getCenter(find.byType(Slider));
await tester.startGesture(center);
await tester.pumpAndSettle();
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
paints..circle(
x: center.dx,
y: center.dy,
radius: uniqueOverlayRadius,
color: sliderTheme.overlayColor,
)
);
});
}
Widget _buildApp(
SliderThemeData sliderTheme, {
double value = 0.0,
bool enabled = true,
int divisions,
}) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return MaterialApp(
home: Scaffold(
body: Center(
child: SliderTheme(
data: sliderTheme,
child: Slider(
value: value,
label: '$value',
onChanged: onChanged,
divisions: divisions,
),
),
),
),
);
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment