Add Material 3 support for Slider - Part 1 (#114079)

This commit is contained in:
Taha Tesser 2022-11-02 04:31:00 +02:00 committed by GitHub
parent f10021b378
commit 97d0247d59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 854 additions and 57 deletions

View file

@ -38,6 +38,7 @@ import 'package:gen_defaults/navigation_rail_template.dart';
import 'package:gen_defaults/popup_menu_template.dart';
import 'package:gen_defaults/progress_indicator_template.dart';
import 'package:gen_defaults/radio_template.dart';
import 'package:gen_defaults/slider_template.dart';
import 'package:gen_defaults/surface_tint.dart';
import 'package:gen_defaults/switch_template.dart';
import 'package:gen_defaults/text_field_template.dart';
@ -144,6 +145,7 @@ Future<void> main(List<String> args) async {
PopupMenuTemplate('PopupMenu', '$materialLib/popup_menu.dart', tokens).updateFile();
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();
RadioTemplate('Radio<T>', '$materialLib/radio.dart', tokens).updateFile();
SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile();
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile();

View file

@ -0,0 +1,77 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'template.dart';
class SliderTemplate extends TokenTemplate {
const SliderTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
super.colorSchemePrefix = '_colors.',
});
final String tokenGroup;
@override
String generate() => '''
class _${blockName}DefaultsM3 extends SliderThemeData {
_${blockName}DefaultsM3(this.context)
: _colors = Theme.of(context).colorScheme,
super(trackHeight: ${tokens['$tokenGroup.active.track.height']});
final BuildContext context;
final ColorScheme _colors;
@override
Color? get activeTrackColor => ${componentColor('$tokenGroup.active.track')};
@override
Color? get inactiveTrackColor => ${componentColor('$tokenGroup.inactive.track')};
@override
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
@override
Color? get disabledActiveTrackColor => ${componentColor('$tokenGroup.disabled.active.track')};
@override
Color? get disabledInactiveTrackColor => ${componentColor('$tokenGroup.disabled.inactive.track')};
@override
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);
@override
Color? get activeTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.active.container')};
@override
Color? get inactiveTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.inactive.container')};
@override
Color? get disabledActiveTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.disabled.container')};
@override
Color? get disabledInactiveTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.disabled.container')};
@override
Color? get thumbColor => ${componentColor('$tokenGroup.handle')};
@override
Color? get disabledThumbColor => Color.alphaBlend(${componentColor('$tokenGroup.disabled.handle')}, _colors.surface);
@override
Color? get overlayColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return ${componentColor('$tokenGroup.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('$tokenGroup.focus.state-layer')};
}
if (states.contains(MaterialState.dragged)) {
return ${componentColor('$tokenGroup.pressed.state-layer')};
}
return Colors.transparent;
});
}
''';
}

View file

