From 39bb6712f65d05a6c0b3c5762f76c875d7bef310 Mon Sep 17 00:00:00 2001 From: Anthony Date: Mon, 11 Feb 2019 14:57:41 -0500 Subject: [PATCH] [Material] Simple API for skipping over certain Slider shapes (#27613) * Provide a simmplified API for skipping over slider thumb, overlay, and tick mark painting * doc fixes * comments * comments * comments * comments * comments * analyzer * comments --- .../lib/src/material/slider_theme.dart | 110 +++++++++++++ .../test/material/slider_theme_test.dart | 146 ++++++++++++++++++ 2 files changed, 256 insertions(+) diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index 3b38ff3d94..593d7509ca 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -154,6 +154,16 @@ enum ShowValueIndicator { /// [RoundSliderTickMarkShape], [PaddleSliderValueIndicatorShape], and /// [RoundSliderOverlayShape] for examples. /// +/// The track painting can be skipped by specifying 0 for [trackHeight]. +/// The thumb painting can be skipped by specifying +/// [SliderComponentShape.noThumb] for [SliderThemeData.thumbShape]. +/// The overlay painting can be skipped by specifying +/// [SliderComponentShape.noOverlay] for [SliderThemeData.overlayShape]. +/// The tick mark painting can be skipped by specifying +/// [SliderTickMarkShape.noTickMark] for [SliderThemeData.tickMarkShape]. +/// The value indicator painting can be skipped by specifying the +/// appropriate [ShowValueIndicator] for [SliderThemeData.showValueIndicator]. +/// /// See also: /// /// * [SliderTheme] widget, which can override the slider theme of its @@ -706,6 +716,9 @@ abstract class SliderTrackShape { /// This is a simplified version of [SliderComponentShape] with a /// [SliderThemeData] passed when getting the preferred size. /// +/// The tick mark painting can be skipped by specifying [noTickMark] for +/// [SliderThemeData.tickMarkShape]. +/// /// See also: /// /// * [RoundSliderTickMarkShape] for a simple example of a tick mark shape. @@ -759,6 +772,45 @@ abstract class SliderTickMarkShape { bool isEnabled, TextDirection textDirection, }); + + /// Special instance of [SliderTickMarkShape] to skip the tick mark painting. + /// + /// See also: + /// + /// * [SliderThemeData.tickMarkShape], which is the shape that the [Slider] + /// uses when painting tick marks. + static final SliderTickMarkShape noTickMark = _EmptySliderTickMarkShape(); +} + +/// A special version of [SliderTickMarkShape] that has a zero size and paints +/// nothing. +/// +/// This class is used to create a special instance of a [SliderTickMarkShape] +/// that will not paint any tick mark shape. A static reference is stored in +/// [SliderTickMarkShape.noTickMark]. When this value is specified for +/// [SliderThemeData.tickMarkShape], the tick mark painting is skipped. +class _EmptySliderTickMarkShape extends SliderTickMarkShape { + @override + Size getPreferredSize({ + SliderThemeData sliderTheme, + bool isEnabled, + }) { + return Size.zero; + } + + @override + void paint( + PaintingContext context, + Offset center, { + RenderBox parentBox, + SliderThemeData sliderTheme, + Animation enableAnimation, + Offset thumbCenter, + bool isEnabled, + TextDirection textDirection, + }) { + // no-op. + } } /// Base class for slider thumb, thumb overlay, and value indicator shapes. @@ -768,6 +820,12 @@ abstract class SliderTickMarkShape { /// All shapes are painted to the same canvas and ordering is important. /// The overlay is painted first, then the value indicator, then the thumb. /// +/// The thumb painting can be skipped by specifying [noThumb] for +/// [SliderThemeData.thumbShape]. +/// +/// The overlay painting can be skipped by specifying [noOverlay] for +/// [SliderThemeData.overlayShape]. +/// /// See also: /// /// * [RoundSliderThumbShape], which is the the default thumb shape. @@ -821,6 +879,52 @@ abstract class SliderComponentShape { TextDirection textDirection, double value, }); + + /// Special instance of [SliderComponentShape] to skip the thumb drawing. + /// + /// See also: + /// + /// * [SliderThemeData.thumbShape], which is the shape that the [Slider] + /// uses when painting the thumb. + static final SliderComponentShape noThumb = _EmptySliderComponentShape(); + + /// Special instance of [SliderComponentShape] to skip the overlay drawing. + /// + /// See also: + /// + /// * [SliderThemeData.overlayShape], which is the shape that the [Slider] + /// uses when painting the overlay. + static final SliderComponentShape noOverlay = _EmptySliderComponentShape(); +} + +/// A special version of [SliderComponentShape] that has a zero size and paints +/// nothing. +/// +/// This class is used to create a special instance of a [SliderComponentShape] +/// that will not paint any component shape. A static reference is stored in +/// [SliderTickMarkShape.noThumb] and [SliderTickMarkShape.noOverlay]. When this value +/// is specified for [SliderThemeData.thumbShape], the thumb painting is +/// skipped. When this value is specified for [SliderThemeData.overlaySHape], +/// the overlay painting is skipped. +class _EmptySliderComponentShape extends SliderComponentShape { + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero; + + @override + void paint( + PaintingContext context, + Offset center, { + Animation activationAnimation, + Animation enableAnimation, + bool isDiscrete, + TextPainter labelPainter, + RenderBox parentBox, + SliderThemeData sliderTheme, + TextDirection textDirection, + double value, + }) { + // no-op. + } } // The following shapes are the material defaults. @@ -894,6 +998,12 @@ class RectangularSliderTrackShape extends SliderTrackShape { bool isDiscrete, bool isEnabled, }) { + // If the slider track height is 0, then it makes no difference whether the + // track is painted or not, therefore the painting can be a no-op. + if (sliderTheme.trackHeight == 0) { + return; + } + // Assign the track segment paints, which are left: active, right: inactive, // but reversed for right to left text. final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor); diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart index 4519f010bd..74c93da779 100644 --- a/packages/flutter/test/material/slider_theme_test.dart +++ b/packages/flutter/test/material/slider_theme_test.dart @@ -590,6 +590,152 @@ void main() { ) ); }); + + // 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 comoponent 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 RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + + expect(sliderBox, paintsNothing); + }); + + 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 RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + + // Only 2 track segments. + expect(sliderBox, paintsExactlyCountTimes(#drawRect, 2)); + expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 0)); + expect(sliderBox, 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, + ), + value: 0.5, + divisions: 4 + )); + + final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + + // Only 5 tick marks. + expect(sliderBox, paintsExactlyCountTimes(#drawRect, 0)); + expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 5)); + expect(sliderBox, 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 RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + + // Only 1 thumb. + expect(sliderBox, paintsExactlyCountTimes(#drawRect, 0)); + expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 1)); + expect(sliderBox, 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 RenderBox sliderBox = tester.firstRenderObject(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(sliderBox, paintsExactlyCountTimes(#drawRect, 0)); + expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 1)); + expect(sliderBox, 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 RenderBox sliderBox = tester.firstRenderObject(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 value indicator. + expect(sliderBox, paintsExactlyCountTimes(#drawRect, 0)); + expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 0)); + expect(sliderBox, paintsExactlyCountTimes(#drawPath, 1)); + + await gesture.up(); + }); } Widget _buildApp(