Unverified Commit 10fe2056 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add a slider demo, and a text theme for SliderThemeData (#15620)

This adds a slider demo with a custom theme to the gallery.

In the process of adding this, I decided to add a text theme to the SliderThemeData, since it's a pain to change the text style on the value indicator otherwise.
parent 7a285301
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart';
class SliderDemo extends StatefulWidget {
......@@ -11,12 +13,124 @@ class SliderDemo extends StatefulWidget {
_SliderDemoState createState() => new _SliderDemoState();
}
Path _triangle(double size, Offset thumbCenter, {bool invert: false}) {
final Path thumbPath = new Path();
final double height = math.sqrt(3.0) / 2.0;
final double halfSide = size / 2.0;
final double centerHeight = size * height / 3.0;
final double sign = invert ? -1.0 : 1.0;
thumbPath.moveTo(thumbCenter.dx - halfSide, thumbCenter.dy + sign * centerHeight);
thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight);
thumbPath.lineTo(thumbCenter.dx + halfSide, thumbCenter.dy + sign * centerHeight);
thumbPath.close();
return thumbPath;
}
class _CustomThumbShape extends SliderComponentShape {
static const double _thumbSize = 4.0;
static const double _disabledThumbSize = 3.0;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return isEnabled ? const Size.fromRadius(_thumbSize) : const Size.fromRadius(_disabledThumbSize);
}
static final Tween<double> sizeTween = new Tween<double>(
begin: _disabledThumbSize,
end: _thumbSize,
);
@override
void paint(
PaintingContext context,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final Canvas canvas = context.canvas;
final ColorTween colorTween = new ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
final Path thumbPath = _triangle(size, thumbCenter);
canvas.drawPath(thumbPath, new Paint()..color = colorTween.evaluate(enableAnimation));
}
}
class _CustomValueIndicatorShape extends SliderComponentShape {
static const double _indicatorSize = 4.0;
static const double _disabledIndicatorSize = 3.0;
static const double _slideUpHeight = 40.0;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return new Size.fromRadius(isEnabled ? _indicatorSize : _disabledIndicatorSize);
}
static final Tween<double> sizeTween = new Tween<double>(
begin: _disabledIndicatorSize,
end: _indicatorSize,
);
@override
void paint(
PaintingContext context,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final Canvas canvas = context.canvas;
final ColorTween enableColor = new ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
);
final Tween<double> slideUpTween = new Tween<double>(
begin: 0.0,
end: _slideUpHeight,
);
final double size = _indicatorSize * sizeTween.evaluate(enableAnimation);
final Offset slideUpOffset = new Offset(0.0, -slideUpTween.evaluate(activationAnimation));
final Path thumbPath = _triangle(
size,
thumbCenter + slideUpOffset,
invert: true,
);
final Color paintColor = enableColor.evaluate(enableAnimation).withAlpha((255.0 * activationAnimation.value).round());
canvas.drawPath(
thumbPath,
new Paint()..color = paintColor,
);
canvas.drawLine(
thumbCenter,
thumbCenter + slideUpOffset,
new Paint()
..color = paintColor
..style = PaintingStyle.stroke
..strokeWidth = 2.0);
labelPainter.paint(canvas, thumbCenter + slideUpOffset + new Offset(-labelPainter.width / 2.0, -labelPainter.height - 4.0));
}
}
class _SliderDemoState extends State<SliderDemo> {
double _value = 25.0;
double _discreteValue = 20.0;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return new Scaffold(
appBar: new AppBar(title: const Text('Sliders')),
body: new Padding(
......@@ -26,7 +140,7 @@ class _SliderDemoState extends State<SliderDemo> {
children: <Widget>[
new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget> [
children: <Widget>[
new Slider(
value: _value,
min: 0.0,
......@@ -35,21 +149,21 @@ class _SliderDemoState extends State<SliderDemo> {
setState(() {
_value = value;
});
}
},
),
const Text('Continuous'),
]
],
),
new Column(
mainAxisSize: MainAxisSize.min,
children: const <Widget> [
children: const <Widget>[
const Slider(value: 0.25, onChanged: null),
const Text('Disabled'),
]
],
),
new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget> [
children: <Widget>[
new Slider(
value: _discreteValue,
min: 0.0,
......@@ -60,11 +174,43 @@ class _SliderDemoState extends State<SliderDemo> {
setState(() {
_discreteValue = value;
});
}
},
),
const Text('Discrete'),
],
),
new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new SliderTheme(
data: theme.sliderTheme.copyWith(
activeRailColor: Colors.deepPurple,
inactiveRailColor: Colors.black26,
activeTickMarkColor: Colors.white70,
inactiveTickMarkColor: Colors.black,
overlayColor: Colors.black12,
thumbColor: Colors.deepPurple,
valueIndicatorColor: Colors.deepPurpleAccent,
thumbShape: new _CustomThumbShape(),
valueIndicatorShape: new _CustomValueIndicatorShape(),
valueIndicatorTextStyle: theme.accentTextTheme.body2.copyWith(color: Colors.black87),
),
child: new Slider(
value: _discreteValue,
min: 0.0,
max: 100.0,
divisions: 5,
label: '${_discreteValue.round()}',
onChanged: (double value) {
setState(() {
_discreteValue = value;
});
},
),
),
const Text('Discrete with Custom Theme'),
],
),
],
),
),
......
......@@ -578,7 +578,10 @@ class _RenderSlider extends RenderBox {
void _updateLabelPainter() {
if (label != null) {
_labelPainter
..text = new TextSpan(style: _theme.accentTextTheme.body2, text: label)
..text = new TextSpan(
style: _sliderTheme.valueIndicatorTextStyle,
text: label,
)
..textDirection = textDirection
..textScaleFactor = _mediaQueryData.textScaleFactor
..layout();
......@@ -849,31 +852,31 @@ class _RenderSlider extends RenderBox {
_valueIndicatorAnimation.status != AnimationStatus.dismissed) {
if (showValueIndicator) {
_sliderTheme.valueIndicatorShape.paint(
this,
context,
isDiscrete,
thumbCenter,
_valueIndicatorAnimation,
_enableAnimation,
_labelPainter,
_sliderTheme,
_textDirection,
value,
activationAnimation: _valueIndicatorAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
labelPainter: _labelPainter,
parentBox: this,
sliderTheme: _sliderTheme,
textDirection: _textDirection,
value: _value,
);
}
}
_sliderTheme.thumbShape.paint(
this,
context,
isDiscrete,
thumbCenter,
_overlayAnimation,
_enableAnimation,
label != null ? _labelPainter : null,
_sliderTheme,
_textDirection,
value,
activationAnimation: _valueIndicatorAnimation,
enableAnimation: _enableAnimation,
isDiscrete: isDiscrete,
labelPainter: _labelPainter,
parentBox: this,
sliderTheme: _sliderTheme,
textDirection: _textDirection,
value: _value,
);
}
......
......@@ -194,6 +194,7 @@ class SliderThemeData extends Diagnosticable {
@required this.thumbShape,
@required this.valueIndicatorShape,
@required this.showValueIndicator,
@required this.valueIndicatorTextStyle,
}) : assert(activeRailColor != null),
assert(inactiveRailColor != null),
assert(disabledActiveRailColor != null),
......@@ -208,6 +209,7 @@ class SliderThemeData extends Diagnosticable {
assert(valueIndicatorColor != null),
assert(thumbShape != null),
assert(valueIndicatorShape != null),
assert(valueIndicatorTextStyle != null),
assert(showValueIndicator != null);
/// Generates a SliderThemeData from three main colors.
......@@ -223,10 +225,12 @@ class SliderThemeData extends Diagnosticable {
@required Color primaryColor,
@required Color primaryColorDark,
@required Color primaryColorLight,
@required TextStyle valueIndicatorTextStyle,
}) {
assert(primaryColor != null);
assert(primaryColorDark != null);
assert(primaryColorLight != null);
assert(valueIndicatorTextStyle != null);
// These are Material Design defaults, and are used to derive
// component Colors (with opacity) from base colors.
......@@ -264,6 +268,7 @@ class SliderThemeData extends Diagnosticable {
valueIndicatorColor: primaryColor.withAlpha(valueIndicatorAlpha),
thumbShape: const RoundSliderThumbShape(),
valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
valueIndicatorTextStyle: valueIndicatorTextStyle,
showValueIndicator: ShowValueIndicator.onlyForDiscrete,
);
}
......@@ -337,6 +342,11 @@ class SliderThemeData extends Diagnosticable {
/// when the thumb is being touched.
final ShowValueIndicator showValueIndicator;
/// The text style for the text on the value indicator.
///
/// By default this is the [ThemeData.accentTextTheme.body2] text theme.
final TextStyle valueIndicatorTextStyle;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
SliderThemeData copyWith({
......@@ -355,6 +365,7 @@ class SliderThemeData extends Diagnosticable {
SliderComponentShape thumbShape,
SliderComponentShape valueIndicatorShape,
ShowValueIndicator showValueIndicator,
TextStyle valueIndicatorTextStyle,
}) {
return new SliderThemeData(
activeRailColor: activeRailColor ?? this.activeRailColor,
......@@ -372,6 +383,7 @@ class SliderThemeData extends Diagnosticable {
thumbShape: thumbShape ?? this.thumbShape,
valueIndicatorShape: valueIndicatorShape ?? this.valueIndicatorShape,
showValueIndicator: showValueIndicator ?? this.showValueIndicator,
valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle,
);
}
......@@ -410,6 +422,7 @@ class SliderThemeData extends Diagnosticable {
thumbShape: t < 0.5 ? a.thumbShape : b.thumbShape,
valueIndicatorShape: t < 0.5 ? a.valueIndicatorShape : b.valueIndicatorShape,
showValueIndicator: t < 0.5 ? a.showValueIndicator : b.showValueIndicator,
valueIndicatorTextStyle: TextStyle.lerp(a.valueIndicatorTextStyle, b.valueIndicatorTextStyle, t),
);
}
......@@ -431,6 +444,7 @@ class SliderThemeData extends Diagnosticable {
thumbShape,
valueIndicatorShape,
showValueIndicator,
valueIndicatorTextStyle,
);
}
......@@ -457,7 +471,8 @@ class SliderThemeData extends Diagnosticable {
otherData.valueIndicatorColor == valueIndicatorColor &&
otherData.thumbShape == thumbShape &&
otherData.valueIndicatorShape == valueIndicatorShape &&
otherData.showValueIndicator == showValueIndicator;
otherData.showValueIndicator == showValueIndicator &&
otherData.valueIndicatorTextStyle == valueIndicatorTextStyle;
}
@override
......@@ -468,6 +483,7 @@ class SliderThemeData extends Diagnosticable {
primaryColor: defaultTheme.primaryColor,
primaryColorDark: defaultTheme.primaryColorDark,
primaryColorLight: defaultTheme.primaryColorLight,
valueIndicatorTextStyle: defaultTheme.accentTextTheme.body2,
);
description.add(new DiagnosticsProperty<Color>('activeRailColor', activeRailColor, defaultValue: defaultData.activeRailColor));
description.add(new DiagnosticsProperty<Color>('inactiveRailColor', inactiveRailColor, defaultValue: defaultData.inactiveRailColor));
......@@ -484,6 +500,7 @@ class SliderThemeData extends Diagnosticable {
description.add(new DiagnosticsProperty<SliderComponentShape>('thumbShape', thumbShape, defaultValue: defaultData.thumbShape, level: DiagnosticLevel.debug));
description.add(new DiagnosticsProperty<SliderComponentShape>('valueIndicatorShape', valueIndicatorShape, defaultValue: defaultData.valueIndicatorShape, level: DiagnosticLevel.debug));
description.add(new EnumProperty<ShowValueIndicator>('showValueIndicator', showValueIndicator, defaultValue: defaultData.showValueIndicator));
description.add(new DiagnosticsProperty<TextStyle>('valueIndicatorTextStyle', valueIndicatorTextStyle, defaultValue: defaultData.valueIndicatorTextStyle));
}
}
......@@ -510,24 +527,27 @@ abstract class SliderComponentShape {
/// [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.
///
/// [value] is the current parametric value (from 0.0 to 1.0) of the slider.
///
/// 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].
/// [value] is the current parametric value (from 0.0 to 1.0) of the slider.
void paint(
RenderBox parentBox,
PaintingContext context,
bool isDiscrete,
Offset thumbCenter,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
);
});
}
/// This is the default shape to a [Slider]'s thumb if no
......@@ -552,17 +572,17 @@ class RoundSliderThumbShape extends SliderComponentShape {
@override
void paint(
RenderBox parentBox,
PaintingContext context,
bool isDiscrete,
Offset thumbCenter,
Offset thumbCenter, {
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 = new Tween<double>(
begin: _disabledThumbRadius,
......@@ -624,8 +644,7 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
static const double _twoSeventyDegrees = 3.0 * math.pi / 2.0;
static const double _ninetyDegrees = math.pi / 2.0;
static const double _thirtyDegrees = math.pi / 6.0;
static const Size _preferredSize =
const Size.fromHeight(_distanceBetweenTopBottomCenters + _topLobeRadius + _bottomLobeRadius);
static const Size _preferredSize = const Size.fromHeight(_distanceBetweenTopBottomCenters + _topLobeRadius + _bottomLobeRadius);
// Set to true if you want a rectangle to be drawn around the label bubble.
// This helps with building tests that check that the label draws in the right
// place (because it prints the rect in the failed test output). It should not
......@@ -767,9 +786,11 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center);
double leftWidthNeeded;
double rightWidthNeeded;
if (shift < 0.0) { // shifting to the left
if (shift < 0.0) {
// shifting to the left
shift = math.max(shift, -halfWidthNeeded);
} else { // shifting to the right
} else {
// shifting to the right
shift = math.min(shift, halfWidthNeeded);
}
rightWidthNeeded = halfWidthNeeded + shift;
......@@ -808,7 +829,8 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
final double stretch = (neckStretchBaseline * t).clamp(0.0, 10.0 * neckStretchBaseline);
final Offset neckStretch = new Offset(0.0, neckStretchBaseline - stretch);
assert(!_debuggingLabelLocation || () {
assert(!_debuggingLabelLocation ||
() {
final Offset leftCenter = _topLobeCenter - new Offset(leftWidthNeeded, 0.0) + neckStretch;
final Offset rightCenter = _topLobeCenter + new Offset(rightWidthNeeded, 0.0) + neckStretch;
final Rect valueRect = new Rect.fromLTRB(
......@@ -819,7 +841,8 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
);
final Paint outlinePaint = new Paint()
..color = const Color(0xffff0000)
..style = PaintingStyle.stroke..strokeWidth = 1.0;
..style = PaintingStyle.stroke
..strokeWidth = 1.0;
canvas.drawRect(valueRect, outlinePaint);
return true;
}());
......@@ -865,18 +888,17 @@ class PaddleSliderValueIndicatorShape extends SliderComponentShape {
@override
void paint(
RenderBox parentBox,
PaintingContext context,
bool isDiscrete,
Offset thumbCenter,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
) {
assert(labelPainter != null);
}) {
final ColorTween enableColor = new ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
......
......@@ -166,6 +166,7 @@ class ThemeData extends Diagnosticable {
primaryColor: primaryColor,
primaryColorLight: primaryColorLight,
primaryColorDark: primaryColorDark,
valueIndicatorTextStyle: accentTextTheme.body2,
);
return new ThemeData.raw(
brightness: brightness,
......
......@@ -25,17 +25,17 @@ class LoggingThumbShape extends SliderComponentShape {
@override
void paint(
RenderBox parentBox,
PaintingContext context,
bool isDiscrete,
Offset thumbCenter,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
) {
}) {
log.add(thumbCenter);
final Paint thumbPaint = new Paint()..color = Colors.red;
context.canvas.drawCircle(thumbCenter, 5.0, thumbPaint);
......
......@@ -104,11 +104,13 @@ void main() {
const Color customColor1 = const Color(0xcafefeed);
const Color customColor2 = const Color(0xdeadbeef);
const Color customColor3 = const Color(0xdecaface);
const Color customColor4 = const Color(0xfeedcafe);
final SliderThemeData sliderTheme = new SliderThemeData.fromPrimaryColors(
primaryColor: customColor1,
primaryColorDark: customColor2,
primaryColorLight: customColor3,
valueIndicatorTextStyle: new ThemeData.fallback().accentTextTheme.body2.copyWith(color: customColor4),
);
expect(sliderTheme.activeRailColor, equals(customColor1.withAlpha(0xff)));
......@@ -126,6 +128,7 @@ void main() {
expect(sliderTheme.thumbShape, equals(const isInstanceOf<RoundSliderThumbShape>()));
expect(sliderTheme.valueIndicatorShape, equals(const isInstanceOf<PaddleSliderValueIndicatorShape>()));
expect(sliderTheme.showValueIndicator, equals(ShowValueIndicator.onlyForDiscrete));
expect(sliderTheme.valueIndicatorTextStyle.color, equals(customColor4));
});
testWidgets('SliderThemeData lerps correctly', (WidgetTester tester) async {
......@@ -133,11 +136,13 @@ void main() {
primaryColor: Colors.black,
primaryColorDark: Colors.black,
primaryColorLight: Colors.black,
valueIndicatorTextStyle: new ThemeData.fallback().accentTextTheme.body2.copyWith(color: Colors.black),
);
final SliderThemeData sliderThemeWhite = new SliderThemeData.fromPrimaryColors(
primaryColor: Colors.white,
primaryColorDark: Colors.white,
primaryColorLight: Colors.white,
valueIndicatorTextStyle: new ThemeData.fallback().accentTextTheme.body2.copyWith(color: Colors.white),
);
final SliderThemeData lerp = SliderThemeData.lerp(sliderThemeBlack, sliderThemeWhite, 0.5);
const Color middleGrey = const Color(0xff7f7f7f);
......@@ -153,6 +158,7 @@ void main() {
expect(lerp.disabledThumbColor, equals(middleGrey.withAlpha(0x52)));
expect(lerp.overlayColor, equals(middleGrey.withAlpha(0x29)));
expect(lerp.valueIndicatorColor, equals(middleGrey.withAlpha(0xff)));
expect(lerp.valueIndicatorTextStyle.color, equals(middleGrey.withAlpha(0xff)));
});
testWidgets('Default slider thumb shape draws correctly', (WidgetTester tester) async {
......
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