mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
2400 lines
80 KiB
Dart
2400 lines
80 KiB
Dart
// 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.
|
|
|
|
// @dart = 2.8
|
|
|
|
import 'dart:ui';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../rendering/mock_canvas.dart';
|
|
import '../widgets/semantics_tester.dart';
|
|
|
|
// A thumb shape that also logs its repaint center.
|
|
class LoggingThumbShape extends SliderComponentShape {
|
|
LoggingThumbShape(this.log);
|
|
|
|
final List<Offset> log;
|
|
|
|
@override
|
|
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
|
return const Size(10.0, 10.0);
|
|
}
|
|
|
|
@override
|
|
void paint(
|
|
PaintingContext context,
|
|
Offset thumbCenter, {
|
|
Animation<double> activationAnimation,
|
|
Animation<double> enableAnimation,
|
|
bool isEnabled,
|
|
bool isDiscrete,
|
|
bool onActiveTrack,
|
|
TextPainter labelPainter,
|
|
RenderBox parentBox,
|
|
SliderThemeData sliderTheme,
|
|
TextDirection textDirection,
|
|
double value,
|
|
double textScaleFactor,
|
|
Size sizeWithOverflow,
|
|
}) {
|
|
log.add(thumbCenter);
|
|
final Paint thumbPaint = Paint()..color = Colors.red;
|
|
context.canvas.drawCircle(thumbCenter, 5.0, thumbPaint);
|
|
}
|
|
}
|
|
|
|
class TallSliderTickMarkShape extends SliderTickMarkShape {
|
|
@override
|
|
Size getPreferredSize({SliderThemeData sliderTheme, bool isEnabled}) {
|
|
return const Size(10.0, 200.0);
|
|
}
|
|
|
|
@override
|
|
void paint(
|
|
PaintingContext context,
|
|
Offset offset, {
|
|
Offset thumbCenter,
|
|
RenderBox parentBox,
|
|
SliderThemeData sliderTheme,
|
|
Animation<double> enableAnimation,
|
|
bool isEnabled,
|
|
TextDirection textDirection,
|
|
}) {
|
|
final Paint paint = Paint()..color = Colors.red;
|
|
context.canvas.drawRect(Rect.fromLTWH(offset.dx, offset.dy, 10.0, 20.0), paint);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
double startValue;
|
|
double endValue;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
onChangeStart: (double value) {
|
|
startValue = value;
|
|
},
|
|
onChangeEnd: (double value) {
|
|
endValue = value;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(value, equals(0.0));
|
|
await tester.tap(find.byKey(sliderKey));
|
|
expect(value, equals(0.5));
|
|
expect(startValue, equals(0.0));
|
|
expect(endValue, equals(0.5));
|
|
startValue = null;
|
|
endValue = null;
|
|
await tester.pump(); // No animation should start.
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
|
|
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
|
|
final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey));
|
|
|
|
final Offset target = topLeft + (bottomRight - topLeft) / 4.0;
|
|
await tester.tapAt(target);
|
|
expect(value, closeTo(0.25, 0.05));
|
|
expect(startValue, equals(0.5));
|
|
expect(endValue, closeTo(0.25, 0.05));
|
|
await tester.pump(); // No animation should start.
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
});
|
|
|
|
testWidgets('Slider can move when tapped (RTL)', (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(value, equals(0.0));
|
|
await tester.tap(find.byKey(sliderKey));
|
|
expect(value, equals(0.5));
|
|
await tester.pump(); // No animation should start.
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
|
|
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
|
|
final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey));
|
|
|
|
final Offset target = topLeft + (bottomRight - topLeft) / 4.0;
|
|
await tester.tapAt(target);
|
|
expect(value, closeTo(0.75, 0.05));
|
|
await tester.pump(); // No animation should start.
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
});
|
|
|
|
testWidgets("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
double startValue;
|
|
double endValue;
|
|
int updates = 0;
|
|
int startValueUpdates = 0;
|
|
int endValueUpdates = 0;
|
|
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
updates++;
|
|
value = newValue;
|
|
});
|
|
},
|
|
onChangeStart: (double value) {
|
|
startValueUpdates++;
|
|
startValue = value;
|
|
},
|
|
onChangeEnd: (double value) {
|
|
endValueUpdates++;
|
|
endValue = value;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(value, equals(0.0));
|
|
await tester.tap(find.byKey(sliderKey));
|
|
expect(value, equals(0.5));
|
|
expect(startValue, equals(0.0));
|
|
expect(endValue, equals(0.5));
|
|
await tester.pump();
|
|
await tester.tap(find.byKey(sliderKey));
|
|
expect(value, equals(0.5));
|
|
await tester.pump();
|
|
expect(updates, equals(1));
|
|
expect(startValueUpdates, equals(2));
|
|
expect(endValueUpdates, equals(2));
|
|
});
|
|
|
|
testWidgets('Value indicator shows for a bit after being tapped', (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
divisions: 4,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(value, equals(0.0));
|
|
await tester.tap(find.byKey(sliderKey));
|
|
expect(value, equals(0.5));
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
// Starts with the position animation and value indicator
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
// Value indicator is longer than position.
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(1));
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
// Shown for long enough, value indicator is animated closed.
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(1));
|
|
await tester.pump(const Duration(milliseconds: 101));
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
});
|
|
|
|
testWidgets('Discrete Slider repaints and animates when dragged', (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
final List<Offset> log = <Offset>[];
|
|
final LoggingThumbShape loggingThumb = LoggingThumbShape(log);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
final SliderThemeData sliderTheme = SliderTheme.of(context).copyWith(thumbShape: loggingThumb);
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: SliderTheme(
|
|
data: sliderTheme,
|
|
child: Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
divisions: 4,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final List<Offset> expectedLog = <Offset>[
|
|
const Offset(24.0, 300.0),
|
|
const Offset(24.0, 300.0),
|
|
const Offset(400.0, 300.0),
|
|
];
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(value, equals(0.5));
|
|
expect(log.length, 3);
|
|
expect(log, orderedEquals(expectedLog));
|
|
await gesture.moveBy(const Offset(-500.0, 0.0));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
expect(value, equals(0.0));
|
|
expect(log.length, 5);
|
|
expect(log.last.dx, closeTo(386.6, 0.1));
|
|
// With no more gesture or value changes, the thumb position should still
|
|
// be redrawn in the animated position.
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
expect(value, equals(0.0));
|
|
expect(log.length, 7);
|
|
expect(log.last.dx, closeTo(344.5, 0.1));
|
|
// Final position.
|
|
await tester.pump(const Duration(milliseconds: 80));
|
|
expectedLog.add(const Offset(24.0, 300.0));
|
|
expect(value, equals(0.0));
|
|
expect(log.length, 8);
|
|
expect(log.last.dx, closeTo(24.0, 0.1));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
int updates = 0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
updates++;
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(value, equals(0.0));
|
|
await tester.tap(find.byKey(sliderKey));
|
|
expect(value, equals(0.5));
|
|
await tester.pump();
|
|
await tester.tap(find.byKey(sliderKey));
|
|
expect(value, equals(0.5));
|
|
await tester.pump();
|
|
expect(updates, equals(1));
|
|
});
|
|
|
|
testWidgets('discrete Slider repaints when dragged', (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
final List<Offset> log = <Offset>[];
|
|
final LoggingThumbShape loggingThumb = LoggingThumbShape(log);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
final SliderThemeData sliderTheme = SliderTheme.of(context).copyWith(thumbShape: loggingThumb);
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: SliderTheme(
|
|
data: sliderTheme,
|
|
child: Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
divisions: 4,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final List<Offset> expectedLog = <Offset>[
|
|
const Offset(24.0, 300.0),
|
|
const Offset(24.0, 300.0),
|
|
const Offset(400.0, 300.0),
|
|
];
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(value, equals(0.5));
|
|
expect(log.length, 3);
|
|
expect(log, orderedEquals(expectedLog));
|
|
await gesture.moveBy(const Offset(-500.0, 0.0));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
expect(value, equals(0.0));
|
|
expect(log.length, 5);
|
|
expect(log.last.dx, closeTo(386.6, 0.1));
|
|
// With no more gesture or value changes, the thumb position should still
|
|
// be redrawn in the animated position.
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
expect(value, equals(0.0));
|
|
expect(log.length, 7);
|
|
expect(log.last.dx, closeTo(344.5, 0.1));
|
|
// Final position.
|
|
await tester.pump(const Duration(milliseconds: 80));
|
|
expectedLog.add(const Offset(24.0, 300.0));
|
|
expect(value, equals(0.0));
|
|
expect(log.length, 8);
|
|
expect(log.last.dx, closeTo(24.0, 0.1));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('Slider take on discrete values', (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 144.0 + 2 * 16.0, // _kPreferredTotalWidth
|
|
child: Slider(
|
|
key: sliderKey,
|
|
min: 0.0,
|
|
max: 100.0,
|
|
divisions: 10,
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(value, equals(0.0));
|
|
await tester.tap(find.byKey(sliderKey));
|
|
expect(value, equals(50.0));
|
|
await tester.drag(find.byKey(sliderKey), const Offset(5.0, 0.0));
|
|
expect(value, equals(50.0));
|
|
await tester.drag(find.byKey(sliderKey), const Offset(40.0, 0.0));
|
|
expect(value, equals(80.0));
|
|
|
|
await tester.pump(); // Starts animation.
|
|
expect(SchedulerBinding.instance.transientCallbackCount, greaterThan(0));
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
// Animation complete.
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
});
|
|
|
|
testWidgets('Slider can be given zero values', (WidgetTester tester) async {
|
|
final List<double> log = <double>[];
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Slider(
|
|
value: 0.0,
|
|
min: 0.0,
|
|
max: 1.0,
|
|
onChanged: (double newValue) {
|
|
log.add(newValue);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(Slider));
|
|
expect(log, <double>[0.5]);
|
|
log.clear();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Slider(
|
|
value: 0.0,
|
|
min: 0.0,
|
|
max: 0.0,
|
|
onChanged: (double newValue) {
|
|
log.add(newValue);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(Slider));
|
|
expect(log, <double>[]);
|
|
log.clear();
|
|
});
|
|
|
|
testWidgets('Slider uses the right theme colors for the right components', (WidgetTester tester) async {
|
|
const Color customColor1 = Color(0xcafefeed);
|
|
const Color customColor2 = Color(0xdeadbeef);
|
|
final ThemeData theme = ThemeData(
|
|
platform: TargetPlatform.android,
|
|
primarySwatch: Colors.blue,
|
|
sliderTheme: const SliderThemeData(
|
|
disabledThumbColor: Color(0xff000001),
|
|
disabledActiveTickMarkColor: Color(0xff000002),
|
|
disabledActiveTrackColor: Color(0xff000003),
|
|
disabledInactiveTickMarkColor: Color(0xff000004),
|
|
disabledInactiveTrackColor: Color(0xff000005),
|
|
activeTrackColor: Color(0xff000006),
|
|
activeTickMarkColor: Color(0xff000007),
|
|
inactiveTrackColor: Color(0xff000008),
|
|
inactiveTickMarkColor: Color(0xff000009),
|
|
overlayColor: Color(0xff000010),
|
|
thumbColor: Color(0xff000011),
|
|
valueIndicatorColor: Color(0xff000012),
|
|
),
|
|
);
|
|
final SliderThemeData sliderTheme = theme.sliderTheme;
|
|
double value = 0.45;
|
|
Widget buildApp({
|
|
Color activeColor,
|
|
Color inactiveColor,
|
|
int divisions,
|
|
bool enabled = true,
|
|
}) {
|
|
final ValueChanged<double> onChanged = !enabled
|
|
? null
|
|
: (double d) {
|
|
value = d;
|
|
};
|
|
return MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Theme(
|
|
data: theme,
|
|
child: Slider(
|
|
value: value,
|
|
label: '$value',
|
|
divisions: divisions,
|
|
activeColor: activeColor,
|
|
inactiveColor: inactiveColor,
|
|
onChanged: onChanged,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildApp());
|
|
|
|
final MaterialInkController material = Material.of(tester.element(find.byType(Slider)));
|
|
final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay));
|
|
|
|
// Check default theme for enabled widget.
|
|
expect(material, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.inactiveTrackColor));
|
|
expect(material, paints..shadow(color: const Color(0xff000000)));
|
|
expect(material, paints..circle(color: sliderTheme.thumbColor));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor)));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.activeTickMarkColor)));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
|
|
|
|
// Test setting only the activeColor.
|
|
await tester.pumpWidget(buildApp(activeColor: customColor1));
|
|
expect(material, paints..rrect(color: customColor1)..rrect(color: sliderTheme.inactiveTrackColor));
|
|
expect(material, paints..shadow(color: Colors.black));
|
|
expect(material, paints..circle(color: customColor1));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.thumbColor)));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor)));
|
|
|
|
// Test setting only the inactiveColor.
|
|
await tester.pumpWidget(buildApp(inactiveColor: customColor1));
|
|
expect(material, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: customColor1));
|
|
expect(material, paints..shadow(color: Colors.black));
|
|
expect(material, paints..circle(color: sliderTheme.thumbColor));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor)));
|
|
|
|
// Test setting both activeColor and inactiveColor.
|
|
await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2));
|
|
expect(material, paints..rrect(color: customColor1)..rrect(color: customColor2));
|
|
expect(material, paints..shadow(color: Colors.black));
|
|
expect(material, paints..circle(color: customColor1));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.thumbColor)));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor)));
|
|
|
|
// Test colors for discrete slider.
|
|
await tester.pumpWidget(buildApp(divisions: 3));
|
|
expect(material, paints..rrect(color: sliderTheme.activeTrackColor)..rrect(color: sliderTheme.inactiveTrackColor));
|
|
expect(
|
|
material,
|
|
paints
|
|
..circle(color: sliderTheme.activeTickMarkColor)
|
|
..circle(color: sliderTheme.activeTickMarkColor)
|
|
..circle(color: sliderTheme.inactiveTickMarkColor)
|
|
..circle(color: sliderTheme.inactiveTickMarkColor)
|
|
..shadow(color: Colors.black)
|
|
..circle(color: sliderTheme.thumbColor)
|
|
);
|
|
expect(material, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor)));
|
|
|
|
// Test colors for discrete slider with inactiveColor and activeColor set.
|
|
await tester.pumpWidget(buildApp(
|
|
activeColor: customColor1,
|
|
inactiveColor: customColor2,
|
|
divisions: 3,
|
|
));
|
|
expect(material, paints..rrect(color: customColor1)..rrect(color: customColor2));
|
|
expect(
|
|
material,
|
|
paints
|
|
..circle(color: customColor2)
|
|
..circle(color: customColor2)
|
|
..circle(color: customColor1)
|
|
..circle(color: customColor1)
|
|
..shadow(color: Colors.black)
|
|
..circle(color: customColor1));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.thumbColor)));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledActiveTrackColor)));
|
|
expect(material, isNot(paints..rrect(color: sliderTheme.disabledInactiveTrackColor)));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.activeTickMarkColor)));
|
|
expect(material, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
|
|
|
|
// Test default theme for disabled widget.
|
|
await tester.pumpWidget(buildApp(enabled: false));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
material,
|
|
paints
|
|
..rrect(color: sliderTheme.disabledActiveTrackColor)
|
|
..rrect(color: sliderTheme.disabledInactiveTrackColor));
|
|
expect(material, paints..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)));
|
|
|
|
// Test setting the activeColor and inactiveColor for disabled widget.
|
|
await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2, enabled: false));
|
|
expect(
|
|
material,
|
|
paints
|
|
..rrect(color: sliderTheme.disabledActiveTrackColor)
|
|
..rrect(color: sliderTheme.disabledInactiveTrackColor));
|
|
expect(material, paints..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)));
|
|
|
|
// Test that the default value indicator has the right colors.
|
|
await tester.pumpWidget(buildApp(divisions: 3));
|
|
Offset center = tester.getCenter(find.byType(Slider));
|
|
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: sliderTheme.valueIndicatorColor)
|
|
..paragraph(),
|
|
);
|
|
await gesture.up();
|
|
// Wait for value indicator animation to finish.
|
|
await tester.pumpAndSettle();
|
|
|
|
// Testing the custom colors are used for the indicator.
|
|
await tester.pumpWidget(buildApp(
|
|
divisions: 3,
|
|
activeColor: customColor1,
|
|
inactiveColor: customColor2,
|
|
));
|
|
center = tester.getCenter(find.byType(Slider));
|
|
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
|
|
..rrect(color: customColor1) // active track
|
|
..rrect(color: customColor2) // inactive track
|
|
..circle(color: customColor1.withOpacity(0.12)) // overlay
|
|
..circle(color: customColor2) // 1st tick mark
|
|
..circle(color: customColor2) // 2nd tick mark
|
|
..circle(color: customColor2) // 3rd tick mark
|
|
..circle(color: customColor1) // 4th tick mark
|
|
..shadow(color: Colors.black)
|
|
..circle(color: customColor1) // thumb
|
|
..path(color: sliderTheme.valueIndicatorColor), // indicator
|
|
);
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async {
|
|
double value = 0.0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: ListView(
|
|
children: <Widget>[
|
|
Slider(
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
value = newValue;
|
|
},
|
|
),
|
|
Container(
|
|
height: 2000.0,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.byType(Slider));
|
|
expect(value, equals(0.5));
|
|
});
|
|
|
|
testWidgets('Slider drags immediately (LTR)', (WidgetTester tester) async {
|
|
double value = 0.0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Slider(
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
value = newValue;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Offset center = tester.getCenter(find.byType(Slider));
|
|
final TestGesture gesture = await tester.startGesture(center);
|
|
|
|
expect(value, equals(0.5));
|
|
|
|
await gesture.moveBy(const Offset(1.0, 0.0));
|
|
|
|
expect(value, greaterThan(0.5));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('Slider drags immediately (RTL)', (WidgetTester tester) async {
|
|
double value = 0.0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Slider(
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
value = newValue;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Offset center = tester.getCenter(find.byType(Slider));
|
|
final TestGesture gesture = await tester.startGesture(center);
|
|
|
|
expect(value, equals(0.5));
|
|
|
|
await gesture.moveBy(const Offset(1.0, 0.0));
|
|
|
|
expect(value, lessThan(0.5));
|
|
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('Slider sizing', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: const Material(
|
|
child: Center(
|
|
child: Slider(
|
|
value: 0.5,
|
|
onChanged: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(800.0, 600.0));
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: const Material(
|
|
child: Center(
|
|
child: IntrinsicWidth(
|
|
child: Slider(
|
|
value: 0.5,
|
|
onChanged: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 600.0));
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: const Material(
|
|
child: Center(
|
|
child: OverflowBox(
|
|
maxWidth: double.infinity,
|
|
maxHeight: double.infinity,
|
|
child: Slider(
|
|
value: 0.5,
|
|
onChanged: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 48.0));
|
|
});
|
|
|
|
testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
|
|
Widget buildSlider({
|
|
double textScaleFactor,
|
|
bool isDiscrete = true,
|
|
ShowValueIndicator show = ShowValueIndicator.onlyForDiscrete,
|
|
}) {
|
|
return MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MediaQuery(
|
|
data: MediaQueryData(textScaleFactor: textScaleFactor),
|
|
child: Material(
|
|
child: Theme(
|
|
data: Theme.of(context).copyWith(
|
|
sliderTheme: Theme.of(context).sliderTheme.copyWith(showValueIndicator: show),
|
|
),
|
|
child: Center(
|
|
child: OverflowBox(
|
|
maxWidth: double.infinity,
|
|
maxHeight: double.infinity,
|
|
child: Slider(
|
|
key: sliderKey,
|
|
min: 0.0,
|
|
max: 100.0,
|
|
divisions: isDiscrete ? 10 : null,
|
|
label: '${value.round()}',
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildSlider(textScaleFactor: 1.0));
|
|
Offset center = tester.getCenter(find.byType(Slider));
|
|
TestGesture gesture = await tester.startGesture(center);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.renderObject(find.byType(Overlay)),
|
|
paints
|
|
..path(
|
|
includes: const <Offset>[
|
|
Offset(0.0, 0.0),
|
|
Offset(0.0, -8.0),
|
|
Offset(-276.0, -16.0),
|
|
Offset(-216.0, -16.0),
|
|
],
|
|
color: const Color(0xf55f5f5f),
|
|
)
|
|
..paragraph(),
|
|
);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.pumpWidget(buildSlider(textScaleFactor: 2.0));
|
|
center = tester.getCenter(find.byType(Slider));
|
|
gesture = await tester.startGesture(center);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.renderObject(find.byType(Overlay)),
|
|
paints
|
|
..path(
|
|
includes: const <Offset>[
|
|
Offset(0.0, 0.0),
|
|
Offset(0.0, -8.0),
|
|
Offset(-304.0, -16.0),
|
|
Offset(-216.0, -16.0),
|
|
],
|
|
color: const Color(0xf55f5f5f),
|
|
)
|
|
..paragraph(),
|
|
);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// Check continuous
|
|
await tester.pumpWidget(buildSlider(
|
|
textScaleFactor: 1.0,
|
|
isDiscrete: false,
|
|
show: ShowValueIndicator.onlyForContinuous,
|
|
));
|
|
center = tester.getCenter(find.byType(Slider));
|
|
gesture = await tester.startGesture(center);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.renderObject(find.byType(Overlay)),
|
|
paints
|
|
..path(
|
|
includes: const <Offset>[
|
|
Offset(0.0, 0.0),
|
|
Offset(0.0, -8.0),
|
|
Offset(-276.0, -16.0),
|
|
Offset(-216.0, -16.0),
|
|
],
|
|
color: const Color(0xf55f5f5f),
|
|
)
|
|
..paragraph(),
|
|
);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.pumpWidget(buildSlider(
|
|
textScaleFactor: 2.0,
|
|
isDiscrete: false,
|
|
show: ShowValueIndicator.onlyForContinuous,
|
|
));
|
|
center = tester.getCenter(find.byType(Slider));
|
|
gesture = await tester.startGesture(center);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.renderObject(find.byType(Overlay)),
|
|
paints
|
|
..path(
|
|
includes: const <Offset>[
|
|
Offset(0.0, 0.0),
|
|
Offset(0.0, -8.0),
|
|
Offset(-276.0, -16.0),
|
|
Offset(-216.0, -16.0),
|
|
],
|
|
color: const Color(0xf55f5f5f),
|
|
)
|
|
..paragraph(),
|
|
);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('Tick marks are skipped when they are too dense', (WidgetTester tester) async {
|
|
Widget buildSlider({
|
|
int divisions,
|
|
}) {
|
|
return MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Slider(
|
|
min: 0.0,
|
|
max: 100.0,
|
|
divisions: divisions,
|
|
value: 0.25,
|
|
onChanged: (double newValue) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Pump a slider with a reasonable amount of divisions to verify that the
|
|
// tick marks are drawn when the number of tick marks is not too dense.
|
|
await tester.pumpWidget(
|
|
buildSlider(
|
|
divisions: 4,
|
|
),
|
|
);
|
|
|
|
final MaterialInkController material = Material.of(tester.element(find.byType(Slider)));
|
|
|
|
// 5 tick marks and a thumb.
|
|
expect(material, paintsExactlyCountTimes(#drawCircle, 6));
|
|
|
|
// 200 divisions will produce a tick interval off less than 6,
|
|
// which would be too dense to draw.
|
|
await tester.pumpWidget(
|
|
buildSlider(
|
|
divisions: 200,
|
|
),
|
|
);
|
|
|
|
// No tick marks are drawn because they are too dense, but the thumb is
|
|
// still drawn.
|
|
expect(material, paintsExactlyCountTimes(#drawCircle, 1));
|
|
});
|
|
|
|
testWidgets('Slider has correct animations when reparented', (WidgetTester tester) async {
|
|
final Key sliderKey = GlobalKey(debugLabel: 'A');
|
|
double value = 0.0;
|
|
|
|
Widget buildSlider(int parents) {
|
|
Widget createParents(int parents, StateSetter setState) {
|
|
Widget slider = Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
divisions: 4,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
);
|
|
|
|
for (int i = 0; i < parents; ++i) {
|
|
slider = Column(children: <Widget>[slider]);
|
|
}
|
|
return slider;
|
|
}
|
|
|
|
return MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: createParents(parents, setState),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> testReparenting(bool reparent) async {
|
|
final MaterialInkController material = Material.of(tester.element(find.byType(Slider)));
|
|
final Offset center = tester.getCenter(find.byType(Slider));
|
|
// Move to 0.0.
|
|
TestGesture gesture = await tester.startGesture(Offset.zero);
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
expect(
|
|
material,
|
|
paints
|
|
..circle(x: 26.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 213.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 400.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 587.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 774.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 24.0, y: 24.0, radius: 10.0),
|
|
);
|
|
|
|
gesture = await tester.startGesture(center);
|
|
await tester.pump();
|
|
// Wait for animations to start.
|
|
await tester.pump(const Duration(milliseconds: 25));
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
|
|
expect(
|
|
material,
|
|
paints
|
|
..circle(x: 111.20703125, y: 24.0, radius: 5.687664985656738)
|
|
..circle(x: 26.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 213.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 400.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 587.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 774.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 111.20703125, y: 24.0, radius: 10.0),
|
|
);
|
|
|
|
// Reparenting in the middle of an animation should do nothing.
|
|
if (reparent) {
|
|
await tester.pumpWidget(buildSlider(2));
|
|
}
|
|
|
|
// Move a little further in the animations.
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
|
|
expect(
|
|
material,
|
|
paints
|
|
..circle(x: 190.0135726928711, y: 24.0, radius: 12.0)
|
|
..circle(x: 26.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 213.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 400.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 587.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 774.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 190.0135726928711, y: 24.0, radius: 10.0),
|
|
);
|
|
// Wait for animations to finish.
|
|
await tester.pumpAndSettle();
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
expect(
|
|
material,
|
|
paints
|
|
..circle(x: 400.0, y: 24.0, radius: 24.0)
|
|
..circle(x: 26.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 213.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 400.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 587.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 774.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 400.0, y: 24.0, radius: 10.0),
|
|
);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
|
expect(
|
|
material,
|
|
paints
|
|
..circle(x: 26.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 213.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 400.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 587.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 774.0, y: 24.0, radius: 1.0)
|
|
..circle(x: 400.0, y: 24.0, radius: 10.0),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildSlider(1));
|
|
// Do it once without reparenting in the middle of an animation
|
|
await testReparenting(false);
|
|
// Now do it again with reparenting in the middle of an animation.
|
|
await testReparenting(true);
|
|
});
|
|
|
|
|
|
testWidgets('Slider Semantics', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Slider(
|
|
value: 0.5,
|
|
onChanged: (double v) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 1,
|
|
textDirection: TextDirection.ltr,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 2,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 3,
|
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 4,
|
|
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable],
|
|
actions: <SemanticsAction>[SemanticsAction.increase, SemanticsAction.decrease],
|
|
value: '50%',
|
|
increasedValue: '55%',
|
|
decreasedValue: '45%',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
|
|
// Disable slider
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: const Material(
|
|
child: Slider(
|
|
value: 0.5,
|
|
onChanged: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 1,
|
|
textDirection: TextDirection.ltr,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 2,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 3,
|
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 4,
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.hasEnabledState,
|
|
// isFocusable is delayed by 1 frame.
|
|
SemanticsFlag.isFocusable,
|
|
],
|
|
value: '50%',
|
|
increasedValue: '55%',
|
|
decreasedValue: '45%',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 1,
|
|
textDirection: TextDirection.ltr,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 2,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 3,
|
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 4,
|
|
flags: <SemanticsFlag>[
|
|
SemanticsFlag.hasEnabledState,
|
|
],
|
|
value: '50%',
|
|
increasedValue: '55%',
|
|
decreasedValue: '45%',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
|
|
semantics.dispose();
|
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
|
|
|
|
testWidgets('Slider Semantics', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Theme(
|
|
data: ThemeData.light(),
|
|
child: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Slider(
|
|
value: 100.0,
|
|
min: 0.0,
|
|
max: 200.0,
|
|
onChanged: (double v) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 1,
|
|
textDirection: TextDirection.ltr,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 2,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 3,
|
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 4,
|
|
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable],
|
|
actions: <SemanticsAction>[SemanticsAction.increase, SemanticsAction.decrease],
|
|
value: '50%',
|
|
increasedValue: '60%',
|
|
decreasedValue: '40%',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
|
|
// Disable slider
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: const Material(
|
|
child: Slider(
|
|
value: 0.5,
|
|
onChanged: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 1,
|
|
textDirection: TextDirection.ltr,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 2,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 3,
|
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 5,
|
|
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState],
|
|
value: '50%',
|
|
increasedValue: '60%',
|
|
decreasedValue: '40%',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
semantics.dispose();
|
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
|
|
|
testWidgets('Slider semantics with custom formatter', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Slider(
|
|
value: 40.0,
|
|
min: 0.0,
|
|
max: 200.0,
|
|
divisions: 10,
|
|
semanticFormatterCallback: (double value) => value.round().toString(),
|
|
onChanged: (double v) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 1,
|
|
textDirection: TextDirection.ltr,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 2,
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 3,
|
|
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 4,
|
|
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable],
|
|
actions: <SemanticsAction>[SemanticsAction.increase, SemanticsAction.decrease],
|
|
value: '40',
|
|
increasedValue: '60',
|
|
decreasedValue: '20',
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('Slider is focusable and has correct focus color', (WidgetTester tester) async {
|
|
final FocusNode focusNode = FocusNode(debugLabel: 'Slider');
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
double value = 0.5;
|
|
Widget buildApp({bool enabled = true}) {
|
|
return MaterialApp(
|
|
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,
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
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: Colors.orange[500]),
|
|
);
|
|
|
|
// Check that the overlay does not show when focused 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: Colors.orange[500])),
|
|
);
|
|
});
|
|
|
|
testWidgets('Slider can be hovered and has correct hover color', (WidgetTester tester) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
double value = 0.5;
|
|
Widget buildApp({bool enabled = true}) {
|
|
return MaterialApp(
|
|
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,
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
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: Colors.orange[500])),
|
|
);
|
|
|
|
// Start hovering.
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
addTearDown(gesture.removePointer);
|
|
await gesture.moveTo(tester.getCenter(find.byType(Slider)));
|
|
|
|
// Slider has overlay when enabled and hovering.
|
|
await tester.pumpWidget(buildApp());
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
Material.of(tester.element(find.byType(Slider))),
|
|
paints..circle(color: Colors.orange[500]),
|
|
);
|
|
|
|
// Slider does not have an overlay when disabled and hovering.
|
|
await tester.pumpWidget(buildApp(enabled: false));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
Material.of(tester.element(find.byType(Slider))),
|
|
isNot(paints..circle(color: Colors.orange[500])),
|
|
);
|
|
});
|
|
|
|
testWidgets('Slider can be incremented and decremented by keyboard shortcuts - LTR', (WidgetTester tester) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
double value = 0.5;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Center(
|
|
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
|
return Slider(
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
autofocus: true,
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.55);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.5);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.55);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.5);
|
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
|
|
|
|
testWidgets('Slider can be incremented and decremented by keyboard shortcuts - LTR', (WidgetTester tester) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
double value = 0.5;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Center(
|
|
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
|
return Slider(
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
autofocus: true,
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.6);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.5);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.6);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.5);
|
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
|
|
|
testWidgets('Slider can be incremented and decremented by keyboard shortcuts - RTL', (WidgetTester tester) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
double value = 0.5;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Center(
|
|
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
|
return Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: Slider(
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
autofocus: true,
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.45);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.5);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.55);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.5);
|
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
|
|
|
|
testWidgets('Slider can be incremented and decremented by keyboard shortcuts - RTL', (WidgetTester tester) async {
|
|
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
|
double value = 0.5;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Material(
|
|
child: Center(
|
|
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
|
return Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: Slider(
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
autofocus: true,
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.4);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.5);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.6);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
|
await tester.pumpAndSettle();
|
|
expect(value, 0.5);
|
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
|
|
|
testWidgets('Value indicator appears when it should', (WidgetTester tester) async {
|
|
final ThemeData baseTheme = ThemeData(
|
|
platform: TargetPlatform.android,
|
|
primarySwatch: Colors.blue,
|
|
);
|
|
SliderThemeData theme = baseTheme.sliderTheme;
|
|
double value = 0.45;
|
|
Widget buildApp({ SliderThemeData sliderTheme, int divisions, bool enabled = true }) {
|
|
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
|
|
return MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Theme(
|
|
data: baseTheme,
|
|
child: SliderTheme(
|
|
data: sliderTheme,
|
|
child: Slider(
|
|
value: value,
|
|
label: '$value',
|
|
divisions: divisions,
|
|
onChanged: onChanged,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> expectValueIndicator({
|
|
bool isVisible,
|
|
SliderThemeData theme,
|
|
int divisions,
|
|
bool enabled = true,
|
|
}) async {
|
|
// Discrete enabled widget.
|
|
await tester.pumpWidget(buildApp(sliderTheme: theme, divisions: divisions, enabled: enabled));
|
|
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();
|
|
|
|
|
|
final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay));
|
|
expect(
|
|
valueIndicatorBox,
|
|
isVisible
|
|
? (paints..path(color: theme.valueIndicatorColor)..paragraph())
|
|
: isNot(paints..path(color: theme.valueIndicatorColor)..paragraph()),
|
|
);
|
|
await gesture.up();
|
|
}
|
|
|
|
// Default (showValueIndicator set to onlyForDiscrete).
|
|
await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, enabled: true);
|
|
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
|
|
await expectValueIndicator(isVisible: false, theme: theme, enabled: true);
|
|
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
|
|
|
|
// With showValueIndicator set to onlyForContinuous.
|
|
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.onlyForContinuous);
|
|
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: true);
|
|
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
|
|
await expectValueIndicator(isVisible: true, theme: theme, enabled: true);
|
|
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
|
|
|
|
// discrete enabled widget with showValueIndicator set to always.
|
|
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.always);
|
|
await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, enabled: true);
|
|
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
|
|
await expectValueIndicator(isVisible: true, theme: theme, enabled: true);
|
|
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
|
|
|
|
// discrete enabled widget with showValueIndicator set to never.
|
|
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.never);
|
|
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: true);
|
|
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
|
|
await expectValueIndicator(isVisible: false, theme: theme, enabled: true);
|
|
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
|
|
});
|
|
|
|
testWidgets("Slider doesn't start any animations after dispose", (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
divisions: 4,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
|
|
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
|
expect(value, equals(0.5));
|
|
await gesture.moveBy(const Offset(-500.0, 0.0));
|
|
await tester.pumpAndSettle(const Duration(milliseconds: 100));
|
|
// Change the tree to dispose the original widget.
|
|
await tester.pumpWidget(Container());
|
|
expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)), equals(1));
|
|
await gesture.up();
|
|
});
|
|
|
|
testWidgets('Slider removes value indicator from overlay if Slider gets disposed without value indicator animation completing.', (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
const Color fillColor = Color(0xf55f5f5f);
|
|
double value = 0.0;
|
|
|
|
Widget buildApp({
|
|
Color activeColor,
|
|
Color inactiveColor,
|
|
int divisions,
|
|
bool enabled = true,
|
|
}) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Builder(
|
|
// The builder is used to pass the context from the MaterialApp widget
|
|
// to the [Navigator]. This context is required in order for the
|
|
// Navigator to work.
|
|
builder: (BuildContext context) {
|
|
return Column(
|
|
children: <Widget>[
|
|
Slider(
|
|
key: sliderKey,
|
|
min: 0.0,
|
|
max: 100.0,
|
|
divisions: divisions,
|
|
label: '${value.round()}',
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
value = newValue;
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
child: const Text('Next'),
|
|
onPressed: () {
|
|
Navigator.of(context).pushReplacement(
|
|
MaterialPageRoute<void>(
|
|
builder: (BuildContext context) {
|
|
return ElevatedButton(
|
|
child: const Text('Inner page'),
|
|
onPressed: () { Navigator.of(context).pop(); },
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildApp(divisions: 3));
|
|
|
|
final RenderObject valueIndicatorBox = tester.renderObject(find.byType(Overlay));
|
|
final Offset topRight = tester.getTopRight(find.byType(Slider)).translate(-24, 0);
|
|
final TestGesture gesture = await tester.startGesture(topRight);
|
|
// Wait for value indicator animation to finish.
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(Slider), isNotNull);
|
|
expect(
|
|
valueIndicatorBox,
|
|
paints
|
|
// Represents the raised button with text, next.
|
|
..path(color: Colors.black)
|
|
..paragraph()
|
|
// Represents the Slider.
|
|
..path(color: fillColor)
|
|
..paragraph()
|
|
);
|
|
|
|
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 2));
|
|
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawParagraph, 2));
|
|
|
|
await tester.tap(find.text('Next'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(Slider), findsNothing);
|
|
expect(
|
|
valueIndicatorBox,
|
|
isNot(
|
|
paints
|
|
..path(color: fillColor)
|
|
..paragraph(),
|
|
),
|
|
);
|
|
|
|
// Represents the ElevatedButton with inner Text, inner page.
|
|
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 1));
|
|
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawParagraph, 1));
|
|
|
|
// Don't stop holding the value indicator.
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
});
|
|
|
|
testWidgets('Slider.adaptive', (WidgetTester tester) async {
|
|
double value = 0.5;
|
|
|
|
Widget buildFrame(TargetPlatform platform) {
|
|
return MaterialApp(
|
|
theme: ThemeData(platform: platform),
|
|
home: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return Material(
|
|
child: Center(
|
|
child: Slider.adaptive(
|
|
value: value,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
for (final TargetPlatform platform in <TargetPlatform>[TargetPlatform.iOS, TargetPlatform.macOS]) {
|
|
value = 0.5;
|
|
await tester.pumpWidget(buildFrame(platform));
|
|
expect(find.byType(Slider), findsOneWidget);
|
|
expect(find.byType(CupertinoSlider), findsOneWidget);
|
|
|
|
expect(value, 0.5, reason: 'on ${describeEnum(platform)}');
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CupertinoSlider)));
|
|
// Drag to the right end of the track.
|
|
await gesture.moveBy(const Offset(600.0, 0.0));
|
|
expect(value, 1.0, reason: 'on ${describeEnum(platform)}');
|
|
await gesture.up();
|
|
}
|
|
|
|
for (final TargetPlatform platform in <TargetPlatform>[TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows]) {
|
|
value = 0.5;
|
|
await tester.pumpWidget(buildFrame(platform));
|
|
await tester.pumpAndSettle(); // Finish the theme change animation.
|
|
expect(find.byType(Slider), findsOneWidget);
|
|
expect(find.byType(CupertinoSlider), findsNothing);
|
|
|
|
expect(value, 0.5, reason: 'on ${describeEnum(platform)}');
|
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Slider)));
|
|
// Drag to the right end of the track.
|
|
await gesture.moveBy(const Offset(600.0, 0.0));
|
|
expect(value, 1.0, reason: 'on ${describeEnum(platform)}');
|
|
await gesture.up();
|
|
}
|
|
});
|
|
|
|
testWidgets('Slider respects height from theme', (WidgetTester tester) async {
|
|
final Key sliderKey = UniqueKey();
|
|
double value = 0.0;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
final SliderThemeData sliderTheme = SliderTheme.of(context).copyWith(tickMarkShape: TallSliderTickMarkShape());
|
|
return MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: Material(
|
|
child: Center(
|
|
child: IntrinsicHeight(
|
|
child: SliderTheme(
|
|
data: sliderTheme,
|
|
child: Slider(
|
|
key: sliderKey,
|
|
value: value,
|
|
divisions: 4,
|
|
onChanged: (double newValue) {
|
|
setState(() {
|
|
value = newValue;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderBox renderObject = tester.renderObject<RenderBox>(find.byType(Slider));
|
|
expect(renderObject.size.height, 200);
|
|
});
|
|
|
|
testWidgets('Slider changes mouse cursor when hovered', (WidgetTester tester) async {
|
|
// Test Slider() constructor
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Material(
|
|
child: Center(
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.forbidden,
|
|
child: Slider(
|
|
mouseCursor: SystemMouseCursors.text,
|
|
value: 0.5,
|
|
onChanged: (double newValue) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
|
|
await gesture.addPointer(location: tester.getCenter(find.byType(Slider)));
|
|
addTearDown(gesture.removePointer);
|
|
|
|
await tester.pump();
|
|
|
|
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
|
|
|
|
// Test Slider.adaptive() constructor
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Material(
|
|
child: Center(
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.forbidden,
|
|
child: Slider.adaptive(
|
|
mouseCursor: SystemMouseCursors.text,
|
|
value: 0.5,
|
|
onChanged: (double newValue) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
|
|
|
|
// Test default cursor
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Material(
|
|
child: Center(
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.forbidden,
|
|
child: Slider(
|
|
value: 0.5,
|
|
onChanged: (double newValue) { },
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
|
|
});
|
|
|
|
testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
|
|
const Slider(
|
|
activeColor: Colors.blue,
|
|
divisions: 10,
|
|
inactiveColor: Colors.grey,
|
|
label: 'Set a value',
|
|
max: 100.0,
|
|
min: 0.0,
|
|
onChanged: null,
|
|
onChangeEnd: null,
|
|
onChangeStart: null,
|
|
semanticFormatterCallback: null,
|
|
value: 50.0,
|
|
).debugFillProperties(builder);
|
|
|
|
final List<String> description = builder.properties
|
|
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
|
|
.map((DiagnosticsNode node) => node.toString()).toList();
|
|
|
|
expect(description, <String>[
|
|
'value: 50.0',
|
|
'disabled',
|
|
'min: 0.0',
|
|
'max: 100.0',
|
|
'divisions: 10',
|
|
'label: "Set a value"',
|
|
'activeColor: MaterialColor(primary value: Color(0xff2196f3))',
|
|
'inactiveColor: MaterialColor(primary value: Color(0xff9e9e9e))',
|
|
]);
|
|
});
|
|
|
|
testWidgets('Slider paint correctly when the track shape is rectangular', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(
|
|
sliderTheme: const SliderThemeData(
|
|
trackShape: RectangularSliderTrackShape(),
|
|
)
|
|
),
|
|
home: Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData.fromWindow(window),
|
|
child: const Material(
|
|
child: Center(
|
|
child: Slider(
|
|
value: 0.5,
|
|
onChanged: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// _RenderSlider is the last render object in the tree.
|
|
final RenderObject renderObject = tester.allRenderObjects.last;
|
|
|
|
// The active track rect should start at 24.0 pixels,
|
|
// and there should not have a gap between active and inactive track.
|
|
expect(renderObject,
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(24.0, 298.0, 400.0, 302.0)) // active track Rect.
|
|
..rect(rect: const Rect.fromLTRB(400.0, 298.0, 776.0, 302.0)) // inactive track Rect.
|
|
);
|
|
});
|
|
}
|