@ -12,6 +12,8 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:flutter/services.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'material.dart';
@ -374,8 +376,9 @@ class Slider extends StatefulWidget {
/// The "active" side of the slider is the side between the thumb and the
/// minimum value.
///
/// Defaults to [SliderThemeData.activeTrackColor] of the current
/// [SliderTheme].
/// If null, [SliderThemeData.activeTrackColor] of the ambient
/// [SliderTheme] is used. If that is null, [ColorScheme.primary] of the
/// surrounding [ThemeData] is used.
///
/// Using a [SliderTheme] gives much more fine-grained control over the
/// appearance of various components of the slider.
@ -386,8 +389,10 @@ class Slider extends StatefulWidget {
/// The "inactive" side of the slider is the side between the thumb and the
/// maximum value.
///
/// Defaults to the [SliderThemeData.inactiveTrackColor] of the current
/// [SliderTheme].
/// If null, [SliderThemeData.inactiveTrackColor] of the ambient [SliderTheme]
/// is used. If that is null and [ThemeData.useMaterial3] is true,
/// [ColorScheme.surfaceVariant] will be used, otherwise [ColorScheme.primary]
/// with an opacity of 0.24 will be used.
///
/// Using a [SliderTheme] gives much more fine-grained control over the
/// appearance of various components of the slider.
@ -401,6 +406,9 @@ class Slider extends StatefulWidget {
/// Defaults to the [SliderThemeData.secondaryActiveTrackColor] of the current
/// [SliderTheme].
///
/// If that is also null, defaults to [ColorScheme.primary] with an
/// opacity of 0.54.
///
/// Using a [SliderTheme] gives much more fine-grained control over the
/// appearance of various components of the slider.
///
@ -409,19 +417,27 @@ class Slider extends StatefulWidget {
/// The color of the thumb.
///
/// If this color is null:
/// * [Slider] will use [activeColor].
/// If this color is null, [Slider] will use [activeColor], If [activeColor]
/// is also null, [Slider] will use [SliderThemeData.thumbColor].
///
/// If that is also null, defaults to [ColorScheme.primary].
///
/// * [CupertinoSlider] will have a white thumb
/// (like the native default iOS slider).
final Color? thumbColor;
/// The highlight color that's typically used to indicate that
/// the slider is focused, hovered, or dragged.
/// the slider thumb is focused, hovered, or dragged.
///
/// If this property is null, [Slider] will use [activeColor] with
/// with an opacity of 0.12, If null, [SliderThemeData.overlayColor]
/// will be used, If this is also null, defaults to [ColorScheme.primary]
/// with an opacity of 0.12.
/// will be used.
///
/// If that is also null, If [ThemeData.useMaterial3] is true,
/// Slider will use [ColorScheme.primary] with an opacity of 0.08 when
/// slider thumb is hovered and with an opacity of 0.12 when slider thumb
/// is focused or dragged, If [ThemeData.useMaterial3] is false, defaults
/// to [ColorScheme.primary] with an opacity of 0.12.
final MaterialStateProperty<Color?>? overlayColor;
/// {@template flutter.material.slider.mouseCursor}
@ -730,6 +746,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
Widget _buildMaterialSlider(BuildContext context) {
final ThemeData theme = Theme.of(context);
SliderThemeData sliderTheme = SliderTheme.of(context);
final SliderThemeData defaults = theme.useMaterial3 ? _SliderDefaultsM3(context) : _SliderDefaultsM2(context);
// If the widget has active or inactive colors specified, then we plug them
// in to the slider theme as best we can. If the developer wants more
@ -738,7 +755,6 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
// the default shapes and text styles are aligned to the Material
// Guidelines.
const double defaultTrackHeight = 4;
const SliderTrackShape defaultTrackShape = RoundedRectSliderTrackShape();
const SliderTickMarkShape defaultTickMarkShape = RoundSliderTickMarkShape();
const SliderComponentShape defaultOverlayShape = RoundSliderOverlayShape();
@ -769,23 +785,23 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
return widget.overlayColor?.resolve(states)
?? widget.activeColor?.withOpacity(0.12)
?? MaterialStateProperty.resolveAs<Color?>(sliderTheme.overlayColor, states)
?? theme.colorScheme.primary.withOpacity(0.12);
?? MaterialStateProperty.resolveAs<Color?>(defaults.overlayColor, states);
}
sliderTheme = sliderTheme.copyWith(
trackHeight: sliderTheme.trackHeight ?? defaultTrackHeight,
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
inactiveTrackColor: widget.inactiveColor ?? sliderTheme.inactiveTrackColor ?? theme.colorScheme.primary.withOpacity(0.24),
secondaryActiveTrackColor: widget.secondaryActiveColor ?? sliderTheme.secondaryActiveTrackColor ?? theme.colorScheme.primary.withOpacity(0.54),
disabledActiveTrackColor: sliderTheme.disabledActiveTrackColor ?? theme.colorScheme.onSurface.withOpacity(0.32),
disabledInactiveTrackColor: sliderTheme.disabledInactiveTrackColor ?? theme.colorScheme.onSurface.withOpacity(0.12),
disabledSecondaryActiveTrackColor: sliderTheme.disabledSecondaryActiveTrackColor ?? theme.colorScheme.onSurface.withOpacity(0.12),
activeTickMarkColor: widget.inactiveColor ?? sliderTheme.activeTickMarkColor ?? theme.colorScheme.onPrimary.withOpacity(0.54),
inactiveTickMarkColor: widget.activeColor ?? sliderTheme.inactiveTickMarkColor ?? theme.colorScheme.primary.withOpacity(0.54),
disabledActiveTickMarkColor: sliderTheme.disabledActiveTickMarkColor ?? theme.colorScheme.onPrimary.withOpacity(0.12),
disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? theme.colorScheme.onSurface.withOpacity(0.12),
thumbColor: widget.thumbColor ?? widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary,
disabledThumbColor: sliderTheme.disabledThumbColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(.38), theme.colorScheme.surface),
trackHeight: sliderTheme.trackHeight ?? defaults.trackHeight,
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? defaults.activeTrackColor,
inactiveTrackColor: widget.inactiveColor ?? sliderTheme.inactiveTrackColor ?? defaults.inactiveTrackColor,
secondaryActiveTrackColor: widget.secondaryActiveColor ?? sliderTheme.secondaryActiveTrackColor ?? defaults.secondaryActiveTrackColor,
disabledActiveTrackColor: sliderTheme.disabledActiveTrackColor ?? defaults.disabledActiveTrackColor,
disabledInactiveTrackColor: sliderTheme.disabledInactiveTrackColor ?? defaults.disabledInactiveTrackColor,
disabledSecondaryActiveTrackColor: sliderTheme.disabledSecondaryActiveTrackColor ?? defaults.disabledSecondaryActiveTrackColor,
activeTickMarkColor: widget.inactiveColor ?? sliderTheme.activeTickMarkColor ?? defaults.activeTickMarkColor,
inactiveTickMarkColor: widget.activeColor ?? sliderTheme.inactiveTickMarkColor ?? defaults.inactiveTickMarkColor,
disabledActiveTickMarkColor: sliderTheme.disabledActiveTickMarkColor ?? defaults.disabledActiveTickMarkColor,
disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? defaults.disabledInactiveTickMarkColor,
thumbColor: widget.thumbColor ?? widget.activeColor ?? sliderTheme.thumbColor ?? defaults.thumbColor,
disabledThumbColor: sliderTheme.disabledThumbColor ?? defaults.disabledThumbColor,
overlayColor: effectiveOverlayColor(),
valueIndicatorColor: valueIndicatorColor,
trackShape: sliderTheme.trackShape ?? defaultTrackShape,
@ -1795,3 +1811,122 @@ class _RenderValueIndicator extends RenderBox with RelayoutWhenSystemFontsChange
return constraints.smallest;
}
}
class _SliderDefaultsM2 extends SliderThemeData {
_SliderDefaultsM2(this.context)
: _colors = Theme.of(context).colorScheme,
super(trackHeight: 4.0);
final BuildContext context;
final ColorScheme _colors;
@override
Color? get activeTrackColor => _colors.primary;
@override
Color? get inactiveTrackColor => _colors.primary.withOpacity(0.24);
@override
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
@override
Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.32);
@override
Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);
@override
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);
@override
Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(0.54);
@override
Color? get inactiveTickMarkColor => _colors.primary.withOpacity(0.54);
@override
Color? get disabledActiveTickMarkColor => _colors.onPrimary.withOpacity(0.12);
@override
Color? get disabledInactiveTickMarkColor => _colors.onSurface.withOpacity(0.12);
@override
Color? get thumbColor => _colors.primary;
@override
Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.withOpacity(.38), _colors.surface);
@override
Color? get overlayColor => _colors.primary.withOpacity(0.12);
}
// BEGIN GENERATED TOKEN PROPERTIES - Slider
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_137
class _SliderDefaultsM3 extends SliderThemeData {
_SliderDefaultsM3(this.context)
: _colors = Theme.of(context).colorScheme,
super(trackHeight: 4.0);
final BuildContext context;
final ColorScheme _colors;
@override
Color? get activeTrackColor => _colors.primary;
@override
Color? get inactiveTrackColor => _colors.surfaceVariant;
@override
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
@override
Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38);
@override
Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);
@override
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);
@override
Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(0.38);
@override
Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.withOpacity(0.38);
@override
Color? get disabledActiveTickMarkColor => _colors.onSurface.withOpacity(0.38);
@override
Color? get disabledInactiveTickMarkColor => _colors.onSurface.withOpacity(0.38);
@override
Color? get thumbColor => _colors.primary;
@override
Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.withOpacity(0.38), _colors.surface);
@override
Color? get overlayColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return _colors.primary.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.primary.withOpacity(0.12);
}
if (states.contains(MaterialState.dragged)) {
return _colors.primary.withOpacity(0.12);
}
return Colors.transparent;
});
}
// END GENERATED TOKEN PROPERTIES - Slider

