Unverified Commit cfe992d0 authored by Anthony's avatar Anthony Committed by GitHub

[Material] Custom track, tick mark, and overlay shape painters for Slider (#25008)

Create a slider shape for custom track, tick mark, and overlay painting, for the material slider.
parent d5d47f63
......@@ -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<double> _overlayRadiusTween = Tween<double>(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<Size> get _sliderPartSizes => <Size>[
_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<double> _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 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 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 Offset thumbCenter = Offset(trackActive, trackVerticalCenter);
_sliderTheme.trackShape.paint(
context,
offset,
parentBox: this,
sliderTheme: _sliderTheme,
enableAnimation: _enableAnimation,
textDirection: _textDirection,
thumbCenter: thumbCenter,
isDiscrete: isDiscrete,
isEnabled: isInteractive
);
// Paint the track.
if (visualPosition > 0.0) {
canvas.drawRect(trackLeftRect, leftTrackPaint);
}
if (visualPosition < 1.0) {
canvas.drawRect(trackRightRect, rightTrackPaint);
// 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,
);
}
_paintOverlay(canvas, thumbCenter);
_paintTickMarks(
canvas,
trackLeftRect,
trackRightRect,
leftTickMarkPaint,
rightTickMarkPaint,
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.status != AnimationStatus.dismissed) {
if (isInteractive && label != null && !_valueIndicatorAnimation.isDismissed) {
if (showValueIndicator) {
_sliderTheme.valueIndicatorShape.paint(
context,
......
......@@ -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,11 +213,15 @@ 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(trackHeight != null),
assert(activeTrackColor != null),
assert(inactiveTrackColor != null),
assert(disabledActiveTrackColor != null),
assert(disabledInactiveTrackColor != null),
......@@ -209,7 +233,10 @@ class SliderThemeData extends Diagnosticable {
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);
......@@ -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<Color>('activeTrackColor', activeTrackColor, defaultValue: defaultData.activeTrackColor));
properties.add(DiagnosticsProperty<Color>('activeTrackColor', activeTrackColor, defaultValue: defaultData.activeTrackColor));
properties.add(DiagnosticsProperty<Color>('inactiveTrackColor', inactiveTrackColor, defaultValue: defaultData.inactiveTrackColor));
properties.add(DiagnosticsProperty<Color>('disabledActiveTrackColor', disabledActiveTrackColor, defaultValue: defaultData.disabledActiveTrackColor, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<Color>('disabledInactiveTrackColor', disabledInactiveTrackColor, defaultValue: defaultData.disabledInactiveTrackColor, level: DiagnosticLevel.debug));
......@@ -489,22 +566,213 @@ class SliderThemeData extends Diagnosticable {
properties.add(DiagnosticsProperty<Color>('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<Color>('overlayColor', overlayColor, defaultValue: defaultData.overlayColor, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<Color>('valueIndicatorColor', valueIndicatorColor, defaultValue: defaultData.valueIndicatorColor));
properties.add(DiagnosticsProperty<SliderTrackShape>('trackShape', trackShape, defaultValue: defaultData.trackShape, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SliderTickMarkShape>('tickMarkShape', tickMarkShape, defaultValue: defaultData.tickMarkShape, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SliderComponentShape>('thumbShape', thumbShape, defaultValue: defaultData.thumbShape, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SliderComponentShape>('overlayShape', overlayShape, defaultValue: defaultData.overlayShape, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SliderComponentShape>('valueIndicatorShape', valueIndicatorShape, defaultValue: defaultData.valueIndicatorShape, level: DiagnosticLevel.debug));
properties.add(EnumProperty<ShowValueIndicator>('showValueIndicator', showValueIndicator, defaultValue: defaultData.showValueIndicator));
properties.add(DiagnosticsProperty<TextStyle>('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<double> 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<double> 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<double> activationAnimation,
Animation<double> 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<double> 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<double> 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<double> activationAnimation,
Animation<double> 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<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
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<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
......@@ -898,7 +1408,7 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
_drawValueIndicator(
parentBox,
context.canvas,
thumbCenter,
center,
Paint()..color = enableColor.evaluate(enableAnimation),
activationAnimation.value,
labelPainter,
......
......@@ -30,7 +30,9 @@ class LoggingThumbShape extends SliderComponentShape {
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isEnabled,
bool isDiscrete,
bool onActiveTrack,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
......@@ -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();
});
......@@ -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),
......
......@@ -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<RectangularSliderTrackShape>()));
expect(sliderTheme.tickMarkShape, equals(isInstanceOf<RoundSliderTickMarkShape>()));
expect(sliderTheme.thumbShape, equals(isInstanceOf<RoundSliderThumbShape>()));
expect(sliderTheme.valueIndicatorShape, equals(isInstanceOf<PaddleSliderValueIndicatorShape>()));
expect(sliderTheme.overlayShape, equals(isInstanceOf<RoundSliderOverlayShape>()));
});
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<RoundSliderThumbShape>()));
expect(sliderTheme.valueIndicatorShape, equals(isInstanceOf<PaddleSliderValueIndicatorShape>()));
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<double> 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<RenderBox>(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<double> 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<RenderBox>(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,
......@@ -213,7 +361,8 @@ void main() {
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.thumbColor, radius: 6.0));
..circle(color: sliderTheme.thumbColor, radius: 6.0)
);
await tester.pumpWidget(buildApp(divisions: 3, enabled: false));
await tester.pumpAndSettle(); // wait for disable animation
......@@ -223,7 +372,9 @@ void main() {
..circle(color: sliderTheme.disabledActiveTickMarkColor)
..circle(color: sliderTheme.disabledInactiveTickMarkColor)
..circle(color: sliderTheme.disabledInactiveTickMarkColor)
..circle(color: sliderTheme.disabledThumbColor, radius: 4.0));
..circle(color: sliderTheme.disabledInactiveTickMarkColor)
..circle(color: sliderTheme.disabledThumbColor, radius: 4.0)
);
});
testWidgets('Default slider value indicator shape draws correctly', (WidgetTester tester) async {
......@@ -277,7 +428,8 @@ void main() {
const Offset(-15.9, -40.0),
],
excludes: <Offset>[const Offset(16.1, -40.0), const Offset(-16.1, -40.0)],
));
)
);
await gesture.up();
......@@ -298,7 +450,8 @@ void main() {
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.
......@@ -318,7 +471,8 @@ void main() {
const Offset(-16.0, -40.0),
],
excludes: <Offset>[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.
......@@ -338,7 +492,8 @@ void main() {
const Offset(-98.0, -40.0),
],
excludes: <Offset>[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.
......@@ -363,7 +518,8 @@ void main() {
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.
......@@ -388,7 +544,8 @@ void main() {
const Offset(98.5, -38.8),
const Offset(-16.1, -38.8),
],
));
)
);
await gesture.up();
});
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment