// 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' show window; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/painting.dart'; import '../rendering/mock_canvas.dart'; void main() { test('SliderThemeData copyWith, ==, hashCode basics', () { expect(const SliderThemeData(), const SliderThemeData().copyWith()); expect(const SliderThemeData().hashCode, const SliderThemeData().copyWith().hashCode); }); testWidgets('Default SliderThemeData debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SliderThemeData().debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, <String>[]); }); testWidgets('SliderThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SliderThemeData( trackHeight: 7.0, activeTrackColor: Color(0xFF000001), inactiveTrackColor: Color(0xFF000002), disabledActiveTrackColor: Color(0xFF000003), disabledInactiveTrackColor: Color(0xFF000004), activeTickMarkColor: Color(0xFF000005), inactiveTickMarkColor: Color(0xFF000006), disabledActiveTickMarkColor: Color(0xFF000007), disabledInactiveTickMarkColor: Color(0xFF000008), thumbColor: Color(0xFF000009), overlappingShapeStrokeColor: Color(0xFF000010), disabledThumbColor: Color(0xFF000011), overlayColor: Color(0xFF000012), valueIndicatorColor: Color(0xFF000013), overlayShape: RoundSliderOverlayShape(), tickMarkShape: RoundSliderTickMarkShape(), thumbShape: RoundSliderThumbShape(), trackShape: RoundedRectSliderTrackShape(), valueIndicatorShape: PaddleSliderValueIndicatorShape(), rangeTickMarkShape: RoundRangeSliderTickMarkShape(), rangeThumbShape: RoundRangeSliderThumbShape(), rangeTrackShape: RoundedRectRangeSliderTrackShape(), rangeValueIndicatorShape: PaddleRangeSliderValueIndicatorShape(), showValueIndicator: ShowValueIndicator.always, valueIndicatorTextStyle: TextStyle(color: Colors.black), ).debugFillProperties(builder); final List<String> description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, <String>[ 'trackHeight: 7.0', 'activeTrackColor: Color(0xff000001)', 'inactiveTrackColor: Color(0xff000002)', 'disabledActiveTrackColor: Color(0xff000003)', 'disabledInactiveTrackColor: Color(0xff000004)', 'activeTickMarkColor: Color(0xff000005)', 'inactiveTickMarkColor: Color(0xff000006)', 'disabledActiveTickMarkColor: Color(0xff000007)', 'disabledInactiveTickMarkColor: Color(0xff000008)', 'thumbColor: Color(0xff000009)', 'overlappingShapeStrokeColor: Color(0xff000010)', 'disabledThumbColor: Color(0xff000011)', 'overlayColor: Color(0xff000012)', 'valueIndicatorColor: Color(0xff000013)', "overlayShape: Instance of 'RoundSliderOverlayShape'", "tickMarkShape: Instance of 'RoundSliderTickMarkShape'", "thumbShape: Instance of 'RoundSliderThumbShape'", "trackShape: Instance of 'RoundedRectSliderTrackShape'", "valueIndicatorShape: Instance of 'PaddleSliderValueIndicatorShape'", "rangeTickMarkShape: Instance of 'RoundRangeSliderTickMarkShape'", "rangeThumbShape: Instance of 'RoundRangeSliderThumbShape'", "rangeTrackShape: Instance of 'RoundedRectRangeSliderTrackShape'", "rangeValueIndicatorShape: Instance of 'PaddleRangeSliderValueIndicatorShape'", 'showValueIndicator: always', 'valueIndicatorTextStyle: TextStyle(inherit: true, color: Color(0xff000000))', ]); }); testWidgets('Slider uses ThemeData slider theme if present', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.red, ); final SliderThemeData sliderTheme = theme.sliderTheme; final SliderThemeData customTheme = sliderTheme.copyWith( activeTrackColor: Colors.purple, inactiveTrackColor: Colors.purple.withAlpha(0x3d), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, enabled: false)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect( material, paints ..rrect(color: customTheme.disabledActiveTrackColor) ..rrect(color: customTheme.disabledInactiveTrackColor), ); }); testWidgets('Slider overrides ThemeData theme if SliderTheme present', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.red, ); final SliderThemeData sliderTheme = theme.sliderTheme; final SliderThemeData customTheme = sliderTheme.copyWith( activeTrackColor: Colors.purple, inactiveTrackColor: Colors.purple.withAlpha(0x3d), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, enabled: false)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect( material, paints ..rrect(color: customTheme.disabledActiveTrackColor) ..rrect(color: customTheme.disabledInactiveTrackColor), ); }); testWidgets('SliderThemeData generates correct opacities for fromPrimaryColors', (WidgetTester tester) async { const Color customColor1 = Color(0xcafefeed); const Color customColor2 = Color(0xdeadbeef); const Color customColor3 = Color(0xdecaface); const Color customColor4 = Color(0xfeedcafe); final SliderThemeData sliderTheme = SliderThemeData.fromPrimaryColors( primaryColor: customColor1, primaryColorDark: customColor2, primaryColorLight: customColor3, valueIndicatorTextStyle: ThemeData.fallback().textTheme.bodyText1.copyWith(color: customColor4), ); expect(sliderTheme.activeTrackColor, equals(customColor1.withAlpha(0xff))); expect(sliderTheme.inactiveTrackColor, equals(customColor1.withAlpha(0x3d))); expect(sliderTheme.disabledActiveTrackColor, equals(customColor2.withAlpha(0x52))); expect(sliderTheme.disabledInactiveTrackColor, equals(customColor2.withAlpha(0x1f))); expect(sliderTheme.activeTickMarkColor, equals(customColor3.withAlpha(0x8a))); expect(sliderTheme.inactiveTickMarkColor, equals(customColor1.withAlpha(0x8a))); expect(sliderTheme.disabledActiveTickMarkColor, equals(customColor3.withAlpha(0x1f))); expect(sliderTheme.disabledInactiveTickMarkColor, equals(customColor2.withAlpha(0x1f))); expect(sliderTheme.thumbColor, equals(customColor1.withAlpha(0xff))); expect(sliderTheme.disabledThumbColor, equals(customColor2.withAlpha(0x52))); expect(sliderTheme.overlayColor, equals(customColor1.withAlpha(0x1f))); expect(sliderTheme.valueIndicatorColor, equals(customColor1.withAlpha(0xff))); expect(sliderTheme.valueIndicatorTextStyle.color, equals(customColor4)); }); testWidgets('SliderThemeData generates correct shapes for fromPrimaryColors', (WidgetTester tester) async { const Color customColor1 = Color(0xcafefeed); const Color customColor2 = Color(0xdeadbeef); const Color customColor3 = Color(0xdecaface); const Color customColor4 = Color(0xfeedcafe); final SliderThemeData sliderTheme = SliderThemeData.fromPrimaryColors( primaryColor: customColor1, primaryColorDark: customColor2, primaryColorLight: customColor3, valueIndicatorTextStyle: ThemeData.fallback().textTheme.bodyText1.copyWith(color: customColor4), ); expect(sliderTheme.overlayShape, const RoundSliderOverlayShape()); expect(sliderTheme.tickMarkShape, const RoundSliderTickMarkShape()); expect(sliderTheme.thumbShape, const RoundSliderThumbShape()); expect(sliderTheme.trackShape, const RoundedRectSliderTrackShape()); expect(sliderTheme.valueIndicatorShape, const PaddleSliderValueIndicatorShape()); expect(sliderTheme.rangeTickMarkShape, const RoundRangeSliderTickMarkShape()); expect(sliderTheme.rangeThumbShape, const RoundRangeSliderThumbShape()); expect(sliderTheme.rangeTrackShape, const RoundedRectRangeSliderTrackShape()); expect(sliderTheme.rangeValueIndicatorShape, const PaddleRangeSliderValueIndicatorShape()); }); testWidgets('SliderThemeData lerps correctly', (WidgetTester tester) async { final SliderThemeData sliderThemeBlack = SliderThemeData.fromPrimaryColors( primaryColor: Colors.black, primaryColorDark: Colors.black, primaryColorLight: Colors.black, valueIndicatorTextStyle: ThemeData.fallback().textTheme.bodyText1.copyWith(color: Colors.black), ).copyWith(trackHeight: 2.0); final SliderThemeData sliderThemeWhite = SliderThemeData.fromPrimaryColors( primaryColor: Colors.white, primaryColorDark: Colors.white, primaryColorLight: Colors.white, valueIndicatorTextStyle: ThemeData.fallback().textTheme.bodyText1.copyWith(color: Colors.white), ).copyWith(trackHeight: 6.0); final SliderThemeData lerp = SliderThemeData.lerp(sliderThemeBlack, sliderThemeWhite, 0.5); const Color middleGrey = Color(0xff7f7f7f); expect(lerp.trackHeight, equals(4.0)); expect(lerp.activeTrackColor, equals(middleGrey.withAlpha(0xff))); expect(lerp.inactiveTrackColor, equals(middleGrey.withAlpha(0x3d))); expect(lerp.disabledActiveTrackColor, equals(middleGrey.withAlpha(0x52))); expect(lerp.disabledInactiveTrackColor, equals(middleGrey.withAlpha(0x1f))); expect(lerp.activeTickMarkColor, equals(middleGrey.withAlpha(0x8a))); expect(lerp.inactiveTickMarkColor, equals(middleGrey.withAlpha(0x8a))); expect(lerp.disabledActiveTickMarkColor, equals(middleGrey.withAlpha(0x1f))); expect(lerp.disabledInactiveTickMarkColor, equals(middleGrey.withAlpha(0x1f))); expect(lerp.thumbColor, equals(middleGrey.withAlpha(0xff))); expect(lerp.disabledThumbColor, equals(middleGrey.withAlpha(0x52))); expect(lerp.overlayColor, equals(middleGrey.withAlpha(0x1f))); expect(lerp.valueIndicatorColor, equals(middleGrey.withAlpha(0xff))); expect(lerp.valueIndicatorTextStyle.color, equals(middleGrey.withAlpha(0xff))); }); testWidgets('Default slider track draws correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, ); final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); const Radius radius = Radius.circular(2); const Radius activatedRadius = Radius.circular(3); // The enabled slider thumb has track segments that extend to and from // the center of the thumb. expect( material, paints ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 297.0, 212.0, 303.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.activeTrackColor) ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 776.0, 302.0, topRight: radius, bottomRight: radius), color: sliderTheme.inactiveTrackColor), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false)); await tester.pumpAndSettle(); // wait for disable animation // The disabled slider thumb is the same size as the enabled thumb. expect( material, paints ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 297.0, 212.0, 303.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.disabledActiveTrackColor) ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 298.0, 776.0, 302.0, topRight: radius, bottomRight: radius), color: sliderTheme.disabledInactiveTrackColor), ); }); testWidgets('Default slider overlay draws correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, ); final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // With no touch, paints only the thumb. expect( material, paints ..circle( color: sliderTheme.thumbColor, x: 212.0, y: 300.0, radius: 10.0, ), ); final Offset center = tester.getCenter(find.byType(Slider)); final TestGesture gesture = await tester.startGesture(center); // Wait for overlay animation to finish. await tester.pumpAndSettle(); // After touch, paints thumb and overlay. expect( material, paints ..circle( color: sliderTheme.overlayColor, x: 212.0, y: 300.0, radius: 24.0, ) ..circle( color: sliderTheme.thumbColor, x: 212.0, y: 300.0, radius: 10.0, ), ); await gesture.up(); await tester.pumpAndSettle(); // After the gesture is up and complete, it again paints only the thumb. expect( material, paints ..circle( color: sliderTheme.thumbColor, x: 212.0, y: 300.0, radius: 10.0, ), ); }); testWidgets('Default slider ticker and thumb shape draw correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, ); final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.45)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect(material, paints..circle(color: sliderTheme.thumbColor, radius: 10.0)); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.45, enabled: false)); await tester.pumpAndSettle(); // wait for disable animation expect(material, paints..circle(color: sliderTheme.disabledThumbColor, radius: 10.0)); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.45, divisions: 3)); await tester.pumpAndSettle(); // wait for enable animation expect( material, paints ..circle(color: sliderTheme.activeTickMarkColor) ..circle(color: sliderTheme.activeTickMarkColor) ..circle(color: sliderTheme.inactiveTickMarkColor) ..circle(color: sliderTheme.inactiveTickMarkColor) ..circle(color: sliderTheme.thumbColor, radius: 10.0), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.45, divisions: 3, enabled: false)); await tester.pumpAndSettle(); // wait for disable animation expect( material, paints ..circle(color: sliderTheme.disabledActiveTickMarkColor) ..circle(color: sliderTheme.disabledInactiveTickMarkColor) ..circle(color: sliderTheme.disabledInactiveTickMarkColor) ..circle(color: sliderTheme.disabledInactiveTickMarkColor) ..circle(color: sliderTheme.disabledThumbColor, radius: 10.0), ); }); testWidgets('Default paddle slider value indicator shape draws correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, ); final SliderThemeData sliderTheme = theme.sliderTheme.copyWith( thumbColor: Colors.red.shade500, showValueIndicator: ShowValueIndicator.always, valueIndicatorShape: const PaddleSliderValueIndicatorShape(), ); Widget buildApp(String value, { double sliderValue = 0.5, double textScale = 1.0 }) { return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScale), child: Material( child: Row( children: <Widget>[ Expanded( child: SliderTheme( data: sliderTheme, child: Slider( value: sliderValue, label: value, divisions: 3, onChanged: (double d) { }, ), ), ), ], ), ), ), ), ); } await tester.pumpWidget(buildApp('1')); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); Offset center = tester.getCenter(find.byType(Slider)); TestGesture gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -40.0), const Offset(15.9, -40.0), const Offset(-15.9, -40.0), ], excludes: <Offset>[const Offset(16.1, -40.0), const Offset(-16.1, -40.0)], ), ); await gesture.up(); // Test that it expands with a larger label. await tester.pumpWidget(buildApp('1000')); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -40.0), const Offset(35.9, -40.0), const Offset(-35.9, -40.0), ], excludes: <Offset>[const Offset(36.1, -40.0), const Offset(-36.1, -40.0)], ), ); await gesture.up(); // Test that it avoids the left edge of the screen. await tester.pumpWidget(buildApp('1000000', sliderValue: 0.0)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -40.0), const Offset(92.0, -40.0), const Offset(-16.0, -40.0), ], excludes: <Offset>[const Offset(98.1, -40.0), const Offset(-20.1, -40.0)], ), ); await gesture.up(); // Test that it avoids the right edge of the screen. await tester.pumpWidget(buildApp('1000000', sliderValue: 1.0)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -40.0), const Offset(16.0, -40.0), const Offset(-92.0, -40.0), ], excludes: <Offset>[const Offset(20.1, -40.0), const Offset(-98.1, -40.0)], ), ); await gesture.up(); // Test that the neck stretches when the text scale gets smaller. await tester.pumpWidget(buildApp('1000000', sliderValue: 0.0, textScale: 0.5)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -49.0), const Offset(68.0, -49.0), const Offset(-24.0, -49.0), ], excludes: <Offset>[ const Offset(98.0, -32.0), // inside full size, outside small const Offset(-40.0, -32.0), // inside full size, outside small const Offset(90.1, -49.0), const Offset(-40.1, -49.0), ], ), ); await gesture.up(); // Test that the neck shrinks when the text scale gets larger. await tester.pumpWidget(buildApp('1000000', sliderValue: 0.0, textScale: 2.5)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -38.8), const Offset(92.0, -38.8), const Offset(8.0, -23.0), // Inside large, outside scale=1.0 const Offset(-2.0, -23.0), // Inside large, outside scale=1.0 ], excludes: <Offset>[ const Offset(98.5, -38.8), const Offset(-16.1, -38.8), ], ), ); await gesture.up(); }); testWidgets('Default paddle slider value indicator shape draws correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, ); final SliderThemeData sliderTheme = theme.sliderTheme.copyWith( thumbColor: Colors.red.shade500, showValueIndicator: ShowValueIndicator.always, valueIndicatorShape: const PaddleSliderValueIndicatorShape(), ); Widget buildApp(String value, { double sliderValue = 0.5, double textScale = 1.0 }) { return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: MediaQueryData.fromWindow(window).copyWith(textScaleFactor: textScale), child: Material( child: Row( children: <Widget>[ Expanded( child: SliderTheme( data: sliderTheme, child: Slider( value: sliderValue, label: value, divisions: 3, onChanged: (double d) { }, ), ), ), ], ), ), ), ), ); } await tester.pumpWidget(buildApp('1')); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); Offset center = tester.getCenter(find.byType(Slider)); TestGesture gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -40.0), const Offset(15.9, -40.0), const Offset(-15.9, -40.0), ], excludes: <Offset>[const Offset(16.1, -40.0), const Offset(-16.1, -40.0)], ), ); await gesture.up(); // Test that it expands with a larger label. await tester.pumpWidget(buildApp('1000')); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -40.0), const Offset(35.9, -40.0), const Offset(-35.9, -40.0), ], excludes: <Offset>[const Offset(36.1, -40.0), const Offset(-36.1, -40.0)], ), ); await gesture.up(); // Test that it avoids the left edge of the screen. await tester.pumpWidget(buildApp('1000000', sliderValue: 0.0)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -40.0), const Offset(92.0, -40.0), const Offset(-16.0, -40.0), ], excludes: <Offset>[const Offset(98.1, -40.0), const Offset(-20.1, -40.0)], ), ); await gesture.up(); // Test that it avoids the right edge of the screen. await tester.pumpWidget(buildApp('1000000', sliderValue: 1.0)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -40.0), const Offset(16.0, -40.0), const Offset(-92.0, -40.0), ], excludes: <Offset>[const Offset(20.1, -40.0), const Offset(-98.1, -40.0)], ), ); await gesture.up(); // Test that the neck stretches when the text scale gets smaller. await tester.pumpWidget(buildApp('1000000', sliderValue: 0.0, textScale: 0.5)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -49.0), const Offset(68.0, -49.0), const Offset(-24.0, -49.0), ], excludes: <Offset>[ const Offset(98.0, -32.0), // inside full size, outside small const Offset(-40.0, -32.0), // inside full size, outside small const Offset(90.1, -49.0), const Offset(-40.1, -49.0), ], ), ); await gesture.up(); // Test that the neck shrinks when the text scale gets larger. await tester.pumpWidget(buildApp('1000000', sliderValue: 0.0, textScale: 2.5)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..path( color: sliderTheme.valueIndicatorColor, includes: <Offset>[ const Offset(0.0, -38.8), const Offset(92.0, -38.8), const Offset(8.0, -23.0), // Inside large, outside scale=1.0 const Offset(-2.0, -23.0), // Inside large, outside scale=1.0 ], excludes: <Offset>[ const Offset(98.5, -38.8), const Offset(-16.1, -38.8), ], ), ); await gesture.up(); }); testWidgets('The slider track height can be overridden', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith(trackHeight: 16); const Radius radius = Radius.circular(8); const Radius activatedRadius = Radius.circular(9); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // Top and bottom are centerY (300) + and - trackRadius (8). expect( material, paints ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 291.0, 212.0, 309.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.activeTrackColor) ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 292.0, 776.0, 308.0, topRight: radius, bottomRight: radius), color: sliderTheme.inactiveTrackColor), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false)); await tester.pumpAndSettle(); // wait for disable animation // The disabled thumb is smaller so the active track has to paint longer to // get to the edge. expect( material, paints ..rrect(rrect: RRect.fromLTRBAndCorners(24.0, 291.0, 212.0, 309.0, topLeft: activatedRadius, bottomLeft: activatedRadius), color: sliderTheme.disabledActiveTrackColor) ..rrect(rrect: RRect.fromLTRBAndCorners(212.0, 292.0, 776.0, 308.0, topRight: radius, bottomRight: radius), color: sliderTheme.disabledInactiveTrackColor), ); }); testWidgets('The default slider thumb shape sizes can be overridden', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 7, disabledThumbRadius: 11, ), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect( material, paints..circle(x: 212, y: 300, radius: 7, color: sliderTheme.thumbColor), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false)); await tester.pumpAndSettle(); // wait for disable animation expect( material, paints..circle(x: 212, y: 300, radius: 11, color: sliderTheme.disabledThumbColor), ); }); testWidgets('The default slider thumb shape disabled size can be inferred from the enabled size', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 9, ), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect( material, paints..circle(x: 212, y: 300, radius: 9, color: sliderTheme.thumbColor), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.25, enabled: false)); await tester.pumpAndSettle(); // wait for disable animation expect( material, paints..circle(x: 212, y: 300, radius: 9, color: sliderTheme.disabledThumbColor), ); }); testWidgets('The default slider tick mark shape size can be overridden', (WidgetTester tester) async { final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 5), activeTickMarkColor: const Color(0xfadedead), inactiveTickMarkColor: const Color(0xfadebeef), disabledActiveTickMarkColor: const Color(0xfadecafe), disabledInactiveTickMarkColor: const Color(0xfadeface), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, divisions: 2)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect( material, paints ..circle(x: 26, y: 300, radius: 5, color: sliderTheme.activeTickMarkColor) ..circle(x: 400, y: 300, radius: 5, color: sliderTheme.activeTickMarkColor) ..circle(x: 774, y: 300, radius: 5, color: sliderTheme.inactiveTickMarkColor), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5, divisions: 2, enabled: false)); await tester.pumpAndSettle(); expect( material, paints ..circle(x: 26, y: 300, radius: 5, color: sliderTheme.disabledActiveTickMarkColor) ..circle(x: 400, y: 300, radius: 5, color: sliderTheme.disabledActiveTickMarkColor) ..circle(x: 774, y: 300, radius: 5, color: sliderTheme.disabledInactiveTickMarkColor), ); }); testWidgets('The default slider overlay shape size can be overridden', (WidgetTester tester) async { const double uniqueOverlayRadius = 23; final SliderThemeData sliderTheme = ThemeData().sliderTheme.copyWith( overlayShape: const RoundSliderOverlayShape( overlayRadius: uniqueOverlayRadius, ), ); await tester.pumpWidget(_buildApp(sliderTheme, value: 0.5)); // Tap center and wait for animation. final Offset center = tester.getCenter(find.byType(Slider)); await tester.startGesture(center); await tester.pumpAndSettle(); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect( material, paints..circle( x: center.dx, y: center.dy, radius: uniqueOverlayRadius, color: sliderTheme.overlayColor, ), ); }); // Only the thumb, overlay, and tick mark have special shortcuts to provide // no-op or empty shapes. // // The track can also be skipped by providing 0 height. // // The value indicator can be skipped by passing the appropriate // [ShowValueIndicator]. testWidgets('The slider can skip all of its component painting', (WidgetTester tester) async { // Pump a slider with all shapes skipped. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( trackHeight: 0, overlayShape: SliderComponentShape.noOverlay, thumbShape: SliderComponentShape.noThumb, tickMarkShape: SliderTickMarkShape.noTickMark, showValueIndicator: ShowValueIndicator.never, ), value: 0.5, divisions: 4, )); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect(material, paintsExactlyCountTimes(#drawRect, 0)); expect(material, paintsExactlyCountTimes(#drawCircle, 0)); expect(material, paintsExactlyCountTimes(#drawPath, 0)); }); testWidgets('The slider can skip all component painting except the track', (WidgetTester tester) async { // Pump a slider with just a track. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( overlayShape: SliderComponentShape.noOverlay, thumbShape: SliderComponentShape.noThumb, tickMarkShape: SliderTickMarkShape.noTickMark, showValueIndicator: ShowValueIndicator.never, ), value: 0.5, divisions: 4, )); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // Only 2 track segments. expect(material, paintsExactlyCountTimes(#drawRRect, 2)); expect(material, paintsExactlyCountTimes(#drawCircle, 0)); expect(material, paintsExactlyCountTimes(#drawPath, 0)); }); testWidgets('The slider can skip all component painting except the tick marks', (WidgetTester tester) async { // Pump a slider with just tick marks. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( trackHeight: 0, overlayShape: SliderComponentShape.noOverlay, thumbShape: SliderComponentShape.noThumb, showValueIndicator: ShowValueIndicator.never, // When the track is hidden to 0 height, a tick mark radius // must be provided to get a non-zero radius. tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 1), ), value: 0.5, divisions: 4, )); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // Only 5 tick marks. expect(material, paintsExactlyCountTimes(#drawRect, 0)); expect(material, paintsExactlyCountTimes(#drawCircle, 5)); expect(material, paintsExactlyCountTimes(#drawPath, 0)); }); testWidgets('The slider can skip all component painting except the thumb', (WidgetTester tester) async { // Pump a slider with just a thumb. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( trackHeight: 0, overlayShape: SliderComponentShape.noOverlay, tickMarkShape: SliderTickMarkShape.noTickMark, showValueIndicator: ShowValueIndicator.never, ), value: 0.5, divisions: 4, )); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // Only 1 thumb. expect(material, paintsExactlyCountTimes(#drawRect, 0)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); expect(material, paintsExactlyCountTimes(#drawPath, 0)); }); testWidgets('The slider can skip all component painting except the overlay', (WidgetTester tester) async { // Pump a slider with just an overlay. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( trackHeight: 0, thumbShape: SliderComponentShape.noThumb, tickMarkShape: SliderTickMarkShape.noTickMark, showValueIndicator: ShowValueIndicator.never, ), value: 0.5, divisions: 4, )); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // Tap the center of the track and wait for animations to finish. final Offset center = tester.getCenter(find.byType(Slider)); final TestGesture gesture = await tester.startGesture(center); await tester.pumpAndSettle(); // Only 1 overlay. expect(material, paintsExactlyCountTimes(#drawRect, 0)); expect(material, paintsExactlyCountTimes(#drawCircle, 1)); expect(material, paintsExactlyCountTimes(#drawPath, 0)); await gesture.up(); }); testWidgets('The slider can skip all component painting except the value indicator', (WidgetTester tester) async { // Pump a slider with just a value indicator. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( trackHeight: 0, overlayShape: SliderComponentShape.noOverlay, thumbShape: SliderComponentShape.noThumb, tickMarkShape: SliderTickMarkShape.noTickMark, showValueIndicator: ShowValueIndicator.always, ), value: 0.5, divisions: 4, )); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); // Tap the center of the track and wait for animations to finish. final Offset center = tester.getCenter(find.byType(Slider)); final TestGesture gesture = await tester.startGesture(center); await tester.pumpAndSettle(); // Only 1 value indicator. expect(material, paintsExactlyCountTimes(#drawRect, 0)); expect(material, paintsExactlyCountTimes(#drawCircle, 0)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 1)); await gesture.up(); }); testWidgets('PaddleSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async { // Pump a slider with just a value indicator. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( trackHeight: 0, overlayShape: SliderComponentShape.noOverlay, thumbShape: SliderComponentShape.noThumb, tickMarkShape: SliderTickMarkShape.noTickMark, showValueIndicator: ShowValueIndicator.always, valueIndicatorShape: const PaddleSliderValueIndicatorShape(), ), value: 0.5, divisions: 4, )); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); // Tap the center of the track to kick off the animation of the value indicator. final Offset center = tester.getCenter(find.byType(Slider)); final TestGesture gesture = await tester.startGesture(center); // Nothing to paint at scale 0. await tester.pump(); expect(material, paintsExactlyCountTimes(#drawRect, 0)); expect(material, paintsExactlyCountTimes(#drawCircle, 0)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 0)); // Painting a path for the value indicator. await tester.pump(const Duration(milliseconds: 16)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 1)); await gesture.up(); }); testWidgets('Default slider value indicator shape skips all painting at zero scale', (WidgetTester tester) async { // Pump a slider with just a value indicator. await tester.pumpWidget(_buildApp( ThemeData().sliderTheme.copyWith( trackHeight: 0, overlayShape: SliderComponentShape.noOverlay, thumbShape: SliderComponentShape.noThumb, tickMarkShape: SliderTickMarkShape.noTickMark, showValueIndicator: ShowValueIndicator.always, ), value: 0.5, divisions: 4, )); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); // Tap the center of the track to kick off the animation of the value indicator. final Offset center = tester.getCenter(find.byType(Slider)); final TestGesture gesture = await tester.startGesture(center); // Nothing to paint at scale 0. await tester.pump(); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 0)); // Painting a path for the value indicator. await tester.pump(const Duration(milliseconds: 16)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 1)); await gesture.up(); }); testWidgets('Default paddle range slider value indicator shape draws correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, ); final SliderThemeData sliderTheme = theme.sliderTheme.copyWith( thumbColor: Colors.red.shade500, showValueIndicator: ShowValueIndicator.always, rangeValueIndicatorShape: const PaddleRangeSliderValueIndicatorShape(), ); await tester.pumpWidget(_buildRangeApp(sliderTheme)); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); final Offset center = tester.getCenter(find.byType(RangeSlider)); final TestGesture gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( valueIndicatorBox, paints ..rrect(rrect: RRect.fromLTRBAndCorners( 24.0, 298.0, 24.0, 302.0, topLeft: const Radius.circular(2.0), topRight: const Radius.circular(0.0), bottomRight: const Radius.circular(0.0), bottomLeft: const Radius.circular(2.0), )) ..rect(rect: const Rect.fromLTRB(24.0, 297.0, 24.0, 303.0)) ..rrect(rrect: RRect.fromLTRBAndCorners( 24.0, 298.0, 776.0, 302.0, topLeft: const Radius.circular(0.0), topRight: const Radius.circular(2.0), bottomRight: const Radius.circular(2.0), bottomLeft: const Radius.circular(0.0), )) ..circle(x: 24.0, y: 300.0) ..shadow(elevation: 1.0) ..circle(x: 24.0, y: 300.0) ..shadow(elevation: 6.0) ..circle(x: 24.0, y: 300.0) ); await gesture.up(); }); testWidgets('PaddleRangeSliderValueIndicatorShape skips all painting at zero scale', (WidgetTester tester) async { // Pump a slider with just a value indicator. await tester.pumpWidget(_buildRangeApp( ThemeData().sliderTheme.copyWith( trackHeight: 0, rangeValueIndicatorShape: const PaddleRangeSliderValueIndicatorShape(), ), values: const RangeValues(0, 0.5), divisions: 4, )); // final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); // Tap the center of the track to kick off the animation of the value indicator. final Offset center = tester.getCenter(find.byType(RangeSlider)); final TestGesture gesture = await tester.startGesture(center); // No value indicator path to paint at scale 0. await tester.pump(); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 0)); // Painting a path for each value indicator. await tester.pump(const Duration(milliseconds: 16)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 2)); await gesture.up(); }); testWidgets('Default range indicator shape skips all painting at zero scale', (WidgetTester tester) async { // Pump a slider with just a value indicator. await tester.pumpWidget(_buildRangeApp( ThemeData().sliderTheme.copyWith( trackHeight: 0, overlayShape: SliderComponentShape.noOverlay, thumbShape: SliderComponentShape.noThumb, tickMarkShape: SliderTickMarkShape.noTickMark, showValueIndicator: ShowValueIndicator.always, ), values: const RangeValues(0, 0.5), divisions: 4, )); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); // Tap the center of the track to kick off the animation of the value indicator. final Offset center = tester.getCenter(find.byType(RangeSlider)); final TestGesture gesture = await tester.startGesture(center); // No value indicator path to paint at scale 0. await tester.pump(); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 0)); // Painting a path for each value indicator. await tester.pump(const Duration(milliseconds: 16)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 2)); await gesture.up(); }); } Widget _buildApp( SliderThemeData sliderTheme, { double value = 0.0, bool enabled = true, int divisions, }) { final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null; return MaterialApp( home: Scaffold( body: Center( child: SliderTheme( data: sliderTheme, child: Slider( value: value, label: '$value', onChanged: onChanged, divisions: divisions, ), ), ), ), ); } Widget _buildRangeApp( SliderThemeData sliderTheme, { RangeValues values = const RangeValues(0, 0), bool enabled = true, int divisions, }) { final ValueChanged<RangeValues> onChanged = enabled ? (RangeValues d) => values = d : null; return MaterialApp( home: Scaffold( body: Center( child: SliderTheme( data: sliderTheme, child: RangeSlider( values: values, labels: RangeLabels(values.start.toString(), values.end.toString()), onChanged: onChanged, divisions: divisions, ), ), ), ), ); }