diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index e7d25c0c5a..33eb21c338 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:math' as math; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -584,15 +585,23 @@ class _RenderSlider extends RenderBox { curve: Curves.easeInOut, ); } - static const Duration _positionAnimationDuration = Duration(milliseconds: 75); - static const double _overlayRadius = 16.0; - static const double _overlayDiameter = _overlayRadius * 2.0; - static const double _trackHeight = 2.0; - static const double _preferredTrackWidth = 144.0; - static const double _preferredTotalWidth = _preferredTrackWidth + _overlayDiameter; static const Duration _minimumInteractionTime = Duration(milliseconds: 500); - static final Animatable _overlayRadiusTween = Tween(begin: 0.0, end: _overlayRadius); + + // This value is the touch target, 48, multiplied by 3. + static const double _minPreferredTrackWidth = 144.0; + + // Compute the largest width and height needed to paint the slider shapes, + // other than the track shape. It is assumed that these shapes are vertically + // centered on the track. + double get _maxSliderPartWidth => _sliderPartSizes.map((Size size) => size.width).reduce(math.max); + double get _maxSliderPartHeight => _sliderPartSizes.map((Size size) => size.width).reduce(math.max); + List get _sliderPartSizes => [ + _sliderTheme.overlayShape.getPreferredSize(isInteractive, isDiscrete), + _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete), + _sliderTheme.tickMarkShape.getPreferredSize(isEnabled: isInteractive, sliderTheme: sliderTheme), + ]; + double get _minPreferredTrackHeight =>_sliderTheme.trackHeight; _SliderState _state; Animation _overlayAnimation; @@ -604,7 +613,15 @@ class _RenderSlider extends RenderBox { bool _active = false; double _currentDragValue = 0.0; - double get _trackLength => size.width - _overlayDiameter; + // This rect is used in gesture calculations, where the gesture coordinates + // are relative to the sliders origin. Therefore, the offset is passed as + // (0,0). + Rect get _trackRect => _sliderTheme.trackShape.getPreferredRect( + parentBox: this, + offset: Offset.zero, + sliderTheme: _sliderTheme, + isDiscrete: false, + ); bool get isInteractive => onChanged != null; @@ -818,7 +835,7 @@ class _RenderSlider extends RenderBox { } double _getValueFromGlobalPosition(Offset globalPosition) { - final double visualPosition = (globalToLocal(globalPosition).dx - _overlayRadius) / _trackLength; + final double visualPosition = (globalToLocal(globalPosition).dx - _trackRect.left) / _trackRect.width; return _getValueFromVisualPosition(visualPosition); } @@ -874,7 +891,7 @@ class _RenderSlider extends RenderBox { void _handleDragUpdate(DragUpdateDetails details) { if (isInteractive) { - final double valueDelta = details.primaryDelta / _trackLength; + final double valueDelta = details.primaryDelta / _trackRect.width; switch (textDirection) { case TextDirection.rtl: _currentDragValue -= valueDelta; @@ -907,25 +924,16 @@ class _RenderSlider extends RenderBox { } @override - double computeMinIntrinsicWidth(double height) { - return math.max( - _overlayDiameter, - _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width, - ); - } + double computeMinIntrinsicWidth(double height) => _minPreferredTrackWidth + _maxSliderPartWidth; @override - double computeMaxIntrinsicWidth(double height) { - // This doesn't quite match the definition of computeMaxIntrinsicWidth, - // but it seems within the spirit... - return _preferredTotalWidth; - } + double computeMaxIntrinsicWidth(double height) => _minPreferredTrackWidth + _maxSliderPartWidth; @override - double computeMinIntrinsicHeight(double width) => _overlayDiameter; + double computeMinIntrinsicHeight(double width) => max(_minPreferredTrackHeight, _maxSliderPartHeight); @override - double computeMaxIntrinsicHeight(double width) => _overlayDiameter; + double computeMaxIntrinsicHeight(double width) => max(_minPreferredTrackHeight, _maxSliderPartHeight); @override bool get sizedByParent => true; @@ -933,126 +941,91 @@ class _RenderSlider extends RenderBox { @override void performResize() { size = Size( - constraints.hasBoundedWidth ? constraints.maxWidth : _preferredTotalWidth, - constraints.hasBoundedHeight ? constraints.maxHeight : _overlayDiameter, + constraints.hasBoundedWidth ? constraints.maxWidth : _minPreferredTrackWidth + _maxSliderPartWidth, + constraints.hasBoundedHeight ? constraints.maxHeight : max(_minPreferredTrackHeight, _maxSliderPartHeight), ); } - void _paintTickMarks( - Canvas canvas, - Rect trackLeft, - Rect trackRight, - Paint leftPaint, - Paint rightPaint, - ) { - if (isDiscrete) { - // The ticks are tiny circles that are the same height as the track. - const double tickRadius = _trackHeight / 2.0; - final double trackWidth = trackRight.right - trackLeft.left; - final double dx = (trackWidth - _trackHeight) / divisions; - // If the ticks would be too dense, don't bother painting them. - if (dx >= 3.0 * _trackHeight) { - for (int i = 0; i <= divisions; i += 1) { - final double left = trackLeft.left + i * dx; - final Offset center = Offset(left + tickRadius, trackLeft.top + tickRadius); - if (trackLeft.contains(center)) { - canvas.drawCircle(center, tickRadius, leftPaint); - } else if (trackRight.contains(center)) { - canvas.drawCircle(center, tickRadius, rightPaint); - } - } - } - } - } - - void _paintOverlay(Canvas canvas, Offset center) { - if (!_overlayAnimation.isDismissed) { - // TODO(gspencer): We don't really follow the spec here for overlays. - // The spec says to use 16% opacity for drawing over light material, - // and 32% for colored material, but we don't really have a way to - // know what the underlying color is, so there's no easy way to - // implement this. Choosing the "light" version for now. - final Paint overlayPaint = Paint()..color = _sliderTheme.overlayColor; - final double radius = _overlayRadiusTween.evaluate(_overlayAnimation); - canvas.drawCircle(center, radius, overlayPaint); - } - } - @override void paint(PaintingContext context, Offset offset) { - final Canvas canvas = context.canvas; - - final double trackLength = size.width - 2 * _overlayRadius; final double value = _state.positionController.value; - final ColorTween activeTrackEnableColor = ColorTween(begin: _sliderTheme.disabledActiveTrackColor, end: _sliderTheme.activeTrackColor); - final ColorTween inactiveTrackEnableColor = ColorTween(begin: _sliderTheme.disabledInactiveTrackColor, end: _sliderTheme.inactiveTrackColor); - final ColorTween activeTickMarkEnableColor = ColorTween(begin: _sliderTheme.disabledActiveTickMarkColor, end: _sliderTheme.activeTickMarkColor); - final ColorTween inactiveTickMarkEnableColor = ColorTween(begin: _sliderTheme.disabledInactiveTickMarkColor, end: _sliderTheme.inactiveTickMarkColor); - - final Paint activeTrackPaint = Paint()..color = activeTrackEnableColor.evaluate(_enableAnimation); - final Paint inactiveTrackPaint = Paint()..color = inactiveTrackEnableColor.evaluate(_enableAnimation); - final Paint activeTickMarkPaint = Paint()..color = activeTickMarkEnableColor.evaluate(_enableAnimation); - final Paint inactiveTickMarkPaint = Paint()..color = inactiveTickMarkEnableColor.evaluate(_enableAnimation); + // The visual position is the position of the thumb from 0 to 1 from left + // to right. In left to right, this is the same as the value, but it is + // reversed for right to left text. double visualPosition; - Paint leftTrackPaint; - Paint rightTrackPaint; - Paint leftTickMarkPaint; - Paint rightTickMarkPaint; switch (textDirection) { case TextDirection.rtl: visualPosition = 1.0 - value; - leftTrackPaint = inactiveTrackPaint; - rightTrackPaint = activeTrackPaint; - leftTickMarkPaint = inactiveTickMarkPaint; - rightTickMarkPaint = activeTickMarkPaint; break; case TextDirection.ltr: visualPosition = value; - leftTrackPaint = activeTrackPaint; - rightTrackPaint = inactiveTrackPaint; - leftTickMarkPaint = activeTickMarkPaint; - rightTickMarkPaint = inactiveTickMarkPaint; break; } - const double trackRadius = _trackHeight / 2.0; - const double thumbGap = 2.0; + final Rect trackRect = _sliderTheme.trackShape.getPreferredRect( + parentBox: this, + offset: offset, + sliderTheme: _sliderTheme, + isDiscrete: isDiscrete + ); + final Offset thumbCenter = Offset(trackRect.left + visualPosition * trackRect.width, trackRect.center.dy); - final double trackVerticalCenter = offset.dy + (size.height) / 2.0; - final double trackLeft = offset.dx + _overlayRadius; - final double trackTop = trackVerticalCenter - trackRadius; - final double trackBottom = trackVerticalCenter + trackRadius; - final double trackRight = trackLeft + trackLength; - final double trackActive = trackLeft + trackLength * visualPosition; - final double thumbRadius = _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width / 2.0; - final double trackActiveLeft = math.max(0.0, trackActive - thumbRadius - thumbGap * (1.0 - _enableAnimation.value)); - final double trackActiveRight = math.min(trackActive + thumbRadius + thumbGap * (1.0 - _enableAnimation.value), trackRight); - final Rect trackLeftRect = Rect.fromLTRB(trackLeft, trackTop, trackActiveLeft, trackBottom); - final Rect trackRightRect = Rect.fromLTRB(trackActiveRight, trackTop, trackRight, trackBottom); - - final Offset thumbCenter = Offset(trackActive, trackVerticalCenter); - - // Paint the track. - if (visualPosition > 0.0) { - canvas.drawRect(trackLeftRect, leftTrackPaint); - } - if (visualPosition < 1.0) { - canvas.drawRect(trackRightRect, rightTrackPaint); - } - - _paintOverlay(canvas, thumbCenter); - - _paintTickMarks( - canvas, - trackLeftRect, - trackRightRect, - leftTickMarkPaint, - rightTickMarkPaint, + _sliderTheme.trackShape.paint( + context, + offset, + parentBox: this, + sliderTheme: _sliderTheme, + enableAnimation: _enableAnimation, + textDirection: _textDirection, + thumbCenter: thumbCenter, + isDiscrete: isDiscrete, + isEnabled: isInteractive ); - if (isInteractive && label != null && - _valueIndicatorAnimation.status != AnimationStatus.dismissed) { + // TODO(closkmith): Move this to paint after the thumb. + if (!_overlayAnimation.isDismissed) { + _sliderTheme.overlayShape.paint( + context, + thumbCenter, + activationAnimation: _overlayAnimation, + enableAnimation: _enableAnimation, + isDiscrete: isDiscrete, + labelPainter: _labelPainter, + parentBox: this, + sliderTheme: _sliderTheme, + textDirection: _textDirection, + value: _value, + ); + } + + if (isDiscrete) { + // TODO(clocksmith): Align tick mark centers to ends of track by not subtracting diameter from length. + final double tickMarkWidth = _sliderTheme.tickMarkShape.getPreferredSize( + isEnabled: isInteractive, + sliderTheme: _sliderTheme, + ).width; + for (int i = 0; i <= divisions; i++) { + final double tickValue = i / divisions; + // The ticks are mapped to be within the track, so the tick mark width + // must be subtracted from the track width. + final double tickX = trackRect.left + tickValue * (trackRect.width - tickMarkWidth) + tickMarkWidth / 2; + final double tickY = trackRect.center.dy; + final Offset tickMarkOffset = Offset(tickX, tickY); + _sliderTheme.tickMarkShape.paint( + context, + tickMarkOffset, + parentBox: this, + sliderTheme: _sliderTheme, + enableAnimation: _enableAnimation, + textDirection: _textDirection, + thumbCenter: thumbCenter, + isEnabled: isInteractive, + ); + } + } + + if (isInteractive && label != null && !_valueIndicatorAnimation.isDismissed) { if (showValueIndicator) { _sliderTheme.valueIndicatorShape.paint( context, @@ -1117,4 +1090,4 @@ class _RenderSlider extends RenderBox { onChanged((value - _semanticActionUnit).clamp(0.0, 1.0)); } } -} +} \ No newline at end of file diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart index efa9788ad4..5734290c23 100644 --- a/packages/flutter/lib/src/material/slider_theme.dart +++ b/packages/flutter/lib/src/material/slider_theme.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:math' as math; -import 'dart:ui' show Path; +import 'dart:ui' show Path, lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -21,12 +21,21 @@ import 'theme_data.dart'; /// [SliderTheme.of]. When a widget uses [SliderTheme.of], it is automatically /// rebuilt if the theme later changes. /// +/// The slider is as big as the largest of +/// the [SliderComponentShape.getPreferredSize] of the thumb shape, +/// the [SliderComponentShape.getPreferredSize] of the overlay shape, +/// and the [SliderTickMarkShape.getPreferredSize] of the tick mark shape +/// /// See also: /// /// * [SliderThemeData], which describes the actual configuration of a slider /// theme. /// * [SliderComponentShape], which can be used to create custom shapes for -/// the slider thumb and value indicator. +/// the slider thumb, overlay, and value indicator. +/// * [SliderTrackShape], which can be used to create custom shapes for the +/// slider track. +/// * [SliderTickMarkShape], which can be used to create custom shapes for the +/// slider tick marks. class SliderTheme extends InheritedWidget { /// Applies the given theme [data] to [child]. /// @@ -123,8 +132,14 @@ enum ShowValueIndicator { /// * The "thumb", which is a shape that slides horizontally when the user /// drags it. /// * The "track", which is the line that the slider thumb slides along. -/// * The "value indicator", which is a shape that pops up when the user -/// is dragging the thumb to indicate the value being selected. +/// * The "tick marks", which are regularly spaced marks that are drawn when +/// using discrete divisions. +/// * The "value indicator", which appears when the user is dragging the thumb +/// to indicate the value being selected. +/// * The "overlay", which appears around the thumb, and is shown when the +/// thumb is pressed, focused, or hovered. It is painted underneath the +/// thumb, so it must extend beyond the bounds of the thumb itself to +/// actually be visible. /// * The "active" side of the slider is the side between the thumb and the /// minimum value. /// * The "inactive" side of the slider is the side between the thumb and the @@ -132,10 +147,12 @@ enum ShowValueIndicator { /// * The [Slider] is disabled when it is not accepting user input. See /// [Slider] for details on when this happens. /// -/// The thumb and the value indicator may have their shapes and behavior -/// customized by creating your own [SliderComponentShape] that does what -/// you want. See [RoundSliderThumbShape] and -/// [PaddleSliderValueIndicatorShape] for examples. +/// The thumb, track, tick marks, value indicator, and overlay can be customized +/// by creating subclasses of [SliderTrackShape], +/// [SliderComponentShape], and/or [SliderTickMarkShape]. See +/// [RoundSliderThumbShape], [RectangularSliderTrackShape], +/// [RoundSliderTickMarkShape], [PaddleSliderValueIndicatorShape], and +/// [RoundSliderOverlayShape] for examples. /// /// See also: /// @@ -144,7 +161,9 @@ enum ShowValueIndicator { /// * [Theme] widget, which performs a similar function to [SliderTheme], /// but for overall themes. /// * [ThemeData], which has a default [SliderThemeData]. +/// * [SliderTrackShape], to define custom slider track shapes. /// * [SliderComponentShape], to define custom slider component shapes. +/// * [SliderTickMarkShape], to define custom slider tick mark shapes. class SliderThemeData extends Diagnosticable { /// Create a [SliderThemeData] given a set of exact values. All the values /// must be specified. @@ -181,6 +200,7 @@ class SliderThemeData extends Diagnosticable { /// ``` /// {@end-tool} const SliderThemeData({ + @required this.trackHeight, @required this.activeTrackColor, @required this.inactiveTrackColor, @required this.disabledActiveTrackColor, @@ -193,26 +213,33 @@ class SliderThemeData extends Diagnosticable { @required this.disabledThumbColor, @required this.overlayColor, @required this.valueIndicatorColor, + @required this.trackShape, + @required this.tickMarkShape, @required this.thumbShape, + @required this.overlayShape, @required this.valueIndicatorShape, @required this.showValueIndicator, @required this.valueIndicatorTextStyle, - }) : assert(activeTrackColor != null), - assert(inactiveTrackColor != null), - assert(disabledActiveTrackColor != null), - assert(disabledInactiveTrackColor != null), - assert(activeTickMarkColor != null), - assert(inactiveTickMarkColor != null), - assert(disabledActiveTickMarkColor != null), - assert(disabledInactiveTickMarkColor != null), - assert(thumbColor != null), - assert(disabledThumbColor != null), - assert(overlayColor != null), - assert(valueIndicatorColor != null), - assert(thumbShape != null), - assert(valueIndicatorShape != null), - assert(valueIndicatorTextStyle != null), - assert(showValueIndicator != null); + }) : assert(trackHeight != null), + assert(activeTrackColor != null), + assert(inactiveTrackColor != null), + assert(disabledActiveTrackColor != null), + assert(disabledInactiveTrackColor != null), + assert(activeTickMarkColor != null), + assert(inactiveTickMarkColor != null), + assert(disabledActiveTickMarkColor != null), + assert(disabledInactiveTickMarkColor != null), + assert(thumbColor != null), + assert(disabledThumbColor != null), + assert(overlayColor != null), + assert(valueIndicatorColor != null), + assert(trackShape != null), + assert(tickMarkShape != null), + assert(thumbShape != null), + assert(overlayShape != null), + assert(valueIndicatorShape != null), + assert(valueIndicatorTextStyle != null), + assert(showValueIndicator != null); /// Generates a SliderThemeData from three main colors. /// @@ -256,6 +283,7 @@ class SliderThemeData extends Diagnosticable { const int overlayLightAlpha = 0x29; // 16% opacity return SliderThemeData( + trackHeight: 2.0, activeTrackColor: primaryColor.withAlpha(activeTrackAlpha), inactiveTrackColor: primaryColor.withAlpha(inactiveTrackAlpha), disabledActiveTrackColor: primaryColorDark.withAlpha(disabledActiveTrackAlpha), @@ -268,13 +296,19 @@ class SliderThemeData extends Diagnosticable { disabledThumbColor: primaryColorDark.withAlpha(disabledThumbAlpha), overlayColor: primaryColor.withAlpha(overlayLightAlpha), valueIndicatorColor: primaryColor.withAlpha(valueIndicatorAlpha), + trackShape: const RectangularSliderTrackShape(), + tickMarkShape: const RoundSliderTickMarkShape(), thumbShape: const RoundSliderThumbShape(), + overlayShape: const RoundSliderOverlayShape(), valueIndicatorShape: const PaddleSliderValueIndicatorShape(), valueIndicatorTextStyle: valueIndicatorTextStyle, showValueIndicator: ShowValueIndicator.onlyForDiscrete, ); } + /// The height of the [Slider] track. + final double trackHeight; + /// The color of the [Slider] track between the [Slider.min] position and the /// current thumb position. final Color activeTrackColor; @@ -323,17 +357,39 @@ class SliderThemeData extends Diagnosticable { /// The color given to the [valueIndicatorShape] to draw itself with. final Color valueIndicatorColor; - /// The shape and behavior that will be used to draw the [Slider]'s thumb. + /// The shape that will be used to draw the [Slider]'s track. /// - /// This can be customized by implementing a subclass of - /// [SliderComponentShape]. + /// The [SliderTrackShape.getPreferredRect] method is used to to map + /// slider-relative gesture coordinates to the correct thumb position on the + /// track. It is also used to horizontally position tick marks, when he slider + /// is discrete. + /// + /// The default value is [RectangularSliderTrackShape]. + final SliderTrackShape trackShape; + + /// The shape that will be used to draw the [Slider]'s tick marks. + /// + /// The [SliderTickMarkShape.getPreferredSize] is used to help determine the + /// location of each tick mark on the track. The slider's minimum size will + /// be at least this big. + /// + /// The default value is [RoundSliderTickMarkShape]. + final SliderTickMarkShape tickMarkShape; + + /// The shape that will be used to draw the [Slider]'s overlay. + /// + /// Both the [overlayColor] and a non default [overlayShape] may be specified. + /// In this case, the [overlayColor] is only used if the [overlayShape] + /// explicitly does so. + /// + /// The default value is [RoundSliderOverlayShape]. + final SliderComponentShape overlayShape; + + /// The shape that will be used to draw the [Slider]'s thumb. final SliderComponentShape thumbShape; - /// The shape and behavior that will be used to draw the [Slider]'s value + /// The shape that will be used to draw the [Slider]'s value /// indicator. - /// - /// This can be customized by implementing a subclass of - /// [SliderComponentShape]. final SliderComponentShape valueIndicatorShape; /// Whether the value indicator should be shown for different types of @@ -352,6 +408,7 @@ class SliderThemeData extends Diagnosticable { /// Creates a copy of this object but with the given fields replaced with the /// new values. SliderThemeData copyWith({ + double trackHeight, Color activeTrackColor, Color inactiveTrackColor, Color disabledActiveTrackColor, @@ -364,12 +421,16 @@ class SliderThemeData extends Diagnosticable { Color disabledThumbColor, Color overlayColor, Color valueIndicatorColor, + SliderTrackShape trackShape, + SliderTickMarkShape tickMarkShape, SliderComponentShape thumbShape, + SliderComponentShape overlayShape, SliderComponentShape valueIndicatorShape, ShowValueIndicator showValueIndicator, TextStyle valueIndicatorTextStyle, }) { return SliderThemeData( + trackHeight: trackHeight ?? this.trackHeight, activeTrackColor: activeTrackColor ?? this.activeTrackColor, inactiveTrackColor: inactiveTrackColor ?? this.inactiveTrackColor, disabledActiveTrackColor: disabledActiveTrackColor ?? this.disabledActiveTrackColor, @@ -382,7 +443,10 @@ class SliderThemeData extends Diagnosticable { disabledThumbColor: disabledThumbColor ?? this.disabledThumbColor, overlayColor: overlayColor ?? this.overlayColor, valueIndicatorColor: valueIndicatorColor ?? this.valueIndicatorColor, + trackShape: trackShape ?? this.trackShape, + tickMarkShape: tickMarkShape ?? this.tickMarkShape, thumbShape: thumbShape ?? this.thumbShape, + overlayShape: overlayShape ?? this.overlayShape, valueIndicatorShape: valueIndicatorShape ?? this.valueIndicatorShape, showValueIndicator: showValueIndicator ?? this.showValueIndicator, valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle, @@ -399,6 +463,7 @@ class SliderThemeData extends Diagnosticable { assert(b != null); assert(t != null); return SliderThemeData( + trackHeight: lerpDouble(a.trackHeight, b.trackHeight, t), activeTrackColor: Color.lerp(a.activeTrackColor, b.activeTrackColor, t), inactiveTrackColor: Color.lerp(a.inactiveTrackColor, b.inactiveTrackColor, t), disabledActiveTrackColor: Color.lerp(a.disabledActiveTrackColor, b.disabledActiveTrackColor, t), @@ -411,7 +476,10 @@ class SliderThemeData extends Diagnosticable { disabledThumbColor: Color.lerp(a.disabledThumbColor, b.disabledThumbColor, t), overlayColor: Color.lerp(a.overlayColor, b.overlayColor, t), valueIndicatorColor: Color.lerp(a.valueIndicatorColor, b.valueIndicatorColor, t), + trackShape: t < 0.5 ? a.trackShape : b.trackShape, + tickMarkShape: t < 0.5 ? a.tickMarkShape : b.tickMarkShape, thumbShape: t < 0.5 ? a.thumbShape : b.thumbShape, + overlayShape: t < 0.5 ? a.overlayShape : b.overlayShape, valueIndicatorShape: t < 0.5 ? a.valueIndicatorShape : b.valueIndicatorShape, showValueIndicator: t < 0.5 ? a.showValueIndicator : b.showValueIndicator, valueIndicatorTextStyle: TextStyle.lerp(a.valueIndicatorTextStyle, b.valueIndicatorTextStyle, t), @@ -421,6 +489,7 @@ class SliderThemeData extends Diagnosticable { @override int get hashCode { return hashValues( + trackHeight, activeTrackColor, inactiveTrackColor, disabledActiveTrackColor, @@ -433,7 +502,10 @@ class SliderThemeData extends Diagnosticable { disabledThumbColor, overlayColor, valueIndicatorColor, + trackShape, + tickMarkShape, thumbShape, + overlayShape, valueIndicatorShape, showValueIndicator, valueIndicatorTextStyle, @@ -449,22 +521,26 @@ class SliderThemeData extends Diagnosticable { return false; } final SliderThemeData otherData = other; - return otherData.activeTrackColor == activeTrackColor && - otherData.inactiveTrackColor == inactiveTrackColor && - otherData.disabledActiveTrackColor == disabledActiveTrackColor && - otherData.disabledInactiveTrackColor == disabledInactiveTrackColor && - otherData.activeTickMarkColor == activeTickMarkColor && - otherData.inactiveTickMarkColor == inactiveTickMarkColor && - otherData.disabledActiveTickMarkColor == disabledActiveTickMarkColor && - otherData.disabledInactiveTickMarkColor == disabledInactiveTickMarkColor && - otherData.thumbColor == thumbColor && - otherData.disabledThumbColor == disabledThumbColor && - otherData.overlayColor == overlayColor && - otherData.valueIndicatorColor == valueIndicatorColor && - otherData.thumbShape == thumbShape && - otherData.valueIndicatorShape == valueIndicatorShape && - otherData.showValueIndicator == showValueIndicator && - otherData.valueIndicatorTextStyle == valueIndicatorTextStyle; + return otherData.trackHeight == trackHeight + && otherData.activeTrackColor == activeTrackColor + && otherData.inactiveTrackColor == inactiveTrackColor + && otherData.disabledActiveTrackColor == disabledActiveTrackColor + && otherData.disabledInactiveTrackColor == disabledInactiveTrackColor + && otherData.activeTickMarkColor == activeTickMarkColor + && otherData.inactiveTickMarkColor == inactiveTickMarkColor + && otherData.disabledActiveTickMarkColor == disabledActiveTickMarkColor + && otherData.disabledInactiveTickMarkColor == disabledInactiveTickMarkColor + && otherData.thumbColor == thumbColor + && otherData.disabledThumbColor == disabledThumbColor + && otherData.overlayColor == overlayColor + && otherData.valueIndicatorColor == valueIndicatorColor + && otherData.trackShape == trackShape + && otherData.tickMarkShape == tickMarkShape + && otherData.thumbShape == thumbShape + && otherData.overlayShape == overlayShape + && otherData.valueIndicatorShape == valueIndicatorShape + && otherData.showValueIndicator == showValueIndicator + && otherData.valueIndicatorTextStyle == valueIndicatorTextStyle; } @override @@ -478,6 +554,7 @@ class SliderThemeData extends Diagnosticable { valueIndicatorTextStyle: defaultTheme.accentTextTheme.body2, ); properties.add(DiagnosticsProperty('activeTrackColor', activeTrackColor, defaultValue: defaultData.activeTrackColor)); + properties.add(DiagnosticsProperty('activeTrackColor', activeTrackColor, defaultValue: defaultData.activeTrackColor)); properties.add(DiagnosticsProperty('inactiveTrackColor', inactiveTrackColor, defaultValue: defaultData.inactiveTrackColor)); properties.add(DiagnosticsProperty('disabledActiveTrackColor', disabledActiveTrackColor, defaultValue: defaultData.disabledActiveTrackColor, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('disabledInactiveTrackColor', disabledInactiveTrackColor, defaultValue: defaultData.disabledInactiveTrackColor, level: DiagnosticLevel.debug)); @@ -489,22 +566,213 @@ class SliderThemeData extends Diagnosticable { properties.add(DiagnosticsProperty('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('overlayColor', overlayColor, defaultValue: defaultData.overlayColor, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('valueIndicatorColor', valueIndicatorColor, defaultValue: defaultData.valueIndicatorColor)); + properties.add(DiagnosticsProperty('trackShape', trackShape, defaultValue: defaultData.trackShape, level: DiagnosticLevel.debug)); + properties.add(DiagnosticsProperty('tickMarkShape', tickMarkShape, defaultValue: defaultData.tickMarkShape, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('thumbShape', thumbShape, defaultValue: defaultData.thumbShape, level: DiagnosticLevel.debug)); + properties.add(DiagnosticsProperty('overlayShape', overlayShape, defaultValue: defaultData.overlayShape, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('valueIndicatorShape', valueIndicatorShape, defaultValue: defaultData.valueIndicatorShape, level: DiagnosticLevel.debug)); properties.add(EnumProperty('showValueIndicator', showValueIndicator, defaultValue: defaultData.showValueIndicator)); properties.add(DiagnosticsProperty('valueIndicatorTextStyle', valueIndicatorTextStyle, defaultValue: defaultData.valueIndicatorTextStyle)); } } -/// Base class for slider thumb and value indicator shapes. +// TEMPLATES FOR ALL SHAPES + +/// {@template flutter.material.slider.shape.context} +/// [context] is the same context for the render box of the [Slider]. +/// {@endtemplate} /// -/// Create a subclass of this if you would like a custom slider thumb or -/// value indicator shape. +/// {@template flutter.material.slider.shape.center} +/// [center] is the offset of the center where this shape should be painted. +/// This offset is relative to the origin of the [context] canvas. +/// {@endtemplate} +/// +/// {@template flutter.material.slider.shape.sliderTheme} +/// [sliderTheme] is the theme assigned to the [Slider] that this shape +/// belongs to. +/// {@endtemplate} +/// +/// {@template flutter.material.slider.shape.isEnabled} +/// [isEnabled] has the same value as [Slider.isInteractive]. If true, the +/// slider will respond to input. +/// {@endtemplate} +/// +/// {@template flutter.material.slider.shape.enableAnimation} +/// [enableAnimation] is an animation triggered when the [Slider] is enabled, +/// and it reverses when the slider is disabled. Enabled is the +/// [Slider.isInteractive] state. Use this to paint intermediate frames for +/// this shape when the slider changes enabled state. +/// {@endtemplate} +/// +/// {@template flutter.material.slider.shape.isDiscrete} +/// [isDiscrete] is true if [Slider.divisions] is non-null. If true, the +/// slider will render tick marks on top of the track. +/// {@endtemplate} +/// +/// {@template flutter.material.slider.shape.parentBox} +/// [parentBox] is the [RenderBox] of the [Slider]. Its attributes, such as +/// size, can be used to assist in painting this shape. +/// {@endtemplate} + +/// Base class for slider track shapes. +/// +/// The slider's thumb moves along the track. A discrete slider's tick marks +/// are drawn after the track, but before the thumb, and are aligned with the +/// track. +/// +/// The [getPreferredRect] helps position the slider thumb and tick marks +/// relative to the track. /// /// See also: /// -/// * [RoundSliderThumbShape] for a simple example of a thumb shape. -/// * [PaddleSliderValueIndicatorShape], for a complex example of a value +/// * [RectangularSliderTrackShape], which is the the default track shape. +/// * [SliderTickMarkShape], which is the default tick mark shape. +/// * [SliderComponentShape], which is the base class for custom a component +/// shape. +abstract class SliderTrackShape { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const SliderTrackShape(); + + /// Returns the preferred bounds of the shape. + /// + /// It is used to provide horizontal boundaries for the thumb's position, and + /// to help position the slider thumb and tick marks relative to the track. + /// + /// [parentBox] can be used to help determine the preferredRect relative to + /// attributes of the render box of the slider itself, such as size. + /// + /// [offset] is relative to the caller's bounding box. It can be used to + /// convert gesture coordinates from global to slider-relative coordinates. + /// + /// {@macro flutter.material.slider.shape.sliderTheme} + /// + /// {@macro flutter.material.slider.shape.isEnabled} + /// + /// {@macro flutter.material.slider.shape.isDiscrete} + Rect getPreferredRect({ + RenderBox parentBox, + Offset offset = Offset.zero, + SliderThemeData sliderTheme, + bool isEnabled, + bool isDiscrete, + }); + + /// Paints the track shape based on the state passed to it. + /// + /// {@macro flutter.material.slider.shape.context} + /// + /// [offset] is the offset of the origin of the [parentBox] to the origin of + /// its [context] canvas. This shape must be painted relative to this + /// offset. See [PaintingContextCallback]. + /// + /// {@macro flutter.material.slider.shape.parentBox} + /// + /// {@macro flutter.material.slider.shape.sliderTheme} + /// + /// {@macro flutter.material.slider.shape.enableAnimation} + /// + /// [thumbCenter] is the offset of the center of the thumb relative to the + /// origin of the [PaintingContext.canvas]. It can be used as the point that + /// divides the track into 2 segments. + /// + /// {@macro flutter.material.slider.shape.isEnabled} + /// + /// {@macro flutter.material.slider.shape.isDiscrete} + /// + /// [textDirection] can be used to determine how the track segments are + /// painted depending on whether they are active or not. The track segment + /// between the start of the slider and the thumb is the active track segment. + /// The track segment between the thumb and the end of the slider is the + /// inactive track segment. In LTR text direction, the start of the slider is + /// on the left, and in RTL text direction, the start of the slider is on the + /// right. + void paint( + PaintingContext context, + Offset offset, { + RenderBox parentBox, + SliderThemeData sliderTheme, + Animation enableAnimation, + Offset thumbCenter, + bool isEnabled, + bool isDiscrete, + TextDirection textDirection, + }); +} + +/// Base class for slider tick mark shapes. +/// +/// Create a subclass of this if you would like a custom slider tick mark shape. +/// This is a simplified version of [SliderComponentShape] with a +/// [SliderThemeData] passed when getting the preferred size. +/// +/// See also: +/// +/// * [RoundSliderTickMarkShape] for a simple example of a tick mark shape. +/// * [SliderTrackShape] for the base class for custom a track shape. +/// * [SliderComponentShape] for the base class for custom a component shape. +abstract class SliderTickMarkShape { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const SliderTickMarkShape(); + + /// Returns the preferred size of the shape. + /// + /// It is used to help position the tick marks within the slider. + /// + /// {@macro flutter.material.slider.shape.sliderTheme} + /// + /// {@macro flutter.material.slider.shape.isEnabled} + Size getPreferredSize({ + SliderThemeData sliderTheme, + bool isEnabled, + }); + + /// Paints the slider track. + /// + /// {@macro flutter.material.slider.shape.context} + /// + /// {@macro flutter.material.slider.shape.center} + /// + /// {@macro flutter.material.slider.shape.parentBox} + /// + /// {@macro flutter.material.slider.shape.sliderTheme} + /// + /// {@macro flutter.material.slider.shape.enableAnimation} + /// + /// {@macro flutter.material.slider.shape.isEnabled} + /// + /// [textDirection] can be used to determine how the tick marks are painting + /// depending on whether they are on an active track segment or not. The track + /// segment between the start of the slider and the thumb is the active track + /// segment. The track segment between the thumb and the end of the slider is + /// the inactive track segment. In LTR text direction, the start of the slider + /// is on the left, and in RTL text direction, the start of the slider is on + /// the right. + void paint( + PaintingContext context, + Offset center, { + RenderBox parentBox, + SliderThemeData sliderTheme, + Animation enableAnimation, + Offset thumbCenter, + bool isEnabled, + TextDirection textDirection, + }); +} + +/// Base class for slider thumb, thumb overlay, and value indicator shapes. +/// +/// Create a subclass of this if you would like a custom shape. +/// +/// All shapes are painted to the same canvas and ordering is important. +/// The overlay is painted first, then the value indicator, then the thumb. +/// +/// See also: +/// +/// * [RoundSliderThumbShape], which is the the default thumb shape. +/// * [RoundSliderOverlayShape], which is the the default overlay shape. +/// * [PaddleSliderValueIndicatorShape], which is the the default value /// indicator shape. abstract class SliderComponentShape { /// Abstract const constructor. This constructor enables subclasses to provide @@ -516,21 +784,34 @@ abstract class SliderComponentShape { /// Paints the shape, taking into account the state passed to it. /// + /// {@macro flutter.material.slider.shape.context} + /// + /// {@macro flutter.material.slider.shape.center} + /// /// [activationAnimation] is an animation triggered when the user beings /// to interact with the slider. It reverses when the user stops interacting /// with the slider. /// - /// [enableAnimation] is an animation triggered when the [Slider] is enabled, - /// and it reverses when the slider is disabled. + /// {@macro flutter.material.slider.shape.enableAnimation} /// - /// [value] is the current parametric value (from 0.0 to 1.0) of the slider. + /// {@macro flutter.material.slider.shape.isDiscrete} /// /// If [labelPainter] is non-null, then [labelPainter.paint] should be /// called with the location that the label should appear. If the labelPainter /// passed is null, then no label was supplied to the [Slider]. + /// + /// {@macro flutter.material.slider.shape.parentBox} + /// + /// {@macro flutter.material.slider.shape.sliderTheme} + /// + /// [textDirection] can be used to determine how any extra text or graphics, + /// besides the text painted by the [labelPainter] should be positioned. The + /// [labelPainter] already has the [textDirection] set. + /// + /// [value] is the current parametric value (from 0.0 to 1.0) of the slider. void paint( PaintingContext context, - Offset thumbCenter, { + Offset center, { Animation activationAnimation, Animation enableAnimation, bool isDiscrete, @@ -542,14 +823,186 @@ abstract class SliderComponentShape { }); } -/// This is the default shape to a [Slider]'s thumb if no -/// other shape is specified. +// The following shapes are the material defaults. + +/// This is the default shape of a [Slider]'s track. +/// +/// It paints a solid colored rectangle, vertically centered in the +/// [parentBox]. The track rectangle extends to the bounds of the [parentBox], +/// but is padded by the [RoundSliderOverlayShape] radius. The height is defined +/// by the [SliderThemeData.trackHeight]. The color is determined by the +/// [Slider]'s enabled state and the track piece's active state which are +/// defined by: +/// [SliderThemeData.activeTrackColor], +/// [SliderThemeData.inactiveTrackColor], +/// [SliderThemeData.disabledActiveTrackColor], +/// [SliderThemeData.disabledInactiveTrackColor]. /// /// See also: /// /// * [Slider] for the component that this is meant to display this shape. /// * [SliderThemeData] where an instance of this class is set to inform the -/// slider of the shape of the its thumb. +/// slider of the visual details of the its track. +/// * [SliderTrackShape] Base component for creating other custom track +/// shapes. +class RectangularSliderTrackShape extends SliderTrackShape { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const RectangularSliderTrackShape(); + + @override + Rect getPreferredRect({ + RenderBox parentBox, + Offset offset = Offset.zero, + SliderThemeData sliderTheme, + bool isEnabled, + bool isDiscrete, + }) { + final double overlayWidth = sliderTheme.overlayShape.getPreferredSize(isEnabled, isDiscrete).width; + final double trackHeight = sliderTheme.trackHeight; + assert(overlayWidth >= 0); + assert(trackHeight >= 0); + assert(parentBox.size.width >= overlayWidth); + assert(parentBox.size.height >= trackHeight); + + final double trackLeft = offset.dx + overlayWidth / 2; + final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2; + // TODO(clocksmith): Although this works for a material, perhaps the default + // rectangular track should be padded not just by the overlay, but by the + // max of the thumb and the overlay, in case there is no overlay. + final double trackWidth = parentBox.size.width - overlayWidth; + return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); + } + + // Spacing for disabled slider state. + static const double _thumbGap = 2.0; + + @override + void paint( + PaintingContext context, + Offset offset, { + RenderBox parentBox, + SliderThemeData sliderTheme, + Animation enableAnimation, + TextDirection textDirection, + Offset thumbCenter, + bool isDiscrete, + bool isEnabled, + }) { + // 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); + final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor); + final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation); + final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation); + Paint leftTrackPaint; + Paint rightTrackPaint; + switch (textDirection) { + case TextDirection.ltr: + leftTrackPaint = activePaint; + rightTrackPaint = inactivePaint; + break; + case TextDirection.rtl: + leftTrackPaint = inactivePaint; + rightTrackPaint = activePaint; + break; + } + + // Used to create a gap around the thumb iff the slider is disabled. + double horizontalAdjustment = 0.0; + if (!isEnabled) { + final double thumbRadius = sliderTheme.thumbShape.getPreferredSize(isEnabled, isDiscrete).width / 2.0; + final double gap = _thumbGap * (1.0 - enableAnimation.value); + horizontalAdjustment = thumbRadius + gap; + } + + final Rect trackRect = getPreferredRect( + parentBox: parentBox, + offset: offset, + sliderTheme: sliderTheme, + isEnabled: isEnabled, + isDiscrete: isDiscrete + ); + final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left, trackRect.top, thumbCenter.dx - horizontalAdjustment, trackRect.bottom); + context.canvas.drawRect(leftTrackSegment, leftTrackPaint); + final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx + horizontalAdjustment, trackRect.top, trackRect.right, trackRect.bottom); + context.canvas.drawRect(rightTrackSegment, rightTrackPaint); + } +} + +/// This is the default shape of each [Slider] tick mark. +/// +/// Tick marks are only displayed if the slider is discrete, which can be done +/// by setting the [Slider.divisions] as non-null. +/// +/// It paints a solid circle, centered in the on the track. +/// The color is determined by the [Slider]'s enabled state and track's active +/// states. These colors are defined in: +/// [SliderThemeData.activeTrackColor], +/// [SliderThemeData.inactiveTrackColor], +/// [SliderThemeData.disabledActiveTrackColor], +/// [SliderThemeData.disabledInactiveTrackColor]. +/// +/// See also: +/// +/// * [Slider], which includes tick marks defined by this shape. +/// * [SliderTheme], which can be used to configure the tick mark shape of all +/// sliders in a widget subtree. +class RoundSliderTickMarkShape extends SliderTickMarkShape { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const RoundSliderTickMarkShape(); + + @override + Size getPreferredSize({ + bool isEnabled, + SliderThemeData sliderTheme, + }) { + return Size.fromRadius(sliderTheme.trackHeight / 2); + } + + @override + void paint( + PaintingContext context, + Offset center, { + RenderBox parentBox, + SliderThemeData sliderTheme, + Animation enableAnimation, + TextDirection textDirection, + Offset thumbCenter, + bool isEnabled, + }) { + // The paint color of the tick mark depends on its position relative + // to the thumb and the text direction. + Color begin; + Color end; + switch (textDirection) { + case TextDirection.ltr: + final bool isTickMarkRightOfThumb = center.dx > thumbCenter.dx; + begin = isTickMarkRightOfThumb ? sliderTheme.disabledInactiveTickMarkColor : sliderTheme.disabledActiveTickMarkColor; + end = isTickMarkRightOfThumb ? sliderTheme.inactiveTickMarkColor : sliderTheme.activeTickMarkColor; + break; + case TextDirection.rtl: + final bool isTickMarkLeftOfThumb = center.dx < thumbCenter.dx; + begin = isTickMarkLeftOfThumb ? sliderTheme.disabledInactiveTickMarkColor : sliderTheme.disabledActiveTickMarkColor; + end = isTickMarkLeftOfThumb ? sliderTheme.inactiveTickMarkColor : sliderTheme.activeTickMarkColor; + break; + } + final Paint paint = Paint()..color = ColorTween(begin: begin, end: end).evaluate(enableAnimation); + + // The tick marks are tiny circles that are the same height as the track. + final double tickMarkRadius = sliderTheme.trackHeight / 2; + context.canvas.drawCircle(center, tickMarkRadius, paint); + } +} + +/// This is the default shape of a [Slider]'s thumb. +/// +/// See also: +/// +/// * [Slider], which includes a thumb defined by this shape. +/// * [SliderTheme], which can be used to configure the thumb shape of all +/// sliders in a widget subtree. class RoundSliderThumbShape extends SliderComponentShape { /// Create a slider thumb that draws a circle. const RoundSliderThumbShape(); @@ -565,7 +1018,7 @@ class RoundSliderThumbShape extends SliderComponentShape { @override void paint( PaintingContext context, - Offset thumbCenter, { + Offset center, { Animation activationAnimation, Animation enableAnimation, bool isDiscrete, @@ -585,21 +1038,78 @@ class RoundSliderThumbShape extends SliderComponentShape { end: sliderTheme.thumbColor, ); canvas.drawCircle( - thumbCenter, + center, radiusTween.evaluate(enableAnimation), Paint()..color = colorTween.evaluate(enableAnimation), ); } } -/// This is the default shape to a [Slider]'s value indicator if no -/// other shape is specified. +/// This is the default shape of a [Slider]'s thumb overlay. +/// +/// The shape of the overlay is a circle with the same center as the thumb, but +/// with a larger radius. It animates to full size when the thumb is pressed, +/// and animates back down to size 0 when it is released. It is painted behind +/// the thumb, and is expected to extend beyond the bounds of the thumb so that +/// it is visible. +/// +/// The overlay color is defined by [SliderThemeData.overlayColor]. /// /// See also: /// -/// * [Slider] for the component that this is meant to display this shape. -/// * [SliderThemeData] where an instance of this class is set to inform the -/// slider of the shape of the its value indicator. +/// * [Slider], which includes an overlay defined by this shape. +/// * [SliderTheme], which can be used to configure the overlay shape of all +/// sliders in a widget subtree. +class RoundSliderOverlayShape extends SliderComponentShape { + /// Create a slider thumb overlay that draws a circle. + const RoundSliderOverlayShape(); + + static const double _overlayRadius = 16.0; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return const Size.fromRadius(_overlayRadius); + } + + @override + void paint( + PaintingContext context, + Offset center, { + Animation activationAnimation, + Animation enableAnimation, + bool isDiscrete, + TextPainter labelPainter, + RenderBox parentBox, + SliderThemeData sliderTheme, + TextDirection textDirection, + double value, + }) { + final Canvas canvas = context.canvas; + final Tween radiusTween = Tween( + begin: 0.0, + end: _overlayRadius, + ); + + // TODO(gspencer): We don't really follow the spec here for overlays. + // The spec says to use 16% opacity for drawing over light material, + // and 32% for colored material, but we don't really have a way to + // know what the underlying color is, so there's no easy way to + // implement this. Choosing the "light" version for now. + canvas.drawCircle( + center, + radiusTween.evaluate(activationAnimation), + Paint()..color = sliderTheme.overlayColor, + ); + } +} + +/// This is the default shape of a [Slider]'s value indicator. +/// +/// See also: +/// +/// * [Slider], which includes a value indicator defined by this shape. +/// * [SliderTheme], which can be used to configure the slider value indicator +/// of all sliders in a widget subtree. class PaddleSliderValueIndicatorShape extends SliderComponentShape { /// Create a slider value indicator in the shape of an upside-down pear. const PaddleSliderValueIndicatorShape(); @@ -881,7 +1391,7 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { @override void paint( PaintingContext context, - Offset thumbCenter, { + Offset center, { Animation activationAnimation, Animation enableAnimation, bool isDiscrete, @@ -898,10 +1408,10 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape { _drawValueIndicator( parentBox, context.canvas, - thumbCenter, + center, Paint()..color = enableColor.evaluate(enableAnimation), activationAnimation.value, labelPainter, ); } -} +} \ No newline at end of file diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart index bf4cec3a36..49cd304e4b 100644 --- a/packages/flutter/test/material/slider_test.dart +++ b/packages/flutter/test/material/slider_test.dart @@ -26,17 +26,19 @@ class LoggingThumbShape extends SliderComponentShape { @override void paint( - PaintingContext context, - Offset thumbCenter, { - Animation activationAnimation, - Animation enableAnimation, - bool isDiscrete, - TextPainter labelPainter, - RenderBox parentBox, - SliderThemeData sliderTheme, - TextDirection textDirection, - double value, - }) { + PaintingContext context, + Offset thumbCenter, { + Animation activationAnimation, + Animation enableAnimation, + bool isEnabled, + bool isDiscrete, + bool onActiveTrack, + TextPainter labelPainter, + RenderBox parentBox, + SliderThemeData sliderTheme, + TextDirection textDirection, + double value, + }) { log.add(thumbCenter); final Paint thumbPaint = Paint()..color = Colors.red; context.canvas.drawCircle(thumbCenter, 5.0, thumbPaint); @@ -720,14 +722,15 @@ void main() { expect( sliderBox, paints - ..rect(color: customColor1) - ..rect(color: customColor2) - ..circle(color: customColor1.withAlpha(0x29)) - ..circle(color: customColor2) - ..circle(color: customColor2) - ..circle(color: customColor1) - ..path(color: customColor1) - ..circle(color: customColor1), + ..rect(color: customColor1) // active track + ..rect(color: customColor2) // inactive track + ..circle(color: customColor1.withAlpha(0x29)) // 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 + ..path(color: customColor1) // indicator + ..circle(color: customColor1), // thumb ); await gesture.up(); }); @@ -896,8 +899,8 @@ void main() { child: Material( child: Theme( data: Theme.of(context).copyWith( - sliderTheme: Theme.of(context).sliderTheme.copyWith(showValueIndicator: show), - ), + sliderTheme: Theme.of(context).sliderTheme.copyWith(showValueIndicator: show), + ), child: Center( child: OverflowBox( maxWidth: double.infinity, @@ -1025,6 +1028,7 @@ void main() { expect( sliderBox, paints + ..circle(x: 17.0, y: 16.0, radius: 1.0) ..circle(x: 208.5, y: 16.0, radius: 1.0) ..circle(x: 400.0, y: 16.0, radius: 1.0) ..circle(x: 591.5, y: 16.0, radius: 1.0) @@ -1077,6 +1081,7 @@ void main() { ..circle(x: 400.0, y: 16.0, radius: 16.0) ..circle(x: 17.0, y: 16.0, radius: 1.0) ..circle(x: 208.5, y: 16.0, radius: 1.0) + ..circle(x: 400.0, y: 16.0, radius: 1.0) ..circle(x: 591.5, y: 16.0, radius: 1.0) ..circle(x: 783.0, y: 16.0, radius: 1.0) ..circle(x: 400.0, y: 16.0, radius: 6.0), @@ -1089,6 +1094,7 @@ void main() { paints ..circle(x: 17.0, y: 16.0, radius: 1.0) ..circle(x: 208.5, y: 16.0, radius: 1.0) + ..circle(x: 400.0, y: 16.0, radius: 1.0) ..circle(x: 591.5, y: 16.0, radius: 1.0) ..circle(x: 783.0, y: 16.0, radius: 1.0) ..circle(x: 400.0, y: 16.0, radius: 6.0), @@ -1367,4 +1373,4 @@ void main() { expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)), equals(1)); await gesture.up(); }); -} +} \ No newline at end of file diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart index c054f08d73..d8639dfc2d 100644 --- a/packages/flutter/test/material/slider_theme_test.dart +++ b/packages/flutter/test/material/slider_theme_test.dart @@ -100,6 +100,20 @@ void main() { expect(sliderBox, paints..rect(color: customTheme.disabledActiveTrackColor)..rect(color: customTheme.disabledInactiveTrackColor)); }); + testWidgets('SliderThemeData assigns the correct default shapes', (WidgetTester tester) async { + final SliderThemeData sliderTheme = ThemeData().sliderTheme; + expect(sliderTheme.trackShape, equals(isInstanceOf())); + expect(sliderTheme.tickMarkShape, equals(isInstanceOf())); + expect(sliderTheme.thumbShape, equals(isInstanceOf())); + expect(sliderTheme.valueIndicatorShape, equals(isInstanceOf())); + expect(sliderTheme.overlayShape, equals(isInstanceOf())); + }); + + testWidgets('SliderThemeData assigns the correct default flags', (WidgetTester tester) async { + final SliderThemeData sliderTheme = ThemeData().sliderTheme; + expect(sliderTheme.showValueIndicator, equals(ShowValueIndicator.onlyForDiscrete)); + }); + testWidgets('SliderThemeData generates correct opacities for fromPrimaryColors', (WidgetTester tester) async { const Color customColor1 = Color(0xcafefeed); const Color customColor2 = Color(0xdeadbeef); @@ -125,9 +139,6 @@ void main() { expect(sliderTheme.disabledThumbColor, equals(customColor2.withAlpha(0x52))); expect(sliderTheme.overlayColor, equals(customColor1.withAlpha(0x29))); expect(sliderTheme.valueIndicatorColor, equals(customColor1.withAlpha(0xff))); - expect(sliderTheme.thumbShape, equals(isInstanceOf())); - expect(sliderTheme.valueIndicatorShape, equals(isInstanceOf())); - expect(sliderTheme.showValueIndicator, equals(ShowValueIndicator.onlyForDiscrete)); expect(sliderTheme.valueIndicatorTextStyle.color, equals(customColor4)); }); @@ -137,15 +148,17 @@ void main() { primaryColorDark: Colors.black, primaryColorLight: Colors.black, valueIndicatorTextStyle: ThemeData.fallback().accentTextTheme.body2.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().accentTextTheme.body2.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))); @@ -161,7 +174,142 @@ void main() { expect(lerp.valueIndicatorTextStyle.color, equals(middleGrey.withAlpha(0xff))); }); - testWidgets('Default slider thumb shape draws correctly', (WidgetTester tester) async { + 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); + double value = 0.25; + Widget buildApp({ bool enabled = true }) { + final ValueChanged onChanged = enabled ? (double d) => value = d : null; + return Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: MediaQueryData.fromWindow(window), + child: Material( + child: Center( + child: SliderTheme( + data: sliderTheme, + child: Slider( + value: value, + label: '$value', + onChanged: onChanged, + ), + ), + ), + ), + ), + ); + } + + await tester.pumpWidget(buildApp()); + + final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + + expect( + sliderBox, + paints + ..rect(rect: Rect.fromLTRB(16.0, 299.0, 208.0, 301.0), color: sliderTheme.activeTrackColor) + ..rect(rect: Rect.fromLTRB(208.0, 299.0, 784.0, 301.0), color: sliderTheme.inactiveTrackColor) + ); + + await tester.pumpWidget(buildApp(enabled: false)); + await tester.pumpAndSettle(); // wait for disable animation + // The disabled thumb is smaller so the track has to paint longer to get + // to the edge. + expect( + sliderBox, + paints + ..rect(rect: Rect.fromLTRB(16.0, 299.0, 202.0, 301.0), color: sliderTheme.disabledActiveTrackColor) + ..rect(rect: Rect.fromLTRB(214.0, 299.0, 784.0, 301.0), 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); + double value = 0.25; + Widget buildApp({ bool enabled = true }) { + final ValueChanged onChanged = enabled ? (double d) => value = d : null; + return Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: MediaQueryData.fromWindow(window), + child: Material( + child: Center( + child: SliderTheme( + data: sliderTheme, + child: Slider( + value: value, + label: '$value', + onChanged: onChanged, + ), + ), + ), + ), + ), + ); + } + + await tester.pumpWidget(buildApp()); + + final RenderBox sliderBox = tester.firstRenderObject(find.byType(Slider)); + + // With no touch, paints only the thumb. + expect( + sliderBox, + paints + ..circle( + color: sliderTheme.thumbColor, + x: 208.0, + y: 300.0, + radius: 6.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( + sliderBox, + paints + ..circle( + color: sliderTheme.overlayColor, + x: 208.0, + y: 300.0, + radius: 16.0, + ) + ..circle( + color: sliderTheme.thumbColor, + x: 208.0, + y: 300.0, + radius: 6.0, + ) + ); + + await gesture.up(); + await tester.pumpAndSettle(); + // After the gesture is up and complete, it again paints only the thumb. + expect( + sliderBox, + paints + ..circle( + color: sliderTheme.thumbColor, + x: 208.0, + y: 300.0, + radius: 6.0, + ) + ); + }); + + testWidgets('Default slider ticker and thumb shape draw correctly', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, primarySwatch: Colors.blue, @@ -207,23 +355,26 @@ void main() { await tester.pumpWidget(buildApp(divisions: 3)); await tester.pumpAndSettle(); // wait for disable animation expect( - sliderBox, - paints - ..circle(color: sliderTheme.activeTickMarkColor) - ..circle(color: sliderTheme.activeTickMarkColor) - ..circle(color: sliderTheme.inactiveTickMarkColor) - ..circle(color: sliderTheme.inactiveTickMarkColor) - ..circle(color: sliderTheme.thumbColor, radius: 6.0)); + sliderBox, + paints + ..circle(color: sliderTheme.activeTickMarkColor) + ..circle(color: sliderTheme.activeTickMarkColor) + ..circle(color: sliderTheme.inactiveTickMarkColor) + ..circle(color: sliderTheme.inactiveTickMarkColor) + ..circle(color: sliderTheme.thumbColor, radius: 6.0) + ); await tester.pumpWidget(buildApp(divisions: 3, enabled: false)); await tester.pumpAndSettle(); // wait for disable animation expect( - sliderBox, - paints - ..circle(color: sliderTheme.disabledActiveTickMarkColor) - ..circle(color: sliderTheme.disabledInactiveTickMarkColor) - ..circle(color: sliderTheme.disabledInactiveTickMarkColor) - ..circle(color: sliderTheme.disabledThumbColor, radius: 4.0)); + sliderBox, + paints + ..circle(color: sliderTheme.disabledActiveTickMarkColor) + ..circle(color: sliderTheme.disabledInactiveTickMarkColor) + ..circle(color: sliderTheme.disabledInactiveTickMarkColor) + ..circle(color: sliderTheme.disabledInactiveTickMarkColor) + ..circle(color: sliderTheme.disabledThumbColor, radius: 4.0) + ); }); testWidgets('Default slider value indicator shape draws correctly', (WidgetTester tester) async { @@ -267,17 +418,18 @@ void main() { // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( - sliderBox, - paints - ..path( - color: sliderTheme.valueIndicatorColor, - includes: [ - const Offset(0.0, -40.0), - const Offset(15.9, -40.0), - const Offset(-15.9, -40.0), - ], - excludes: [const Offset(16.1, -40.0), const Offset(-16.1, -40.0)], - )); + sliderBox, + paints + ..path( + color: sliderTheme.valueIndicatorColor, + includes: [ + const Offset(0.0, -40.0), + const Offset(15.9, -40.0), + const Offset(-15.9, -40.0), + ], + excludes: [const Offset(16.1, -40.0), const Offset(-16.1, -40.0)], + ) + ); await gesture.up(); @@ -288,17 +440,18 @@ void main() { // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( - sliderBox, - paints - ..path( - color: sliderTheme.valueIndicatorColor, - includes: [ - const Offset(0.0, -40.0), - const Offset(35.9, -40.0), - const Offset(-35.9, -40.0), - ], - excludes: [const Offset(36.1, -40.0), const Offset(-36.1, -40.0)], - )); + sliderBox, + paints + ..path( + color: sliderTheme.valueIndicatorColor, + includes: [ + const Offset(0.0, -40.0), + const Offset(35.9, -40.0), + const Offset(-35.9, -40.0), + ], + excludes: [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. @@ -308,17 +461,18 @@ void main() { // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( - sliderBox, - paints - ..path( - color: sliderTheme.valueIndicatorColor, - includes: [ - const Offset(0.0, -40.0), - const Offset(98.0, -40.0), - const Offset(-16.0, -40.0), - ], - excludes: [const Offset(98.1, -40.0), const Offset(-16.1, -40.0)], - )); + sliderBox, + paints + ..path( + color: sliderTheme.valueIndicatorColor, + includes: [ + const Offset(0.0, -40.0), + const Offset(98.0, -40.0), + const Offset(-16.0, -40.0), + ], + excludes: [const Offset(98.1, -40.0), const Offset(-16.1, -40.0)], + ) + ); await gesture.up(); // Test that it avoids the right edge of the screen. @@ -328,17 +482,18 @@ void main() { // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( - sliderBox, - paints - ..path( - color: sliderTheme.valueIndicatorColor, - includes: [ - const Offset(0.0, -40.0), - const Offset(16.0, -40.0), - const Offset(-98.0, -40.0), - ], - excludes: [const Offset(16.1, -40.0), const Offset(-98.1, -40.0)], - )); + sliderBox, + paints + ..path( + color: sliderTheme.valueIndicatorColor, + includes: [ + const Offset(0.0, -40.0), + const Offset(16.0, -40.0), + const Offset(-98.0, -40.0), + ], + excludes: [const Offset(16.1, -40.0), const Offset(-98.1, -40.0)], + ) + ); await gesture.up(); // Test that the neck stretches when the text scale gets smaller. @@ -348,22 +503,23 @@ void main() { // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( - sliderBox, - paints - ..path( - color: sliderTheme.valueIndicatorColor, - includes: [ - const Offset(0.0, -49.0), - const Offset(90.0, -49.0), - const Offset(-24.0, -49.0), - ], - excludes: [ - const Offset(98.0, -32.0), // inside full size, outside small - const Offset(-16.0, -32.0), // inside full size, outside small - const Offset(90.1, -49.0), - const Offset(-24.1, -49.0), - ], - )); + sliderBox, + paints + ..path( + color: sliderTheme.valueIndicatorColor, + includes: [ + const Offset(0.0, -49.0), + const Offset(90.0, -49.0), + const Offset(-24.0, -49.0), + ], + excludes: [ + const Offset(98.0, -32.0), // inside full size, outside small + const Offset(-16.0, -32.0), // inside full size, outside small + const Offset(90.1, -49.0), + const Offset(-24.1, -49.0), + ], + ) + ); await gesture.up(); // Test that the neck shrinks when the text scale gets larger. @@ -373,22 +529,23 @@ void main() { // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect( - sliderBox, - paints - ..path( - color: sliderTheme.valueIndicatorColor, - includes: [ - const Offset(0.0, -38.8), - const Offset(98.0, -38.8), - const Offset(-16.0, -38.8), - const Offset(10.0, -23.0), // Inside large, outside scale=1.0 - const Offset(-4.0, -23.0), // Inside large, outside scale=1.0 - ], - excludes: [ - const Offset(98.5, -38.8), - const Offset(-16.1, -38.8), - ], - )); + sliderBox, + paints + ..path( + color: sliderTheme.valueIndicatorColor, + includes: [ + const Offset(0.0, -38.8), + const Offset(98.0, -38.8), + const Offset(-16.0, -38.8), + const Offset(10.0, -23.0), // Inside large, outside scale=1.0 + const Offset(-4.0, -23.0), // Inside large, outside scale=1.0 + ], + excludes: [ + const Offset(98.5, -38.8), + const Offset(-16.1, -38.8), + ], + ) + ); await gesture.up(); }); -} +} \ No newline at end of file