View file

@ -1710,28 +1710,25 @@ void main() {
testWidgets('Slider is focusable and has correct focus color', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Slider');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final ThemeData theme = ThemeData(useMaterial3: true);
double value = 0.5;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return SliderTheme(
data: SliderThemeData(
overlayColor: Colors.orange[500],
),
child: Slider(
value: value,
onChanged: enabled
? (double newValue) {
setState(() {
value = newValue;
});
}
: null,
autofocus: true,
focusNode: focusNode,
),
return Slider(
value: value,
onChanged: enabled
? (double newValue) {
setState(() {
value = newValue;
});
}
: null,
autofocus: true,
focusNode: focusNode,
);
}),
),
@ -1745,7 +1742,7 @@ void main() {
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Slider))),
paints..circle(color: Colors.orange[500]),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
// Check that the overlay does not show when unfocused and disabled.
@ -1754,7 +1751,7 @@ void main() {
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
Material.of(tester.element(find.byType(Slider))),
isNot(paints..circle(color: Colors.orange[500])),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
});
@ -1813,26 +1810,23 @@ void main() {
testWidgets('Slider can be hovered and has correct hover color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final ThemeData theme = ThemeData(useMaterial3: true);
double value = 0.5;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return SliderTheme(
data: SliderThemeData(
overlayColor: Colors.orange[500],
),
child: Slider(
value: value,
onChanged: enabled
? (double newValue) {
setState(() {
value = newValue;
});
}
: null,
),
return Slider(
value: value,
onChanged: enabled
? (double newValue) {
setState(() {
value = newValue;
});
}
: null,
);
}),
),
@ -1858,7 +1852,7 @@ void main() {
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Slider))),
paints..circle(color: Colors.orange[500]),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.08)),
);
// Slider does not have an overlay when disabled and hovered.
@ -1931,6 +1925,67 @@ void main() {
);
});
testWidgets('Slider is draggable and has correct dragged color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
double value = 0.5;
final ThemeData theme = ThemeData(useMaterial3: true);
final Key sliderKey = UniqueKey();
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Slider(
value: value,
key: sliderKey,
onChanged: enabled
? (double newValue) {
setState(() {
value = newValue;
});
}
: null,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
// Slider does not have overlay when enabled and not dragged.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Slider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
// Start dragging.
final TestGesture drag = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
await tester.pump(kPressTimeout);
// Less than configured touch slop, more than default touch slop
await drag.moveBy(const Offset(19.0, 0));
await tester.pump();
// Slider has overlay when enabled and dragged.
expect(
Material.of(tester.element(find.byType(Slider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
await drag.up();
await tester.pumpAndSettle();
// Slider still has overlay when stopped dragging.
expect(
Material.of(tester.element(find.byType(Slider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
});
testWidgets('Slider has correct dragged color from overlayColor property', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
double value = 0.5;
@ -3381,4 +3436,171 @@ void main() {
isNot(paints..path(color: const Color(0xff000000))..paragraph()),
);
}, variant: TargetPlatformVariant.desktop());
group('Material 2', () {
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
// is turned on by default, these tests can be removed.
testWidgets('Slider can be hovered and has correct hover color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final ThemeData theme = ThemeData();
double value = 0.5;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Slider(
value: value,
onChanged: enabled
? (double newValue) {
setState(() {
value = newValue;
});
}
: null,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
// Slider does not have overlay when enabled and not hovered.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Slider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
// Start hovering.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Slider)));
// Slider has overlay when enabled and hovered.
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Slider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
// Slider does not have an overlay when disabled and hovered.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Slider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
});
testWidgets('Slider is focusable and has correct focus color', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Slider');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final ThemeData theme = ThemeData();
double value = 0.5;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Slider(
value: value,
onChanged: enabled
? (double newValue) {
setState(() {
value = newValue;
});
}
: null,
autofocus: true,
focusNode: focusNode,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
// Check that the overlay shows when focused.
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Slider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
// Check that the overlay does not show when unfocused and disabled.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
Material.of(tester.element(find.byType(Slider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
});
testWidgets('Slider is draggable and has correct dragged color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
double value = 0.5;
final ThemeData theme = ThemeData();
final Key sliderKey = UniqueKey();
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Slider(
value: value,
key: sliderKey,
onChanged: enabled
? (double newValue) {
setState(() {
value = newValue;
});
}
: null,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
// Slider does not have overlay when enabled and not dragged.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Slider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
// Start dragging.
final TestGesture drag = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
await tester.pump(kPressTimeout);
// Less than configured touch slop, more than default touch slop
await drag.moveBy(const Offset(19.0, 0));
await tester.pump();
// Slider has overlay when enabled and dragged.
expect(
Material.of(tester.element(find.byType(Slider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
await drag.up();
await tester.pumpAndSettle();
// Slider still has overlay when stopped dragging.
expect(
Material.of(tester.element(find.byType(Slider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
});
});
}

View file

@ -97,6 +97,137 @@ void main() {
]);
});
testWidgets('Slider defaults', (WidgetTester tester) async {
debugDisableShadows = false;
final ThemeData theme = ThemeData(useMaterial3: true);
final ColorScheme colorScheme = theme.colorScheme;
const double trackHeight = 4.0;
final Color activeTrackColor = Color(colorScheme.primary.value);
final Color inactiveTrackColor = colorScheme.surfaceVariant;
final Color secondaryActiveTrackColor = colorScheme.primary.withOpacity(0.54);
final Color disabledActiveTrackColor = colorScheme.onSurface.withOpacity(0.38);
final Color disabledInactiveTrackColor = colorScheme.onSurface.withOpacity(0.12);
final Color disabledSecondaryActiveTrackColor = colorScheme.onSurface.withOpacity(0.12);
final Color shadowColor = colorScheme.shadow;
final Color thumbColor = Color(colorScheme.primary.value);
final Color disabledThumbColor = Color.alphaBlend(colorScheme.onSurface.withOpacity(0.38), colorScheme.surface);
final Color activeTickMarkColor = colorScheme.onPrimary.withOpacity(0.38);
final Color inactiveTickMarkColor = colorScheme.onSurfaceVariant.withOpacity(0.38);
final Color disabledActiveTickMarkColor = colorScheme.onSurface.withOpacity(0.38);
final Color disabledInactiveTickMarkColor = colorScheme.onSurface.withOpacity(0.38);
try {
double value = 0.45;
Widget buildApp({
int? divisions,
bool enabled = true,
}) {
final ValueChanged<double>? onChanged = !enabled
? null
: (double d) {
value = d;
};
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: Theme(
data: theme,
child: Slider(
value: value,
secondaryTrackValue: 0.75,
label: '$value',
divisions: divisions,
onChanged: onChanged,
),
),
),
),
),
);
}
await tester.pumpWidget(buildApp());
final MaterialInkController material = Material.of(tester.element(find.byType(Slider)));
// Test default track height.
const Radius radius = Radius.circular(trackHeight / 2);
const Radius activatedRadius = Radius.circular((trackHeight + 2) / 2);
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),
);
// Test default colors for enabled slider.
expect(material, paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor)..rrect(color: secondaryActiveTrackColor));
expect(material, paints..shadow(color: shadowColor));
expect(material, paints..circle(color: thumbColor));
expect(material, isNot(paints..circle(color: disabledThumbColor)));
expect(material, isNot(paints..rrect(color: disabledActiveTrackColor)));
expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor)));
expect(material, isNot(paints..rrect(color: disabledSecondaryActiveTrackColor)));
expect(material, isNot(paints..circle(color: activeTickMarkColor)));
expect(material, isNot(paints..circle(color: inactiveTickMarkColor)));
// 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
..circle(color: activeTickMarkColor)
..circle(color: activeTickMarkColor)
..circle(color: inactiveTickMarkColor)
..circle(color: inactiveTickMarkColor)
..shadow(color: Colors.black)
..circle(color: thumbColor),
);
expect(material, isNot(paints..circle(color: disabledThumbColor)));
expect(material, isNot(paints..rrect(color: disabledActiveTrackColor)));
expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor)));
expect(material, isNot(paints..rrect(color: disabledSecondaryActiveTrackColor)));
// Test defaults colors for disabled slider.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
material,
paints
..rrect(color: disabledActiveTrackColor)
..rrect(color: disabledInactiveTrackColor)
..rrect(color: disabledSecondaryActiveTrackColor),
);
expect(material, paints..shadow(color: shadowColor)..circle(color: disabledThumbColor));
expect(material, isNot(paints..circle(color: thumbColor)));
expect(material, isNot(paints..rrect(color: activeTrackColor)));
expect(material, isNot(paints..rrect(color: inactiveTrackColor)));
expect(material, isNot(paints..rrect(color: secondaryActiveTrackColor)));
// Test defaults colors for disabled discrete slider.
await tester.pumpWidget(buildApp(divisions: 3, enabled: false));
expect(
material,
paints
..circle(color: disabledActiveTickMarkColor)
..circle(color: disabledActiveTickMarkColor)
..circle(color: disabledInactiveTickMarkColor)
..circle(color: disabledInactiveTickMarkColor)
..shadow(color: shadowColor)
..circle(color: disabledThumbColor),
);
expect(material, isNot(paints..circle(color: thumbColor)));
expect(material, isNot(paints..rrect(color: activeTrackColor)));
expect(material, isNot(paints..rrect(color: inactiveTrackColor)));
expect(material, isNot(paints..rrect(color: secondaryActiveTrackColor)));
} finally {
debugDisableShadows = true;
}
});
testWidgets('Slider uses the right theme colors for the right components', (WidgetTester tester) async {
debugDisableShadows = false;
try {
@ -279,6 +410,25 @@ void main() {
expect(material, isNot(paints..rrect(color: sliderTheme.inactiveTrackColor)));
expect(material, isNot(paints..rrect(color: sliderTheme.secondaryActiveTrackColor)));
// Test default theme for disabled discrete widget.
await tester.pumpWidget(buildApp(divisions: 3, enabled: false));
expect(
material,
paints
..circle(color: sliderTheme.disabledActiveTickMarkColor)
..circle(color: sliderTheme.disabledActiveTickMarkColor)
..circle(color: sliderTheme.disabledInactiveTickMarkColor)
..circle(color: sliderTheme.disabledInactiveTickMarkColor)
..shadow(color: Colors.black)
..circle(color: sliderTheme.disabledThumbColor),
);
expect(material, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(material, isNot(paints..rrect(color: sliderTheme.activeTrackColor)));
expect(material, isNot(paints..rrect(color: sliderTheme.inactiveTrackColor)));
expect(material, isNot(paints..rrect(color: sliderTheme.secondaryActiveTrackColor)));
expect(material, isNot(paints..circle(color: sliderTheme.activeTickMarkColor)));
expect(material, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
// Test setting the activeColor, inactiveColor and secondaryActiveColor for disabled widget.
await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2, secondaryActiveColor: customColor3, enabled: false));
expect(
@ -343,6 +493,60 @@ void main() {
}
});
testWidgets('Slider parameters overrides theme properties', (WidgetTester tester) async {
debugDisableShadows = false;
const Color activeTrackColor = Color(0xffff0001);
const Color inactiveTrackColor = Color(0xffff0002);
const Color secondaryActiveTrackColor = Color(0xffff0003);
const Color thumbColor = Color(0xffff0004);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
activeTrackColor: Color(0xff000001),
inactiveTickMarkColor: Color(0xff000002),
secondaryActiveTrackColor: Color(0xff000003),
thumbColor: Color(0xff000004),
),
);
try {
const double value = 0.45;
Widget buildApp({ bool enabled = true }) {
return MaterialApp(
theme: theme,
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: Slider(
activeColor: activeTrackColor,
inactiveColor: inactiveTrackColor,
secondaryActiveColor: secondaryActiveTrackColor,
thumbColor: thumbColor,
value: value,
secondaryTrackValue: 0.75,
label: '$value',
onChanged: (double value) { },
),
),
),
),
);
}
await tester.pumpWidget(buildApp());
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..circle(color: thumbColor));
} finally {
debugDisableShadows = true;
}
});
testWidgets('Slider uses ThemeData slider theme if present', (WidgetTester tester) async {
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
@ -1652,6 +1856,161 @@ void main() {
await tester.pumpAndSettle();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
});
group('Material 2', () {
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
// is turned on by default, these tests can be removed.
testWidgets('Slider defaults', (WidgetTester tester) async {
debugDisableShadows = false;
final ThemeData theme = ThemeData();
const double trackHeight = 4.0;
final ColorScheme colorScheme = theme.colorScheme;
final Color activeTrackColor = Color(colorScheme.primary.value);
final Color inactiveTrackColor = colorScheme.primary.withOpacity(0.24);
final Color secondaryActiveTrackColor = colorScheme.primary.withOpacity(0.54);
final Color disabledActiveTrackColor = colorScheme.onSurface.withOpacity(0.32);
final Color disabledInactiveTrackColor = colorScheme.onSurface.withOpacity(0.12);
final Color disabledSecondaryActiveTrackColor = colorScheme.onSurface.withOpacity(0.12);
final Color shadowColor = colorScheme.shadow;
final Color thumbColor = Color(colorScheme.primary.value);
final Color disabledThumbColor = Color.alphaBlend(colorScheme.onSurface.withOpacity(.38), colorScheme.surface);
final Color activeTickMarkColor = colorScheme.onPrimary.withOpacity(0.54);
final Color inactiveTickMarkColor = colorScheme.primary.withOpacity(0.54);
final Color disabledActiveTickMarkColor = colorScheme.onPrimary.withOpacity(0.12);
final Color disabledInactiveTickMarkColor = colorScheme.onSurface.withOpacity(0.12);
final Color valueIndicatorColor = Color.alphaBlend(colorScheme.onSurface.withOpacity(0.60), colorScheme.surface.withOpacity(0.90));
try {
double value = 0.45;
Widget buildApp({
int? divisions,
bool enabled = true,
}) {
final ValueChanged<double>? onChanged = !enabled
? null
: (double d) {
value = d;
};
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: Slider(
value: value,
secondaryTrackValue: 0.75,
label: '$value',
divisions: divisions,
onChanged: onChanged,
),
),
),
),
);
}
await tester.pumpWidget(buildApp());
final MaterialInkController material = Material.of(tester.element(find.byType(Slider)));
final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay));
// Test default track height.
const Radius radius = Radius.circular(trackHeight / 2);
const Radius activatedRadius = Radius.circular((trackHeight + 2) / 2);
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),
);
// Test default colors for enabled slider.
expect(material, paints..rrect(color: activeTrackColor)..rrect(color: inactiveTrackColor)..rrect(color: secondaryActiveTrackColor));
expect(material, paints..shadow(color: shadowColor));
expect(material, paints..circle(color: thumbColor));
expect(material, isNot(paints..circle(color: disabledThumbColor)));
expect(material, isNot(paints..rrect(color: disabledActiveTrackColor)));
expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor)));
expect(material, isNot(paints..rrect(color: disabledSecondaryActiveTrackColor)));
expect(material, isNot(paints..circle(color: activeTickMarkColor)));
expect(material, isNot(paints..circle(color: inactiveTickMarkColor)));
// 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
..circle(color: activeTickMarkColor)
..circle(color: activeTickMarkColor)
..circle(color: inactiveTickMarkColor)
..circle(color: inactiveTickMarkColor)
..shadow(color: Colors.black)
..circle(color: thumbColor),
);
expect(material, isNot(paints..circle(color: disabledThumbColor)));
expect(material, isNot(paints..rrect(color: disabledActiveTrackColor)));
expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor)));
expect(material, isNot(paints..rrect(color: disabledSecondaryActiveTrackColor)));
// Test defaults colors for disabled slider.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
material,
paints
..rrect(color: disabledActiveTrackColor)
..rrect(color: disabledInactiveTrackColor)
..rrect(color: disabledSecondaryActiveTrackColor),
);
expect(material, paints..shadow(color: Colors.black)..circle(color: disabledThumbColor));
expect(material, isNot(paints..circle(color: thumbColor)));
expect(material, isNot(paints..rrect(color: activeTrackColor)));
expect(material, isNot(paints..rrect(color: inactiveTrackColor)));
expect(material, isNot(paints..rrect(color: secondaryActiveTrackColor)));
// Test defaults colors for disabled discrete slider.
await tester.pumpWidget(buildApp(divisions: 3, enabled: false));
expect(
material,
paints
..circle(color: disabledActiveTickMarkColor)
..circle(color: disabledActiveTickMarkColor)
..circle(color: disabledInactiveTickMarkColor)
..circle(color: disabledInactiveTickMarkColor)
..shadow(color: Colors.black)
..circle(color: disabledThumbColor),
);
expect(material, isNot(paints..circle(color: thumbColor)));
expect(material, isNot(paints..rrect(color: activeTrackColor)));
expect(material, isNot(paints..rrect(color: inactiveTrackColor)));
expect(material, isNot(paints..rrect(color: secondaryActiveTrackColor)));
expect(material, isNot(paints..circle(color: activeTickMarkColor)));
expect(material, isNot(paints..circle(color: inactiveTickMarkColor)));
// Test the default color for value indicator.
await tester.pumpWidget(buildApp(divisions: 3));
final Offset center = tester.getCenter(find.byType(Slider));
final TestGesture gesture = await tester.startGesture(center);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(value, equals(2.0 / 3.0));
expect(
valueIndicatorBox,
paints
..path(color: valueIndicatorColor)
..paragraph(),
);
await gesture.up();
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
} finally {
debugDisableShadows = true;
}
});
});
}
class RoundedRectSliderTrackShapeWithCustomAdditionalActiveTrackHeight extends RoundedRectSliderTrackShape {
@ -1681,6 +2040,7 @@ Widget _buildApp(
double? secondaryTrackValue,
bool enabled = true,
int? divisions,
FocusNode? focusNode,
}) {
final ValueChanged<double>? onChanged = enabled ? (double d) => value = d : null;
return MaterialApp(
@ -1694,6 +2054,7 @@ Widget _buildApp(
label: '$value',
onChanged: onChanged,
divisions: divisions,
focusNode: focusNode,
),
),
),