mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Slider Visual Update (#14901)
This implements an update to the look of the Slider widget. Specifically, it does the following: * Adds the ability to customize the colors of all components of the slider * Adds the ability to customize the shape of the slider thumb and value indicator * Adds the ability to show the value indicator on continuous sliders * Updates the default value indicator to be a "paddle" shape with new animations. * Changes the tick marks to be visible all the time on discrete sliders * Fixes a memory leak of an animation controller. * Removes "thumbOpenAtMin" flag, which is no longer needed, and can be emulated by the custom thumb shape support. It was not widely used. * Adds tests for all of the new features.
This commit is contained in:
parent
d2dcec22ce
commit
701eff4ac5
|
@ -31,7 +31,6 @@ class _SliderDemoState extends State<SliderDemo> {
|
|||
value: _value,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
thumbOpenAtMin: true,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
|
@ -44,7 +43,7 @@ class _SliderDemoState extends State<SliderDemo> {
|
|||
new Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const <Widget> [
|
||||
const Slider(value: 0.25, thumbOpenAtMin: true, onChanged: null),
|
||||
const Slider(value: 0.25, onChanged: null),
|
||||
const Text('Disabled'),
|
||||
]
|
||||
),
|
||||
|
@ -57,7 +56,6 @@ class _SliderDemoState extends State<SliderDemo> {
|
|||
max: 100.0,
|
||||
divisions: 5,
|
||||
label: '${_discreteValue.round()}',
|
||||
thumbOpenAtMin: true,
|
||||
onChanged: (double value) {
|
||||
setState(() {
|
||||
_discreteValue = value;
|
||||
|
|
|
@ -77,6 +77,7 @@ export 'src/material/scaffold.dart';
|
|||
export 'src/material/scrollbar.dart';
|
||||
export 'src/material/shadows.dart';
|
||||
export 'src/material/slider.dart';
|
||||
export 'src/material/slider_theme.dart';
|
||||
export 'src/material/snack_bar.dart';
|
||||
export 'src/material/stepper.dart';
|
||||
export 'src/material/switch.dart';
|
||||
|
|
|
@ -135,7 +135,7 @@ class ButtonTheme extends InheritedWidget {
|
|||
/// A button theme can be specified as part of the overall Material theme
|
||||
/// using [ThemeData.buttomTheme]. The Material theme's button theme data
|
||||
/// can be overridden with [ButtonTheme].
|
||||
class ButtonThemeData {
|
||||
class ButtonThemeData extends Diagnosticable {
|
||||
/// Create a button theme object that can be used with [ButtonTheme]
|
||||
/// or [ThemeData].
|
||||
///
|
||||
|
@ -251,4 +251,18 @@ class ButtonThemeData {
|
|||
shape,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
final ButtonThemeData defaultTheme = const ButtonThemeData();
|
||||
description.add(new EnumProperty<ButtonTextTheme>('textTheme', textTheme,
|
||||
defaultValue: defaultTheme.textTheme));
|
||||
description.add(new DoubleProperty('minWidth', minWidth, defaultValue: defaultTheme.minWidth));
|
||||
description.add(new DoubleProperty('height', height, defaultValue: defaultTheme.height));
|
||||
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding,
|
||||
defaultValue: defaultTheme.padding));
|
||||
description.add(
|
||||
new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultTheme.shape));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -312,7 +312,7 @@ class InkResponse extends StatefulWidget {
|
|||
/// * [highlightColor], the color of the highlight.
|
||||
/// * [InkSplash.splashFactory], which defines the default splash.
|
||||
/// * [InkRipple.splashFactory], which defines a splash that spreads out
|
||||
/// more aggresively than the default.
|
||||
/// more aggressively than the default.
|
||||
final InteractiveInkFeatureFactory splashFactory;
|
||||
|
||||
/// Whether detected gestures should provide acoustic and/or haptic feedback.
|
||||
|
|
|
@ -53,7 +53,7 @@ class _InputBorderGap extends ChangeNotifier {
|
|||
|
||||
// Used to interpolate between two InputBorders.
|
||||
class _InputBorderTween extends Tween<InputBorder> {
|
||||
_InputBorderTween({ InputBorder begin, InputBorder end }) : super(begin: begin, end: end);
|
||||
_InputBorderTween({InputBorder begin, InputBorder end}) : super(begin: begin, end: end);
|
||||
|
||||
@override
|
||||
InputBorder lerp(double t) => ShapeBorder.lerp(begin, end, t);
|
||||
|
@ -108,7 +108,7 @@ class _BorderContainer extends StatefulWidget {
|
|||
@required this.border,
|
||||
@required this.gap,
|
||||
@required this.gapAnimation,
|
||||
this.child
|
||||
this.child,
|
||||
}) : assert(border != null),
|
||||
assert(gap != null),
|
||||
super(key: key);
|
||||
|
@ -2164,7 +2164,7 @@ class InputDecoration {
|
|||
/// The [InputDecoration.applyDefaults] method is used to combine a input
|
||||
/// decoration theme with an [InputDecoration] object.
|
||||
@immutable
|
||||
class InputDecorationTheme {
|
||||
class InputDecorationTheme extends Diagnosticable {
|
||||
/// Creates a value for [ThemeData.inputDecorationTheme] that
|
||||
/// defines default values for [InputDecorator].
|
||||
///
|
||||
|
@ -2300,4 +2300,36 @@ class InputDecorationTheme {
|
|||
/// * [OutlineInputBorder], an [InputDecorator] border which draws a
|
||||
/// rounded rectangle around the input decorator's container.
|
||||
final InputBorder border;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
final InputDecorationTheme defaultTheme = const InputDecorationTheme();
|
||||
description.add(new DiagnosticsProperty<TextStyle>('labelStyle', labelStyle,
|
||||
defaultValue: defaultTheme.labelStyle));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('helperStyle', helperStyle,
|
||||
defaultValue: defaultTheme.helperStyle));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('hintStyle', hintStyle,
|
||||
defaultValue: defaultTheme.hintStyle));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('errorStyle', errorStyle,
|
||||
defaultValue: defaultTheme.errorStyle));
|
||||
description
|
||||
.add(new DiagnosticsProperty<bool>('isDense', isDense, defaultValue: defaultTheme.isDense));
|
||||
description.add(new DiagnosticsProperty<EdgeInsets>('contentPadding', contentPadding,
|
||||
defaultValue: defaultTheme.contentPadding));
|
||||
description.add(new DiagnosticsProperty<bool>('isCollapsed', isCollapsed,
|
||||
defaultValue: defaultTheme.isCollapsed));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('prefixStyle', prefixStyle,
|
||||
defaultValue: defaultTheme.prefixStyle));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('suffixStyle', suffixStyle,
|
||||
defaultValue: defaultTheme.suffixStyle));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('counterStyle', counterStyle,
|
||||
defaultValue: defaultTheme.counterStyle));
|
||||
description
|
||||
.add(new DiagnosticsProperty<bool>('filled', filled, defaultValue: defaultTheme.filled));
|
||||
description.add(new DiagnosticsProperty<Color>('fillColor', fillColor,
|
||||
defaultValue: defaultTheme.fillColor));
|
||||
description.add(
|
||||
new DiagnosticsProperty<InputBorder>('border', border, defaultValue: defaultTheme.border));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,39 +9,61 @@ import 'package:flutter/gestures.dart';
|
|||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'constants.dart';
|
||||
import 'debug.dart';
|
||||
import 'material.dart';
|
||||
import 'slider_theme.dart';
|
||||
import 'theme.dart';
|
||||
import 'typography.dart';
|
||||
|
||||
/// A material design slider.
|
||||
/// A Material Design slider.
|
||||
///
|
||||
/// Used to select from a range of values.
|
||||
///
|
||||
/// A slider can be used to select from either a continuous or a discrete set of
|
||||
/// values. The default is use a continuous range of values from [min] to [max].
|
||||
/// To use discrete values, use a non-null value for [divisions], which
|
||||
/// values. The default is to use a continuous range of values from [min] to
|
||||
/// [max]. To use discrete values, use a non-null value for [divisions], which
|
||||
/// indicates the number of discrete intervals. For example, if [min] is 0.0 and
|
||||
/// [max] is 50.0 and [divisions] is 5, then the slider can take on the values
|
||||
/// [max] is 50.0 and [divisions] is 5, then the slider can take on the
|
||||
/// discrete values 0.0, 10.0, 20.0, 30.0, 40.0, and 50.0.
|
||||
///
|
||||
/// The terms for the parts of a slider are:
|
||||
///
|
||||
/// * The "thumb", which is a shape that slides horizontally when the user
|
||||
/// drags it.
|
||||
/// * The "rail", 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 "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
|
||||
/// maximum value.
|
||||
///
|
||||
/// The slider will be disabled if [onChanged] is null or if the range given by
|
||||
/// [min]..[max] is empty (i.e. if [min] is equal to [max]).
|
||||
///
|
||||
/// The slider itself does not maintain any state. Instead, when the state of
|
||||
/// the slider changes, the widget calls the [onChanged] callback. Most widgets
|
||||
/// that use a slider will listen for the [onChanged] callback and rebuild the
|
||||
/// slider with a new [value] to update the visual appearance of the slider.
|
||||
/// The slider widget itself does not maintain any state. Instead, when the state
|
||||
/// of the slider changes, the widget calls the [onChanged] callback. Most
|
||||
/// widgets that use a slider will listen for the [onChanged] callback and
|
||||
/// rebuild the slider with a new [value] to update the visual appearance of the
|
||||
/// slider.
|
||||
///
|
||||
/// By default, a slider will be as wide as possible, centered vertically. When
|
||||
/// given unbounded constraints, it will attempt to make the track 144 pixels
|
||||
/// given unbounded constraints, it will attempt to make the rail 144 pixels
|
||||
/// wide (with margins on each side) and will shrink-wrap vertically.
|
||||
///
|
||||
/// Requires one of its ancestors to be a [Material] widget.
|
||||
///
|
||||
/// To determine how it should be displayed (e.g. colors, thumb shape, etc.),
|
||||
/// a slider uses the [SliderThemeData] available from either a [SliderTheme]
|
||||
/// widget or the [ThemeData.sliderTheme] a [Theme] widget above it in the
|
||||
/// widget tree. You can also override some of the colors with the [activeColor]
|
||||
/// and [inactiveColor] properties, although more fine-grained control of the
|
||||
/// look is achieved using a [SliderThemeData].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliderTheme] and [SliderThemeData] for information about controlling
|
||||
/// the visual appearance of the slider.
|
||||
/// * [Radio], for selecting among a set of explicit values.
|
||||
/// * [Checkbox] and [Switch], for toggling a particular value on or off.
|
||||
/// * <https://material.google.com/components/sliders.html>
|
||||
|
@ -55,6 +77,10 @@ class Slider extends StatefulWidget {
|
|||
///
|
||||
/// * [value] determines currently selected value for this slider.
|
||||
/// * [onChanged] is called when the user selects a new value for the slider.
|
||||
///
|
||||
/// You can override some of the colors with the [activeColor] and
|
||||
/// [inactiveColor] properties, although more fine-grained control of the
|
||||
/// appearance is achieved using a [SliderThemeData].
|
||||
const Slider({
|
||||
Key key,
|
||||
@required this.value,
|
||||
|
@ -65,14 +91,12 @@ class Slider extends StatefulWidget {
|
|||
this.label,
|
||||
this.activeColor,
|
||||
this.inactiveColor,
|
||||
this.thumbOpenAtMin: false,
|
||||
}) : assert(value != null),
|
||||
assert(min != null),
|
||||
assert(max != null),
|
||||
assert(min <= max),
|
||||
assert(value >= min && value <= max),
|
||||
assert(divisions == null || divisions > 0),
|
||||
assert(thumbOpenAtMin != null),
|
||||
super(key: key);
|
||||
|
||||
/// The currently selected value for this slider.
|
||||
|
@ -131,32 +155,43 @@ class Slider extends StatefulWidget {
|
|||
|
||||
/// A label to show above the slider when the slider is active.
|
||||
///
|
||||
/// Typically used to display the value of a discrete slider.
|
||||
/// It is used to display the value of a discrete slider, and it is displayed
|
||||
/// as part of the value indicator shape.
|
||||
///
|
||||
/// The label is rendered using the active [ThemeData]'s
|
||||
/// [ThemeData.accentTextTheme.body2] text style.
|
||||
///
|
||||
/// If null, then the value indicator will not be displayed.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliderComponentShape] for how to create a custom value indicator
|
||||
/// shape.
|
||||
final String label;
|
||||
|
||||
/// The color to use for the portion of the slider that has been selected.
|
||||
/// The color to use for the portion of the slider rail that is active.
|
||||
///
|
||||
/// Defaults to accent color of the current [Theme].
|
||||
/// The "active" side of the slider is the side between the thumb and the
|
||||
/// minimum value.
|
||||
///
|
||||
/// Defaults to [SliderTheme.activeRailColor] of the current [SliderTheme].
|
||||
///
|
||||
/// Using a [SliderTheme] gives much more fine-grained control over the
|
||||
/// appearance of various components of the slider.
|
||||
final Color activeColor;
|
||||
|
||||
/// The color for the unselected portion of the slider.
|
||||
/// The color for the inactive portion of the slider rail.
|
||||
///
|
||||
/// Defaults to the unselected widget color of the current [Theme].
|
||||
/// The "inactive" side of the slider is the side between the thumb and the
|
||||
/// maximum value.
|
||||
///
|
||||
/// Defaults to the [SliderTheme.inactiveRailColor] of the current
|
||||
/// [SliderTheme].
|
||||
///
|
||||
/// Using a [SliderTheme] gives much more fine-grained control over the
|
||||
/// appearance of various components of the slider.
|
||||
final Color inactiveColor;
|
||||
|
||||
/// Whether the thumb should be an open circle when the slider is at its minimum position.
|
||||
///
|
||||
/// When this property is false, the thumb does not change when it the slider
|
||||
/// reaches its minimum position.
|
||||
///
|
||||
/// This property is useful, for example, when the minimum value represents a
|
||||
/// qualitatively different state. For a slider that controls the volume of
|
||||
/// a sound, for example, the minimum value represents "no sound at all,"
|
||||
/// which is qualitatively different from even a very soft sound.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool thumbOpenAtMin;
|
||||
|
||||
@override
|
||||
_SliderState createState() => new _SliderState();
|
||||
|
||||
|
@ -170,44 +205,88 @@ class Slider extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
||||
_SliderState() {
|
||||
_reactionController = new AnimationController(
|
||||
static const Duration enableAnimationDuration = const Duration(milliseconds: 75);
|
||||
static const Duration positionAnimationDuration = const Duration(milliseconds: 75);
|
||||
|
||||
AnimationController reactionController;
|
||||
AnimationController enableController;
|
||||
AnimationController positionController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
reactionController = new AnimationController(
|
||||
duration: kRadialReactionDuration,
|
||||
vsync: this,
|
||||
);
|
||||
enableController = new AnimationController(
|
||||
duration: enableAnimationDuration,
|
||||
vsync: this,
|
||||
);
|
||||
positionController = new AnimationController(
|
||||
duration: positionAnimationDuration,
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleChanged(double value) {
|
||||
assert(widget.onChanged != null);
|
||||
widget.onChanged(value * (widget.max - widget.min) + widget.min);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_reactionController?.dispose();
|
||||
reactionController.dispose();
|
||||
enableController.dispose();
|
||||
positionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Have to keep the reaction controller here so that we may dispose of it
|
||||
// properly.
|
||||
AnimationController _reactionController;
|
||||
void _handleChanged(double value) {
|
||||
assert(widget.onChanged != null);
|
||||
final double transformedValue = _lerp(value);
|
||||
widget.onChanged(transformedValue);
|
||||
}
|
||||
|
||||
// Returns a number between min and max, proportional to value, which must
|
||||
// be between 0.0 and 1.0.
|
||||
double _lerp(double value) {
|
||||
assert(value >= 0.0);
|
||||
assert(value <= 1.0);
|
||||
return value * (widget.max - widget.min) + widget.min;
|
||||
}
|
||||
|
||||
// Returns a number between 0.0 and 1.0, given a value between min and max.
|
||||
double _unlerp(double value) {
|
||||
assert(value <= widget.max);
|
||||
assert(value >= widget.min);
|
||||
return widget.max > widget.min ? (widget.value - widget.min) / (widget.max - widget.min) : 0.0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterial(context));
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
SliderThemeData sliderTheme = SliderTheme.of(context);
|
||||
|
||||
// If the widget has active or inactive colors specified, then we plug them
|
||||
// in to the slider theme as best we can. If the developer wants more
|
||||
// control than that, then they need to use a SliderTheme.
|
||||
if (widget.activeColor != null || widget.inactiveColor != null) {
|
||||
sliderTheme = sliderTheme.copyWith(
|
||||
activeRailColor: widget.activeColor,
|
||||
inactiveRailColor: widget.inactiveColor,
|
||||
activeTickMarkColor: widget.inactiveColor,
|
||||
inactiveTickMarkColor: widget.activeColor,
|
||||
thumbColor: widget.activeColor,
|
||||
valueIndicatorColor: widget.activeColor,
|
||||
overlayColor: widget.activeColor?.withAlpha(0x29),
|
||||
);
|
||||
}
|
||||
|
||||
return new _SliderRenderObjectWidget(
|
||||
value: widget.max > widget.min ? (widget.value - widget.min) / (widget.max - widget.min) : 0.0,
|
||||
value: _unlerp(widget.value),
|
||||
divisions: widget.divisions,
|
||||
label: widget.label,
|
||||
activeColor: widget.activeColor ?? theme.accentColor,
|
||||
inactiveColor: widget.inactiveColor ?? theme.unselectedWidgetColor,
|
||||
thumbOpenAtMin: widget.thumbOpenAtMin,
|
||||
textTheme: theme.accentTextTheme,
|
||||
sliderTheme: sliderTheme,
|
||||
textScaleFactor: MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
|
||||
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
|
||||
vsync: this,
|
||||
reactionController: _reactionController,
|
||||
state: this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -218,27 +297,19 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
this.value,
|
||||
this.divisions,
|
||||
this.label,
|
||||
this.activeColor,
|
||||
this.inactiveColor,
|
||||
this.thumbOpenAtMin,
|
||||
this.textTheme,
|
||||
this.sliderTheme,
|
||||
this.textScaleFactor,
|
||||
this.onChanged,
|
||||
this.vsync,
|
||||
this.reactionController,
|
||||
this.state,
|
||||
}) : super(key: key);
|
||||
|
||||
final double value;
|
||||
final int divisions;
|
||||
final String label;
|
||||
final Color activeColor;
|
||||
final Color inactiveColor;
|
||||
final bool thumbOpenAtMin;
|
||||
final TextTheme textTheme;
|
||||
final SliderThemeData sliderTheme;
|
||||
final double textScaleFactor;
|
||||
final ValueChanged<double> onChanged;
|
||||
final TickerProvider vsync;
|
||||
final AnimationController reactionController;
|
||||
final _SliderState state;
|
||||
|
||||
@override
|
||||
_RenderSlider createRenderObject(BuildContext context) {
|
||||
|
@ -246,14 +317,11 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
value: value,
|
||||
divisions: divisions,
|
||||
label: label,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
thumbOpenAtMin: thumbOpenAtMin,
|
||||
textTheme: textTheme,
|
||||
sliderTheme: sliderTheme,
|
||||
theme: Theme.of(context),
|
||||
textScaleFactor: textScaleFactor,
|
||||
onChanged: onChanged,
|
||||
vsync: vsync,
|
||||
reactionController: reactionController,
|
||||
state: state,
|
||||
textDirection: Directionality.of(context),
|
||||
);
|
||||
}
|
||||
|
@ -264,74 +332,47 @@ class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
|
|||
..value = value
|
||||
..divisions = divisions
|
||||
..label = label
|
||||
..activeColor = activeColor
|
||||
..inactiveColor = inactiveColor
|
||||
..thumbOpenAtMin = thumbOpenAtMin
|
||||
..textTheme = textTheme
|
||||
..sliderTheme = sliderTheme
|
||||
..theme = Theme.of(context)
|
||||
..textScaleFactor = textScaleFactor
|
||||
..onChanged = onChanged
|
||||
..textDirection = Directionality.of(context);
|
||||
// Ticker provider cannot change since there's a 1:1 relationship between
|
||||
// the _SliderRenderObjectWidget object and the _SliderState object.
|
||||
// Ticker provider cannot change since there's a 1:1 relationship between
|
||||
// the _SliderRenderObjectWidget object and the _SliderState object.
|
||||
}
|
||||
}
|
||||
|
||||
const double _kThumbRadius = 6.0;
|
||||
const double _kActiveThumbRadius = 9.0;
|
||||
const double _kDisabledThumbRadius = 4.0;
|
||||
const double _kReactionRadius = 16.0;
|
||||
const double _kPreferredTrackWidth = 144.0;
|
||||
const double _kMinimumTrackWidth = _kActiveThumbRadius; // biggest of the thumb radii
|
||||
const double _kPreferredTotalWidth = _kPreferredTrackWidth + 2 * _kReactionRadius;
|
||||
const double _kMinimumTotalWidth = _kMinimumTrackWidth + 2 * _kReactionRadius;
|
||||
const double _overlayRadius = 16.0;
|
||||
const double _overlayDiameter = _overlayRadius * 2.0;
|
||||
const double _railHeight = 2.0;
|
||||
const double _preferredRailWidth = 144.0;
|
||||
const double _preferredTotalWidth = _preferredRailWidth + _overlayDiameter;
|
||||
|
||||
final Color _kActiveTrackColor = Colors.grey;
|
||||
final Tween<double> _kReactionRadiusTween = new Tween<double>(begin: _kThumbRadius, end: _kReactionRadius);
|
||||
final Tween<double> _kThumbRadiusTween = new Tween<double>(begin: _kThumbRadius, end: _kActiveThumbRadius);
|
||||
final ColorTween _kTickColorTween = new ColorTween(begin: Colors.transparent, end: Colors.black54);
|
||||
const Duration _kDiscreteTransitionDuration = const Duration(milliseconds: 500);
|
||||
|
||||
const double _kLabelBalloonRadius = 14.0;
|
||||
final Tween<double> _kLabelBalloonCenterTween = new Tween<double>(begin: 0.0, end: -_kLabelBalloonRadius * 2.0);
|
||||
final Tween<double> _kLabelBalloonRadiusTween = new Tween<double>(begin: _kThumbRadius, end: _kLabelBalloonRadius);
|
||||
final Tween<double> _kLabelBalloonTipTween = new Tween<double>(begin: 0.0, end: -8.0);
|
||||
final double _kLabelBalloonTipAttachmentRatio = math.sin(math.PI / 4.0);
|
||||
|
||||
const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider.
|
||||
|
||||
double _getAdditionalHeightForLabel(String label) {
|
||||
return label == null ? 0.0 : _kLabelBalloonRadius * 2.0;
|
||||
}
|
||||
|
||||
double _getPreferredTotalHeight(String label) {
|
||||
return 2 * _kReactionRadius + _getAdditionalHeightForLabel(label);
|
||||
}
|
||||
const double _adjustmentUnit = 0.1; // Matches iOS implementation of material slider.
|
||||
final Tween<double> _overlayRadiusTween = new Tween<double>(begin: 0.0, end: _overlayRadius);
|
||||
|
||||
class _RenderSlider extends RenderBox {
|
||||
_RenderSlider({
|
||||
@required double value,
|
||||
int divisions,
|
||||
String label,
|
||||
Color activeColor,
|
||||
Color inactiveColor,
|
||||
bool thumbOpenAtMin,
|
||||
TextTheme textTheme,
|
||||
SliderThemeData sliderTheme,
|
||||
ThemeData theme,
|
||||
double textScaleFactor,
|
||||
ValueChanged<double> onChanged,
|
||||
TickerProvider vsync,
|
||||
@required _SliderState state,
|
||||
@required TextDirection textDirection,
|
||||
@required AnimationController reactionController,
|
||||
}) : assert(value != null && value >= 0.0 && value <= 1.0),
|
||||
assert(state != null),
|
||||
assert(textDirection != null),
|
||||
_label = label,
|
||||
_value = value,
|
||||
_divisions = divisions,
|
||||
_activeColor = activeColor,
|
||||
_inactiveColor = inactiveColor,
|
||||
_thumbOpenAtMin = thumbOpenAtMin,
|
||||
_textTheme = textTheme,
|
||||
_sliderTheme = sliderTheme,
|
||||
_theme = theme,
|
||||
_textScaleFactor = textScaleFactor,
|
||||
_onChanged = onChanged,
|
||||
_state = state,
|
||||
_textDirection = textDirection {
|
||||
_updateLabelPainter();
|
||||
final GestureArenaTeam team = new GestureArenaTeam();
|
||||
|
@ -342,104 +383,106 @@ class _RenderSlider extends RenderBox {
|
|||
..onEnd = _handleDragEnd;
|
||||
_tap = new TapGestureRecognizer()
|
||||
..team = team
|
||||
..onTapCancel = _endInteraction
|
||||
..onTapDown = _handleTapDown
|
||||
..onTapUp = _handleTapUp;
|
||||
_reactionController = reactionController;
|
||||
_reaction = new CurvedAnimation(
|
||||
parent: _reactionController,
|
||||
curve: Curves.fastOutSlowIn
|
||||
)..addListener(markNeedsPaint);
|
||||
_position = new AnimationController(
|
||||
value: value,
|
||||
duration: _kDiscreteTransitionDuration,
|
||||
vsync: vsync,
|
||||
)..addListener(markNeedsPaint);
|
||||
_reaction = new CurvedAnimation(parent: state.reactionController, curve: Curves.fastOutSlowIn)
|
||||
..addListener(markNeedsPaint);
|
||||
state.enableController.value = isInteractive ? 1.0 : 0.0;
|
||||
_enableAnimation = new CurvedAnimation(parent: state.enableController, curve: Curves.easeInOut)
|
||||
..addListener(markNeedsPaint);
|
||||
state.positionController.value = _value;
|
||||
}
|
||||
|
||||
double get value => _value;
|
||||
double _value;
|
||||
|
||||
_SliderState _state;
|
||||
|
||||
set value(double newValue) {
|
||||
assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
|
||||
if (newValue == _value)
|
||||
final double convertedValue = isDiscrete ? _discretize(newValue) : newValue;
|
||||
if (convertedValue == _value) {
|
||||
return;
|
||||
_value = newValue;
|
||||
if (divisions != null)
|
||||
_position.animateTo(newValue, curve: Curves.fastOutSlowIn);
|
||||
else
|
||||
_position.value = newValue;
|
||||
}
|
||||
_value = convertedValue;
|
||||
if (isDiscrete) {
|
||||
_state.positionController.animateTo(convertedValue, curve: Curves.easeInOut);
|
||||
} else {
|
||||
_state.positionController.value = convertedValue;
|
||||
}
|
||||
}
|
||||
|
||||
int get divisions => _divisions;
|
||||
int _divisions;
|
||||
|
||||
set divisions(int value) {
|
||||
if (value == _divisions)
|
||||
if (value == _divisions) {
|
||||
return;
|
||||
}
|
||||
_divisions = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
String get label => _label;
|
||||
String _label;
|
||||
|
||||
set label(String value) {
|
||||
if (value == _label)
|
||||
if (value == _label) {
|
||||
return;
|
||||
}
|
||||
_label = value;
|
||||
_updateLabelPainter();
|
||||
}
|
||||
|
||||
Color get activeColor => _activeColor;
|
||||
Color _activeColor;
|
||||
set activeColor(Color value) {
|
||||
if (value == _activeColor)
|
||||
SliderThemeData get sliderTheme => _sliderTheme;
|
||||
SliderThemeData _sliderTheme;
|
||||
|
||||
set sliderTheme(SliderThemeData value) {
|
||||
if (value == _sliderTheme) {
|
||||
return;
|
||||
_activeColor = value;
|
||||
}
|
||||
_sliderTheme = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
Color get inactiveColor => _inactiveColor;
|
||||
Color _inactiveColor;
|
||||
set inactiveColor(Color value) {
|
||||
if (value == _inactiveColor)
|
||||
return;
|
||||
_inactiveColor = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
ThemeData get theme => _theme;
|
||||
ThemeData _theme;
|
||||
|
||||
bool get thumbOpenAtMin => _thumbOpenAtMin;
|
||||
bool _thumbOpenAtMin;
|
||||
set thumbOpenAtMin(bool value) {
|
||||
if (value == _thumbOpenAtMin)
|
||||
set theme(ThemeData value) {
|
||||
if (value == _theme) {
|
||||
return;
|
||||
_thumbOpenAtMin = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
TextTheme get textTheme => _textTheme;
|
||||
TextTheme _textTheme;
|
||||
set textTheme(TextTheme value) {
|
||||
if (value == _textTheme)
|
||||
return;
|
||||
_textTheme = value;
|
||||
}
|
||||
_theme = value;
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
double get textScaleFactor => _textScaleFactor;
|
||||
double _textScaleFactor;
|
||||
|
||||
set textScaleFactor(double value) {
|
||||
if (value == _textScaleFactor)
|
||||
if (value == _textScaleFactor) {
|
||||
return;
|
||||
}
|
||||
_textScaleFactor = value;
|
||||
_updateLabelPainter();
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
ValueChanged<double> get onChanged => _onChanged;
|
||||
ValueChanged<double> _onChanged;
|
||||
|
||||
set onChanged(ValueChanged<double> value) {
|
||||
if (value == _onChanged)
|
||||
if (value == _onChanged) {
|
||||
return;
|
||||
}
|
||||
final bool wasInteractive = isInteractive;
|
||||
_onChanged = value;
|
||||
if (wasInteractive != isInteractive) {
|
||||
if (isInteractive) {
|
||||
_state.enableController.forward();
|
||||
} else {
|
||||
_state.enableController.reverse();
|
||||
}
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
@ -447,21 +490,23 @@ class _RenderSlider extends RenderBox {
|
|||
|
||||
TextDirection get textDirection => _textDirection;
|
||||
TextDirection _textDirection;
|
||||
|
||||
set textDirection(TextDirection value) {
|
||||
assert(value != null);
|
||||
if (value == _textDirection)
|
||||
if (value == _textDirection) {
|
||||
return;
|
||||
}
|
||||
_textDirection = value;
|
||||
_updateLabelPainter();
|
||||
}
|
||||
|
||||
void _updateLabelPainter() {
|
||||
if (label != null) {
|
||||
// We have to account for the text scale factor in the supplied theme.
|
||||
final TextStyle style = _theme.accentTextTheme.body2
|
||||
.copyWith(fontSize: _theme.accentTextTheme.body2.fontSize * _textScaleFactor);
|
||||
_labelPainter
|
||||
..text = new TextSpan(
|
||||
style: _textTheme.body1.copyWith(fontSize: 10.0 * _textScaleFactor),
|
||||
text: label,
|
||||
)
|
||||
..text = new TextSpan(style: style, text: label)
|
||||
..textDirection = textDirection
|
||||
..layout();
|
||||
} else {
|
||||
|
@ -473,14 +518,11 @@ class _RenderSlider extends RenderBox {
|
|||
markNeedsLayout();
|
||||
}
|
||||
|
||||
double get _trackLength => size.width - 2.0 * _kReactionRadius;
|
||||
double get _railLength => size.width - _overlayDiameter;
|
||||
|
||||
Animation<double> _reaction;
|
||||
AnimationController _reactionController;
|
||||
|
||||
AnimationController _position;
|
||||
Animation<double> _enableAnimation;
|
||||
final TextPainter _labelPainter = new TextPainter();
|
||||
|
||||
HorizontalDragGestureRecognizer _drag;
|
||||
TapGestureRecognizer _tap;
|
||||
bool _active = false;
|
||||
|
@ -488,6 +530,8 @@ class _RenderSlider extends RenderBox {
|
|||
|
||||
bool get isInteractive => onChanged != null;
|
||||
|
||||
bool get isDiscrete => divisions != null && divisions > 0;
|
||||
|
||||
double _getValueFromVisualPosition(double visualPosition) {
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
|
@ -499,29 +543,40 @@ class _RenderSlider extends RenderBox {
|
|||
}
|
||||
|
||||
double _getValueFromGlobalPosition(Offset globalPosition) {
|
||||
final double visualPosition = (globalToLocal(globalPosition).dx - _kReactionRadius) / _trackLength;
|
||||
final double visualPosition = (globalToLocal(globalPosition).dx - _overlayRadius) / _railLength;
|
||||
return _getValueFromVisualPosition(visualPosition);
|
||||
}
|
||||
|
||||
double _discretize(double value) {
|
||||
double result = value.clamp(0.0, 1.0);
|
||||
if (divisions != null)
|
||||
if (isDiscrete) {
|
||||
result = (result * divisions).round() / divisions;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
void _startInteraction(Offset globalPosition) {
|
||||
if (isInteractive) {
|
||||
_active = true;
|
||||
_currentDragValue = _getValueFromGlobalPosition(details.globalPosition);
|
||||
_currentDragValue = _getValueFromGlobalPosition(globalPosition);
|
||||
onChanged(_discretize(_currentDragValue));
|
||||
_reactionController.forward();
|
||||
_state.reactionController.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _endInteraction() {
|
||||
if (_active) {
|
||||
_active = false;
|
||||
_currentDragValue = 0.0;
|
||||
_state.reactionController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDragStart(DragStartDetails details) => _startInteraction(details.globalPosition);
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
if (isInteractive) {
|
||||
final double valueDelta = details.primaryDelta / _trackLength;
|
||||
final double valueDelta = details.primaryDelta / _railLength;
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
_currentDragValue -= valueDelta;
|
||||
|
@ -534,18 +589,11 @@ class _RenderSlider extends RenderBox {
|
|||
}
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
if (_active) {
|
||||
_active = false;
|
||||
_currentDragValue = 0.0;
|
||||
_reactionController.reverse();
|
||||
}
|
||||
}
|
||||
void _handleDragEnd(DragEndDetails details) => _endInteraction();
|
||||
|
||||
void _handleTapUp(TapUpDetails details) {
|
||||
if (isInteractive && !_active)
|
||||
onChanged(_discretize(_getValueFromGlobalPosition(details.globalPosition)));
|
||||
}
|
||||
void _handleTapDown(TapDownDetails details) => _startInteraction(details.globalPosition);
|
||||
|
||||
void _handleTapUp(TapUpDetails details) => _endInteraction();
|
||||
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => true;
|
||||
|
@ -562,25 +610,22 @@ class _RenderSlider extends RenderBox {
|
|||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
return _kMinimumTotalWidth;
|
||||
return math.max(_overlayDiameter,
|
||||
_sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width);
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
// This doesn't quite match the definition of computeMaxIntrinsicWidth,
|
||||
// but it seems within the spirit...
|
||||
return _kPreferredTotalWidth;
|
||||
return _preferredTotalWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
return _getPreferredTotalHeight(label);
|
||||
}
|
||||
double computeMinIntrinsicHeight(double width) => _overlayDiameter;
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
return _getPreferredTotalHeight(label);
|
||||
}
|
||||
double computeMaxIntrinsicHeight(double width) => _overlayDiameter;
|
||||
|
||||
@override
|
||||
bool get sizedByParent => true;
|
||||
|
@ -588,123 +633,174 @@ class _RenderSlider extends RenderBox {
|
|||
@override
|
||||
void performResize() {
|
||||
size = new Size(
|
||||
constraints.hasBoundedWidth ? constraints.maxWidth : _kPreferredTotalWidth,
|
||||
constraints.hasBoundedHeight ? constraints.maxHeight : _getPreferredTotalHeight(label),
|
||||
constraints.hasBoundedWidth ? constraints.maxWidth : _preferredTotalWidth,
|
||||
constraints.hasBoundedHeight ? constraints.maxHeight : _overlayDiameter,
|
||||
);
|
||||
}
|
||||
|
||||
void _paintTickMarks(
|
||||
Canvas canvas, Rect railLeft, Rect railRight, Paint leftPaint, Paint rightPaint) {
|
||||
if (isDiscrete) {
|
||||
// The ticks are tiny circles that are the same height as the rail.
|
||||
const double tickRadius = _railHeight / 2.0;
|
||||
final double railWidth = railRight.right - railLeft.left;
|
||||
final double dx = (railWidth - _railHeight) / divisions;
|
||||
// If the ticks would be too dense, don't bother painting them.
|
||||
if (dx >= 3.0 * _railHeight) {
|
||||
for (int i = 0; i <= divisions; i += 1) {
|
||||
final double left = railLeft.left + i * dx;
|
||||
final Offset center = new Offset(left + tickRadius, railLeft.top + tickRadius);
|
||||
if (railLeft.contains(center)) {
|
||||
canvas.drawCircle(center, tickRadius, leftPaint);
|
||||
} else if (railRight.contains(center)) {
|
||||
canvas.drawCircle(center, tickRadius, rightPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _paintOverlay(Canvas canvas, Offset center) {
|
||||
if (!_reaction.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 reactionPaint = new Paint()..color = _sliderTheme.overlayColor;
|
||||
final double radius = _overlayRadiusTween.evaluate(_reaction);
|
||||
canvas.drawCircle(center, radius, reactionPaint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final Canvas canvas = context.canvas;
|
||||
|
||||
final double trackLength = size.width - 2 * _kReactionRadius;
|
||||
final bool enabled = isInteractive;
|
||||
final double value = _position.value;
|
||||
final bool thumbAtMin = value == 0.0;
|
||||
final double railLength = size.width - 2 * _overlayRadius;
|
||||
final double value = _state.positionController.value;
|
||||
final ColorTween activeRailEnableColor = new ColorTween(
|
||||
begin: _sliderTheme.disabledActiveRailColor, end: _sliderTheme.activeRailColor);
|
||||
final ColorTween inactiveRailEnableColor = new ColorTween(
|
||||
begin: _sliderTheme.disabledInactiveRailColor, end: _sliderTheme.inactiveRailColor);
|
||||
final ColorTween activeTickMarkEnableColor = new ColorTween(
|
||||
begin: _sliderTheme.disabledActiveTickMarkColor, end: _sliderTheme.activeTickMarkColor);
|
||||
final ColorTween inactiveTickMarkEnableColor = new ColorTween(
|
||||
begin: _sliderTheme.disabledInactiveTickMarkColor, end: _sliderTheme.inactiveTickMarkColor);
|
||||
|
||||
final Paint primaryPaint = new Paint()..color = enabled ? _activeColor : _inactiveColor;
|
||||
final Paint trackPaint = new Paint()..color = _inactiveColor;
|
||||
final Paint activeRailPaint = new Paint()
|
||||
..color = activeRailEnableColor.evaluate(_enableAnimation);
|
||||
final Paint inactiveRailPaint = new Paint()
|
||||
..color = inactiveRailEnableColor.evaluate(_enableAnimation);
|
||||
final Paint activeTickMarkPaint = new Paint()
|
||||
..color = activeTickMarkEnableColor.evaluate(_enableAnimation);
|
||||
final Paint inactiveTickMarkPaint = new Paint()
|
||||
..color = inactiveTickMarkEnableColor.evaluate(_enableAnimation);
|
||||
|
||||
double visualPosition;
|
||||
Paint leftPaint;
|
||||
Paint rightPaint;
|
||||
Paint leftRailPaint;
|
||||
Paint rightRailPaint;
|
||||
Paint leftTickMarkPaint;
|
||||
Paint rightTickMarkPaint;
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
visualPosition = 1.0 - value;
|
||||
leftPaint = trackPaint;
|
||||
rightPaint = primaryPaint;
|
||||
leftRailPaint = inactiveRailPaint;
|
||||
rightRailPaint = activeRailPaint;
|
||||
leftTickMarkPaint = inactiveTickMarkPaint;
|
||||
rightTickMarkPaint = activeTickMarkPaint;
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
visualPosition = value;
|
||||
leftPaint = primaryPaint;
|
||||
rightPaint = trackPaint;
|
||||
leftRailPaint = activeRailPaint;
|
||||
rightRailPaint = inactiveRailPaint;
|
||||
leftTickMarkPaint = activeTickMarkPaint;
|
||||
rightTickMarkPaint = inactiveTickMarkPaint;
|
||||
break;
|
||||
}
|
||||
|
||||
final double additionalHeightForLabel = _getAdditionalHeightForLabel(label);
|
||||
final double trackCenter = offset.dy + (size.height - additionalHeightForLabel) / 2.0 + additionalHeightForLabel;
|
||||
final double trackLeft = offset.dx + _kReactionRadius;
|
||||
final double trackTop = trackCenter - 1.0;
|
||||
final double trackBottom = trackCenter + 1.0;
|
||||
final double trackRight = trackLeft + trackLength;
|
||||
final double trackActive = trackLeft + trackLength * visualPosition;
|
||||
const double railRadius = _railHeight / 2.0;
|
||||
const double thumbGap = 2.0;
|
||||
|
||||
final Offset thumbCenter = new Offset(trackActive, trackCenter);
|
||||
final double thumbRadius = enabled ? _kThumbRadiusTween.evaluate(_reaction) : _kDisabledThumbRadius;
|
||||
final double railVerticalCenter = offset.dy + (size.height) / 2.0;
|
||||
final double railLeft = offset.dx + _overlayRadius;
|
||||
final double railTop = railVerticalCenter - railRadius;
|
||||
final double railBottom = railVerticalCenter + railRadius;
|
||||
final double railRight = railLeft + railLength;
|
||||
final double railActive = railLeft + railLength * visualPosition;
|
||||
final double thumbRadius =
|
||||
_sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width / 2.0;
|
||||
final double railActiveLeft =
|
||||
math.max(0.0, railActive - thumbRadius - thumbGap * (1.0 - _enableAnimation.value));
|
||||
final double railActiveRight =
|
||||
math.min(railActive + thumbRadius + thumbGap * (1.0 - _enableAnimation.value), railRight);
|
||||
final Rect railLeftRect = new Rect.fromLTRB(railLeft, railTop, railActiveLeft, railBottom);
|
||||
final Rect railRightRect = new Rect.fromLTRB(railActiveRight, railTop, railRight, railBottom);
|
||||
|
||||
if (enabled) {
|
||||
if (visualPosition > 0.0)
|
||||
canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive, trackBottom), leftPaint);
|
||||
if (visualPosition < 1.0) {
|
||||
final bool hasBalloon = _reaction.status != AnimationStatus.dismissed && label != null;
|
||||
final double trackActiveDelta = hasBalloon ? 0.0 : thumbRadius - 1.0;
|
||||
canvas.drawRect(new Rect.fromLTRB(trackActive + trackActiveDelta, trackTop, trackRight, trackBottom), rightPaint);
|
||||
}
|
||||
} else {
|
||||
if (visualPosition > 0.0)
|
||||
canvas.drawRect(new Rect.fromLTRB(trackLeft, trackTop, trackActive - _kDisabledThumbRadius - 2, trackBottom), trackPaint);
|
||||
if (visualPosition < 1.0)
|
||||
canvas.drawRect(new Rect.fromLTRB(trackActive + _kDisabledThumbRadius + 2, trackTop, trackRight, trackBottom), trackPaint);
|
||||
final Offset thumbCenter = new Offset(railActive, railVerticalCenter);
|
||||
|
||||
// Paint the rail.
|
||||
if (visualPosition > 0.0) {
|
||||
canvas.drawRect(railLeftRect, leftRailPaint);
|
||||
}
|
||||
if (visualPosition < 1.0) {
|
||||
canvas.drawRect(railRightRect, rightRailPaint);
|
||||
}
|
||||
|
||||
if (_reaction.status != AnimationStatus.dismissed) {
|
||||
final int divisions = this.divisions;
|
||||
if (divisions != null) {
|
||||
const double tickWidth = 2.0;
|
||||
final double dx = (trackLength - tickWidth) / divisions;
|
||||
// If the ticks would be too dense, don't bother painting them.
|
||||
if (dx >= 3 * tickWidth) {
|
||||
final Paint tickPaint = new Paint()..color = _kTickColorTween.evaluate(_reaction);
|
||||
for (int i = 0; i <= divisions; i += 1) {
|
||||
final double left = trackLeft + i * dx;
|
||||
canvas.drawRect(new Rect.fromLTRB(left, trackTop, left + tickWidth, trackBottom), tickPaint);
|
||||
}
|
||||
}
|
||||
_paintOverlay(canvas, thumbCenter);
|
||||
|
||||
_paintTickMarks(
|
||||
canvas,
|
||||
railLeftRect,
|
||||
railRightRect,
|
||||
leftTickMarkPaint,
|
||||
rightTickMarkPaint,
|
||||
);
|
||||
|
||||
if (isInteractive && _reaction.status != AnimationStatus.dismissed && label != null) {
|
||||
bool showValueIndicator;
|
||||
switch (_sliderTheme.showValueIndicator) {
|
||||
case ShowValueIndicator.onlyForDiscrete:
|
||||
showValueIndicator = isDiscrete;
|
||||
break;
|
||||
case ShowValueIndicator.onlyForContinuous:
|
||||
showValueIndicator = !isDiscrete;
|
||||
break;
|
||||
case ShowValueIndicator.always:
|
||||
showValueIndicator = true;
|
||||
break;
|
||||
case ShowValueIndicator.never:
|
||||
showValueIndicator = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (label != null) {
|
||||
final Offset center = new Offset(
|
||||
trackActive,
|
||||
_kLabelBalloonCenterTween.evaluate(_reaction) * textScaleFactor + trackCenter
|
||||
if (showValueIndicator) {
|
||||
_sliderTheme.valueIndicatorShape.paint(
|
||||
context,
|
||||
isDiscrete,
|
||||
thumbCenter,
|
||||
_reaction,
|
||||
_enableAnimation,
|
||||
_labelPainter,
|
||||
_sliderTheme,
|
||||
_textDirection,
|
||||
_textScaleFactor,
|
||||
value,
|
||||
);
|
||||
final double radius = _kLabelBalloonRadiusTween.evaluate(_reaction) * textScaleFactor;
|
||||
final Offset tip = new Offset(
|
||||
trackActive,
|
||||
_kLabelBalloonTipTween.evaluate(_reaction) * textScaleFactor + trackCenter
|
||||
);
|
||||
final double tipAttachment = _kLabelBalloonTipAttachmentRatio * radius;
|
||||
|
||||
canvas.drawCircle(center, radius, primaryPaint);
|
||||
final Path path = new Path()
|
||||
..moveTo(tip.dx, tip.dy)
|
||||
..lineTo(center.dx - tipAttachment, center.dy + tipAttachment)
|
||||
..lineTo(center.dx + tipAttachment, center.dy + tipAttachment)
|
||||
..close();
|
||||
canvas.drawPath(path, primaryPaint);
|
||||
final Offset labelOffset = new Offset(
|
||||
center.dx - _labelPainter.width / 2.0,
|
||||
center.dy - _labelPainter.height / 2.0
|
||||
);
|
||||
_labelPainter.paint(canvas, labelOffset);
|
||||
return;
|
||||
} else {
|
||||
final Color reactionBaseColor = thumbAtMin ? _kActiveTrackColor : _activeColor;
|
||||
final Paint reactionPaint = new Paint()..color = reactionBaseColor.withAlpha(kRadialReactionAlpha);
|
||||
canvas.drawCircle(thumbCenter, _kReactionRadiusTween.evaluate(_reaction), reactionPaint);
|
||||
}
|
||||
}
|
||||
|
||||
Paint thumbPaint = primaryPaint;
|
||||
double thumbRadiusDelta = 0.0;
|
||||
if (thumbAtMin && thumbOpenAtMin) {
|
||||
thumbPaint = trackPaint;
|
||||
// This is destructive to trackPaint.
|
||||
thumbPaint
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0;
|
||||
thumbRadiusDelta = -1.0;
|
||||
}
|
||||
canvas.drawCircle(thumbCenter, thumbRadius + thumbRadiusDelta, thumbPaint);
|
||||
_sliderTheme.thumbShape.paint(
|
||||
context,
|
||||
isDiscrete,
|
||||
thumbCenter,
|
||||
_reaction,
|
||||
_enableAnimation,
|
||||
label != null ? _labelPainter : null,
|
||||
_sliderTheme,
|
||||
_textDirection,
|
||||
_textScaleFactor,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -718,15 +814,17 @@ class _RenderSlider extends RenderBox {
|
|||
}
|
||||
}
|
||||
|
||||
double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _kAdjustmentUnit;
|
||||
double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _adjustmentUnit;
|
||||
|
||||
void _increaseAction() {
|
||||
if (isInteractive)
|
||||
if (isInteractive) {
|
||||
onChanged((value + _semanticActionUnit).clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
void _decreaseAction() {
|
||||
if (isInteractive)
|
||||
if (isInteractive) {
|
||||
onChanged((value - _semanticActionUnit).clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
729
packages/flutter/lib/src/material/slider_theme.dart
Normal file
729
packages/flutter/lib/src/material/slider_theme.dart
Normal file
|
@ -0,0 +1,729 @@
|
|||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// 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 'dart:ui' show Path;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'theme.dart';
|
||||
import 'theme_data.dart';
|
||||
|
||||
/// Applies a slider theme to descendant [Slider] widgets.
|
||||
///
|
||||
/// A slider theme describes the colors and shape choices of the slider
|
||||
/// components.
|
||||
///
|
||||
/// Descendant widgets obtain the current theme's [SliderThemeData] object using
|
||||
/// [SliderTheme.of]. When a widget uses [SliderTheme.of], it is automatically
|
||||
/// rebuilt if the theme later changes.
|
||||
///
|
||||
/// 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.
|
||||
class SliderTheme extends InheritedWidget {
|
||||
/// Applies the given theme [data] to [child].
|
||||
///
|
||||
/// The [data] and [child] arguments must not be null.
|
||||
const SliderTheme({
|
||||
Key key,
|
||||
@required this.data,
|
||||
@required Widget child,
|
||||
}) : assert(child != null),
|
||||
assert(data != null),
|
||||
super(key: key, child: child);
|
||||
|
||||
/// Specifies the color and shape values for descendant slider widgets.
|
||||
final SliderThemeData data;
|
||||
|
||||
/// Returns the data from the closest [SliderTheme] instance that encloses
|
||||
/// the given context.
|
||||
///
|
||||
/// Defaults to the ambient [ThemeData.sliderTheme] if there is no
|
||||
/// [SliderTheme] in the given build context.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// double _rocketThrust;
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return new SliderTheme(
|
||||
/// data: SliderTheme.of(context).copyWith(activeRail: Colors.orange),
|
||||
/// child: new Slider(
|
||||
/// onChanged: (double value) => setState(() => _rocketThrust = value),
|
||||
/// value: _rocketThrust;
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliderThemeData], which describes the actual configuration of a slider
|
||||
/// theme.
|
||||
static SliderThemeData of(BuildContext context) {
|
||||
final SliderTheme inheritedTheme = context.inheritFromWidgetOfExactType(SliderTheme);
|
||||
return inheritedTheme != null ? inheritedTheme.data : Theme.of(context).sliderTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(SliderTheme old) => data != old.data;
|
||||
}
|
||||
|
||||
/// Describes the conditions under which the value indicator on a [Slider]
|
||||
/// will be shown. Used with [SliderThemeData.showValueIndicator].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Slider], a Material Design slider widget.
|
||||
/// * [SliderThemeData], which describes the actual configuration of a slider
|
||||
/// theme.
|
||||
enum ShowValueIndicator {
|
||||
/// The value indicator will only be shown for discrete sliders (sliders
|
||||
/// where [Slider.divisions] is non-null).
|
||||
onlyForDiscrete,
|
||||
|
||||
/// The value indicator will only be shown for continuous sliders (sliders
|
||||
/// where [Slider.divisions] is null).
|
||||
onlyForContinuous,
|
||||
|
||||
/// The value indicator will be shown for all types of sliders.
|
||||
always,
|
||||
|
||||
/// The value indicator will never be shown.
|
||||
never,
|
||||
}
|
||||
|
||||
/// Holds the color, shape, and typography values for a material design slider
|
||||
/// theme.
|
||||
///
|
||||
/// Use this class to configure a [SliderTheme] widget, or to set the
|
||||
/// [ThemeData.sliderTheme] for a [Theme] widget.
|
||||
///
|
||||
/// To obtain the current ambient slider theme, use [SliderTheme.of].
|
||||
///
|
||||
/// The parts of a slider are:
|
||||
///
|
||||
/// * The "thumb", which is a shape that slides horizontally when the user
|
||||
/// drags it.
|
||||
/// * The "rail", 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 "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
|
||||
/// maximum value.
|
||||
/// * 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.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliderTheme] widget, which can override the slider theme of its
|
||||
/// children.
|
||||
/// * [Theme] widget, which performs a similar function to [SliderTheme],
|
||||
/// but for overall themes.
|
||||
/// * [ThemeData], which has a default [SliderThemeData].
|
||||
/// * [SliderComponentShape], to define custom slider component shapes.
|
||||
class SliderThemeData extends Diagnosticable {
|
||||
/// Create a [SliderThemeData] given a set of exact values. All the values
|
||||
/// must be specified.
|
||||
///
|
||||
/// This will rarely be used directly. It is used by [lerp] to
|
||||
/// create intermediate themes based on two themes.
|
||||
///
|
||||
/// The simplest way to create a SliderThemeData is to use
|
||||
/// [copyWith] on the one you get from [SliderTheme.of], or create an
|
||||
/// entirely new one with [SliderThemeData.materialDefaults].
|
||||
const SliderThemeData({
|
||||
@required this.activeRailColor,
|
||||
@required this.inactiveRailColor,
|
||||
@required this.disabledActiveRailColor,
|
||||
@required this.disabledInactiveRailColor,
|
||||
@required this.activeTickMarkColor,
|
||||
@required this.inactiveTickMarkColor,
|
||||
@required this.disabledActiveTickMarkColor,
|
||||
@required this.disabledInactiveTickMarkColor,
|
||||
@required this.thumbColor,
|
||||
@required this.disabledThumbColor,
|
||||
@required this.overlayColor,
|
||||
@required this.valueIndicatorColor,
|
||||
@required this.thumbShape,
|
||||
@required this.valueIndicatorShape,
|
||||
@required this.showValueIndicator,
|
||||
}) : assert(activeRailColor != null),
|
||||
assert(inactiveRailColor != null),
|
||||
assert(disabledActiveRailColor != null),
|
||||
assert(disabledInactiveRailColor != 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(showValueIndicator != null);
|
||||
|
||||
/// Generates a SliderThemeData from three main colors.
|
||||
///
|
||||
/// Usually these are the primary, dark and light colors from
|
||||
/// a [ThemeData].
|
||||
///
|
||||
/// The opacities of these colors will be overridden with the Material Design
|
||||
/// defaults when assigning them to the slider theme component colors.
|
||||
///
|
||||
/// This is used to generate the default slider theme for a [ThemeData].
|
||||
factory SliderThemeData.materialDefaults({
|
||||
@required Color primaryColor,
|
||||
@required Color primaryColorDark,
|
||||
@required Color primaryColorLight,
|
||||
}) {
|
||||
assert(primaryColor != null);
|
||||
assert(primaryColorDark != null);
|
||||
assert(primaryColorLight != null);
|
||||
|
||||
// These are Material Design defaults, and are used to derive
|
||||
// component Colors (with opacity) from base colors.
|
||||
const int activeRailAlpha = 0xff;
|
||||
const int inactiveRailAlpha = 0x3d; // 24% opacity
|
||||
const int disabledActiveRailAlpha = 0x52; // 32% opacity
|
||||
const int disabledInactiveRailAlpha = 0x1f; // 12% opacity
|
||||
const int activeTickMarkAlpha = 0x8a; // 54% opacity
|
||||
const int inactiveTickMarkAlpha = 0x8a; // 54% opacity
|
||||
const int disabledActiveTickMarkAlpha = 0x1f; // 12% opacity
|
||||
const int disabledInactiveTickMarkAlpha = 0x1f; // 12% opacity
|
||||
const int thumbAlpha = 0xff;
|
||||
const int disabledThumbAlpha = 0x52; // 32% opacity
|
||||
const int valueIndicatorAlpha = 0xff;
|
||||
|
||||
// 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.
|
||||
const int overlayLightAlpha = 0x29; // 16% opacity
|
||||
|
||||
return new SliderThemeData(
|
||||
activeRailColor: primaryColor.withAlpha(activeRailAlpha),
|
||||
inactiveRailColor: primaryColor.withAlpha(inactiveRailAlpha),
|
||||
disabledActiveRailColor: primaryColorDark.withAlpha(disabledActiveRailAlpha),
|
||||
disabledInactiveRailColor: primaryColorDark.withAlpha(disabledInactiveRailAlpha),
|
||||
activeTickMarkColor: primaryColorLight.withAlpha(activeTickMarkAlpha),
|
||||
inactiveTickMarkColor: primaryColor.withAlpha(inactiveTickMarkAlpha),
|
||||
disabledActiveTickMarkColor: primaryColorLight.withAlpha(disabledActiveTickMarkAlpha),
|
||||
disabledInactiveTickMarkColor: primaryColorDark.withAlpha(disabledInactiveTickMarkAlpha),
|
||||
thumbColor: primaryColor.withAlpha(thumbAlpha),
|
||||
disabledThumbColor: primaryColorDark.withAlpha(disabledThumbAlpha),
|
||||
overlayColor: primaryColor.withAlpha(overlayLightAlpha),
|
||||
valueIndicatorColor: primaryColor.withAlpha(valueIndicatorAlpha),
|
||||
thumbShape: const RoundSliderThumbShape(),
|
||||
valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
|
||||
showValueIndicator: ShowValueIndicator.onlyForDiscrete,
|
||||
);
|
||||
}
|
||||
|
||||
final Color activeRailColor;
|
||||
final Color inactiveRailColor;
|
||||
final Color disabledActiveRailColor;
|
||||
final Color disabledInactiveRailColor;
|
||||
final Color activeTickMarkColor;
|
||||
final Color inactiveTickMarkColor;
|
||||
final Color disabledActiveTickMarkColor;
|
||||
final Color disabledInactiveTickMarkColor;
|
||||
final Color thumbColor;
|
||||
final Color disabledThumbColor;
|
||||
final Color overlayColor;
|
||||
final Color valueIndicatorColor;
|
||||
final SliderComponentShape thumbShape;
|
||||
final SliderComponentShape valueIndicatorShape;
|
||||
|
||||
/// Whether the value indicator should be shown for different types of sliders.
|
||||
///
|
||||
/// By default, [showValueIndicator] is set to
|
||||
/// [ShowValueIndicator.onlyForDiscrete]. The value indicator is only shown
|
||||
/// when the thumb is being touched.
|
||||
final ShowValueIndicator showValueIndicator;
|
||||
|
||||
SliderThemeData copyWith({
|
||||
Color activeRailColor,
|
||||
Color inactiveRailColor,
|
||||
Color disabledActiveRailColor,
|
||||
Color disabledInactiveRailColor,
|
||||
Color activeTickMarkColor,
|
||||
Color inactiveTickMarkColor,
|
||||
Color disabledActiveTickMarkColor,
|
||||
Color disabledInactiveTickMarkColor,
|
||||
Color thumbColor,
|
||||
Color disabledThumbColor,
|
||||
Color overlayColor,
|
||||
Color valueIndicatorColor,
|
||||
SliderComponentShape thumbShape,
|
||||
SliderComponentShape valueIndicatorShape,
|
||||
ShowValueIndicator showValueIndicator,
|
||||
}) {
|
||||
return new SliderThemeData(
|
||||
activeRailColor: activeRailColor ?? this.activeRailColor,
|
||||
inactiveRailColor: inactiveRailColor ?? this.inactiveRailColor,
|
||||
disabledActiveRailColor: disabledActiveRailColor ?? this.disabledActiveRailColor,
|
||||
disabledInactiveRailColor: disabledInactiveRailColor ?? this.disabledInactiveRailColor,
|
||||
activeTickMarkColor: activeTickMarkColor ?? this.activeTickMarkColor,
|
||||
inactiveTickMarkColor: inactiveTickMarkColor ?? this.inactiveTickMarkColor,
|
||||
disabledActiveTickMarkColor: disabledActiveTickMarkColor ?? this.disabledActiveTickMarkColor,
|
||||
disabledInactiveTickMarkColor:
|
||||
disabledInactiveTickMarkColor ?? this.disabledInactiveTickMarkColor,
|
||||
thumbColor: thumbColor ?? this.thumbColor,
|
||||
disabledThumbColor: disabledThumbColor ?? this.disabledThumbColor,
|
||||
overlayColor: overlayColor ?? this.overlayColor,
|
||||
valueIndicatorColor: valueIndicatorColor ?? this.valueIndicatorColor,
|
||||
thumbShape: thumbShape ?? this.thumbShape,
|
||||
valueIndicatorShape: valueIndicatorShape ?? this.valueIndicatorShape,
|
||||
showValueIndicator: showValueIndicator ?? this.showValueIndicator,
|
||||
);
|
||||
}
|
||||
|
||||
/// Linearly interpolate between two slider themes.
|
||||
///
|
||||
/// The arguments must not be null.
|
||||
///
|
||||
/// The `t` argument represents position on the timeline, with 0.0 meaning
|
||||
/// that the interpolation has not started, returning `a` (or something
|
||||
/// equivalent to `a`), 1.0 meaning that the interpolation has finished,
|
||||
/// returning `b` (or something equivalent to `b`), and values in between
|
||||
/// meaning that the interpolation is at the relevant point on the timeline
|
||||
/// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
|
||||
/// 1.0, so negative values and values greater than 1.0 are valid (and can
|
||||
/// easily be generated by curves such as [Curves.elasticInOut]).
|
||||
///
|
||||
/// Values for `t` are usually obtained from an [Animation<double>], such as
|
||||
/// an [AnimationController].
|
||||
static SliderThemeData lerp(SliderThemeData a, SliderThemeData b, double t) {
|
||||
assert(a != null);
|
||||
assert(b != null);
|
||||
assert(t != null);
|
||||
return new SliderThemeData(
|
||||
activeRailColor: Color.lerp(a.activeRailColor, b.activeRailColor, t),
|
||||
inactiveRailColor: Color.lerp(a.inactiveRailColor, b.inactiveRailColor, t),
|
||||
disabledActiveRailColor: Color.lerp(a.disabledActiveRailColor, b.disabledActiveRailColor, t),
|
||||
disabledInactiveRailColor:
|
||||
Color.lerp(a.disabledInactiveRailColor, b.disabledInactiveRailColor, t),
|
||||
activeTickMarkColor: Color.lerp(a.activeTickMarkColor, b.activeTickMarkColor, t),
|
||||
inactiveTickMarkColor: Color.lerp(a.inactiveTickMarkColor, b.inactiveTickMarkColor, t),
|
||||
disabledActiveTickMarkColor:
|
||||
Color.lerp(a.disabledActiveTickMarkColor, b.disabledActiveTickMarkColor, t),
|
||||
disabledInactiveTickMarkColor:
|
||||
Color.lerp(a.disabledInactiveTickMarkColor, b.disabledInactiveTickMarkColor, t),
|
||||
thumbColor: Color.lerp(a.thumbColor, b.thumbColor, t),
|
||||
disabledThumbColor: Color.lerp(a.disabledThumbColor, b.disabledThumbColor, t),
|
||||
overlayColor: Color.lerp(a.overlayColor, b.overlayColor, t),
|
||||
valueIndicatorColor: Color.lerp(a.valueIndicatorColor, b.valueIndicatorColor, t),
|
||||
thumbShape: t < 0.5 ? a.thumbShape : b.thumbShape,
|
||||
valueIndicatorShape: t < 0.5 ? a.valueIndicatorShape : b.valueIndicatorShape,
|
||||
showValueIndicator: t < 0.5 ? a.showValueIndicator : b.showValueIndicator,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return hashValues(
|
||||
activeRailColor,
|
||||
inactiveRailColor,
|
||||
disabledActiveRailColor,
|
||||
disabledInactiveRailColor,
|
||||
activeTickMarkColor,
|
||||
inactiveTickMarkColor,
|
||||
disabledActiveTickMarkColor,
|
||||
disabledInactiveTickMarkColor,
|
||||
thumbColor,
|
||||
disabledThumbColor,
|
||||
overlayColor,
|
||||
valueIndicatorColor,
|
||||
thumbShape,
|
||||
valueIndicatorShape,
|
||||
showValueIndicator,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
final SliderThemeData otherData = other;
|
||||
return otherData.activeRailColor == activeRailColor &&
|
||||
otherData.inactiveRailColor == inactiveRailColor &&
|
||||
otherData.disabledActiveRailColor == disabledActiveRailColor &&
|
||||
otherData.disabledInactiveRailColor == disabledInactiveRailColor &&
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
final ThemeData defaultTheme = new ThemeData.fallback();
|
||||
final SliderThemeData defaultData = new SliderThemeData.materialDefaults(
|
||||
primaryColor: defaultTheme.primaryColor,
|
||||
primaryColorDark: defaultTheme.primaryColorDark,
|
||||
primaryColorLight: defaultTheme.primaryColorLight,
|
||||
);
|
||||
description.add(new DiagnosticsProperty<Color>('activeRailColor', activeRailColor,
|
||||
defaultValue: defaultData.activeRailColor));
|
||||
description.add(new DiagnosticsProperty<Color>('inactiveRailColor', inactiveRailColor,
|
||||
defaultValue: defaultData.inactiveRailColor));
|
||||
description.add(new DiagnosticsProperty<Color>(
|
||||
'disabledActiveRailColor', disabledActiveRailColor,
|
||||
defaultValue: defaultData.disabledActiveRailColor));
|
||||
description.add(new DiagnosticsProperty<Color>(
|
||||
'disabledInactiveRailColor', disabledInactiveRailColor,
|
||||
defaultValue: defaultData.disabledInactiveRailColor));
|
||||
description.add(new DiagnosticsProperty<Color>('activeTickMarkColor', activeTickMarkColor,
|
||||
defaultValue: defaultData.activeTickMarkColor));
|
||||
description.add(new DiagnosticsProperty<Color>('inactiveTickMarkColor', inactiveTickMarkColor,
|
||||
defaultValue: defaultData.inactiveTickMarkColor));
|
||||
description.add(new DiagnosticsProperty<Color>(
|
||||
'disabledActiveTickMarkColor', disabledActiveTickMarkColor,
|
||||
defaultValue: defaultData.disabledActiveTickMarkColor));
|
||||
description.add(new DiagnosticsProperty<Color>(
|
||||
'disabledInactiveTickMarkColor', disabledInactiveTickMarkColor,
|
||||
defaultValue: defaultData.disabledInactiveTickMarkColor));
|
||||
description.add(new DiagnosticsProperty<Color>('thumbColor', thumbColor,
|
||||
defaultValue: defaultData.thumbColor));
|
||||
description.add(new DiagnosticsProperty<Color>('disabledThumbColor', disabledThumbColor,
|
||||
defaultValue: defaultData.disabledThumbColor));
|
||||
description.add(new DiagnosticsProperty<Color>('overlayColor', overlayColor,
|
||||
defaultValue: defaultData.overlayColor));
|
||||
description.add(new DiagnosticsProperty<Color>('valueIndicatorColor', valueIndicatorColor,
|
||||
defaultValue: defaultData.valueIndicatorColor));
|
||||
description.add(new DiagnosticsProperty<SliderComponentShape>('thumbShape', thumbShape,
|
||||
defaultValue: defaultData.thumbShape));
|
||||
description.add(new DiagnosticsProperty<SliderComponentShape>(
|
||||
'valueIndicatorShape', valueIndicatorShape,
|
||||
defaultValue: defaultData.valueIndicatorShape));
|
||||
description.add(new DiagnosticsProperty<ShowValueIndicator>(
|
||||
'showValueIndicator', showValueIndicator,
|
||||
defaultValue: defaultData.showValueIndicator));
|
||||
}
|
||||
}
|
||||
|
||||
/// Base class for slider thumb and value indicator shapes.
|
||||
///
|
||||
/// Create a subclass of this if you would like a custom slider thumb or
|
||||
/// value indicator shape.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RoundSliderThumbShape] for a simple example of a thumb shape.
|
||||
/// * [PaddleSliderValueIndicatorShape], for a complex example of a value
|
||||
/// indicator shape.
|
||||
abstract class SliderComponentShape {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
const SliderComponentShape();
|
||||
|
||||
/// Returns the preferred size of the shape, based on the given conditions.
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete);
|
||||
|
||||
/// Paints the shape, taking into account the state passed to it.
|
||||
///
|
||||
/// [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.
|
||||
/// 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(
|
||||
PaintingContext context,
|
||||
bool isDiscrete,
|
||||
Offset thumbCenter,
|
||||
Animation<double> activationAnimation,
|
||||
Animation<double> enableAnimation,
|
||||
TextPainter labelPainter,
|
||||
SliderThemeData sliderTheme,
|
||||
TextDirection textDirection,
|
||||
double textScaleFactor,
|
||||
double value,
|
||||
);
|
||||
}
|
||||
|
||||
/// This is the default shape to a [Slider]'s thumb if no
|
||||
/// other shape is specified.
|
||||
///
|
||||
/// 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.
|
||||
class RoundSliderThumbShape extends SliderComponentShape {
|
||||
const RoundSliderThumbShape();
|
||||
static const double _thumbRadius = 6.0;
|
||||
static const double _disabledThumbRadius = 4.0;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return new Size.fromRadius(isEnabled ? _thumbRadius : _disabledThumbRadius);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
bool isDiscrete,
|
||||
Offset thumbCenter,
|
||||
Animation<double> activationAnimation,
|
||||
Animation<double> enableAnimation,
|
||||
TextPainter labelPainter,
|
||||
SliderThemeData sliderTheme,
|
||||
TextDirection textDirection,
|
||||
double textScaleFactor,
|
||||
double value,
|
||||
) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final Tween<double> radiusTween =
|
||||
new Tween<double>(begin: _disabledThumbRadius, end: _thumbRadius);
|
||||
final ColorTween colorTween =
|
||||
new ColorTween(begin: sliderTheme.disabledThumbColor, end: sliderTheme.thumbColor);
|
||||
canvas.drawCircle(
|
||||
thumbCenter,
|
||||
radiusTween.evaluate(enableAnimation),
|
||||
new Paint()..color = colorTween.evaluate(enableAnimation),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the default shape to a [Slider]'s value indicator if no
|
||||
/// other shape is specified.
|
||||
///
|
||||
/// 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.
|
||||
class PaddleSliderValueIndicatorShape extends SliderComponentShape {
|
||||
const PaddleSliderValueIndicatorShape();
|
||||
|
||||
// These constants define the shape of the default value indicator.
|
||||
// The value indicator changes shape based on the size of
|
||||
// the label: The top lobe spreads horizontally, and the
|
||||
// top arc on the neck moves down to keep it merging smoothly
|
||||
// with the top lobe as it expands.
|
||||
|
||||
// Radius of the top lobe of the value indicator.
|
||||
static const double _topLobeRadius = 16.0;
|
||||
// Radius of the bottom lobe of the value indicator.
|
||||
static const double _bottomLobeRadius = 6.0;
|
||||
// The starting angle for the bottom lobe. Picked to get the desired
|
||||
// thickness for the neck.
|
||||
static const double _bottomLobeStartAngle = -1.1 * math.pi / 4.0;
|
||||
// The ending angle for the bottom lobe. Picked to get the desired
|
||||
// thickness for the neck.
|
||||
static const double _bottomLobeEndAngle = 1.1 * 5 * math.pi / 4.0;
|
||||
// The padding on either side of the label.
|
||||
static const double _labelPadding = 8.0;
|
||||
static const double _distanceBetweenTopBottomCenters = 40.0;
|
||||
static const Offset _topLobeCenter = const Offset(0.0, -_distanceBetweenTopBottomCenters);
|
||||
static const double _topNeckRadius = 14.0;
|
||||
// The length of the hypotenuse of the triangle formed by the center
|
||||
// of the left top lobe arc and the center of the top left neck arc.
|
||||
// Used to calculate the position of the center of the arc.
|
||||
static const double _neckTriangleHypotenuse = _topLobeRadius + _topNeckRadius;
|
||||
// Some convenience values to help readability.
|
||||
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 final Tween<double> _slideUpTween = new Tween<double>(begin: 0.0, end: 1.0);
|
||||
static Path _bottomLobePath; // Initialized by _generateBottomLobe
|
||||
static Offset _bottomLobeEnd; // Initialized by _generateBottomLobe
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) => preferredSize;
|
||||
|
||||
// Adds an arc to the path that has the attributes passed in. This is
|
||||
// a convenience to make adding arcs have less boilerplate.
|
||||
static void _addArc(Path path, Offset center, double radius, double startAngle, double endAngle) {
|
||||
final Rect arcRect = new Rect.fromCircle(center: center, radius: radius);
|
||||
path.arcTo(arcRect, startAngle, endAngle - startAngle, false);
|
||||
}
|
||||
|
||||
// Generates the bottom lobe path, which is the same for all instances of
|
||||
// the value indicator, so we reuse it for each one.
|
||||
static void _generateBottomLobe() {
|
||||
const double bottomNeckRadius = 4.5;
|
||||
const double bottomNeckStartAngle = _bottomLobeEndAngle - math.pi;
|
||||
const double bottomNeckEndAngle = 0.0;
|
||||
|
||||
final Path path = new Path();
|
||||
final Offset bottomKnobStart = new Offset(
|
||||
_bottomLobeRadius * math.cos(_bottomLobeStartAngle),
|
||||
_bottomLobeRadius * math.sin(_bottomLobeStartAngle),
|
||||
);
|
||||
final Offset bottomNeckRightCenter = bottomKnobStart +
|
||||
new Offset(
|
||||
bottomNeckRadius * math.cos(bottomNeckStartAngle),
|
||||
-bottomNeckRadius * math.sin(bottomNeckStartAngle),
|
||||
);
|
||||
final Offset bottomNeckLeftCenter = new Offset(
|
||||
-bottomNeckRightCenter.dx,
|
||||
bottomNeckRightCenter.dy,
|
||||
);
|
||||
final Offset bottomNeckStartRight = new Offset(
|
||||
bottomNeckRightCenter.dx - bottomNeckRadius,
|
||||
bottomNeckRightCenter.dy,
|
||||
);
|
||||
path.moveTo(bottomNeckStartRight.dx, bottomNeckStartRight.dy);
|
||||
_addArc(
|
||||
path,
|
||||
bottomNeckRightCenter,
|
||||
bottomNeckRadius,
|
||||
math.pi - bottomNeckEndAngle,
|
||||
math.pi - bottomNeckStartAngle,
|
||||
);
|
||||
_addArc(
|
||||
path,
|
||||
Offset.zero,
|
||||
_bottomLobeRadius,
|
||||
_bottomLobeStartAngle,
|
||||
_bottomLobeEndAngle,
|
||||
);
|
||||
_addArc(
|
||||
path,
|
||||
bottomNeckLeftCenter,
|
||||
bottomNeckRadius,
|
||||
bottomNeckStartAngle,
|
||||
bottomNeckEndAngle,
|
||||
);
|
||||
|
||||
_bottomLobeEnd = new Offset(
|
||||
-bottomNeckStartRight.dx,
|
||||
bottomNeckStartRight.dy,
|
||||
);
|
||||
_bottomLobePath = path;
|
||||
}
|
||||
|
||||
Offset _addBottomLobe(Path path) {
|
||||
if (_bottomLobePath == null || _bottomLobeEnd == null) {
|
||||
// Generate this lazily so as to not slow down app startup.
|
||||
_generateBottomLobe();
|
||||
}
|
||||
path.extendWithPath(_bottomLobePath, Offset.zero);
|
||||
return _bottomLobeEnd;
|
||||
}
|
||||
|
||||
void _drawValueIndicator(Canvas canvas, Offset center, Paint paint, double scale,
|
||||
TextPainter labelPainter, double textScaleFactor) {
|
||||
canvas.save();
|
||||
canvas.translate(center.dx, center.dy);
|
||||
// The entire value indicator should scale with the text scale factor,
|
||||
// to keep it large enough to encompass the label text.
|
||||
canvas.scale(scale * textScaleFactor, scale * textScaleFactor);
|
||||
final double inverseTextScale = 1.0 / textScaleFactor;
|
||||
final double labelHalfWidth = labelPainter.width / 2.0;
|
||||
|
||||
// This is the needed extra width for the label. It is only positive when
|
||||
// the label exceeds the minimum size contained by the round top lobe.
|
||||
final double halfWidthNeeded =
|
||||
math.max(0.0, inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding));
|
||||
|
||||
final Path path = new Path();
|
||||
final Offset bottomLobeEnd = _addBottomLobe(path);
|
||||
// The base of the triangle between the top lobe center and the centers of
|
||||
// the two top neck arcs.
|
||||
final double neckTriangleBase = _topNeckRadius - bottomLobeEnd.dx;
|
||||
// The parameter that describes how far along the transition from round to
|
||||
// stretched we are.
|
||||
final double t = math.max(0.0, math.min(1.0, halfWidthNeeded / neckTriangleBase));
|
||||
// The angle between the top neck arc's center and the top lobe's center
|
||||
// and vertical.
|
||||
final double theta = (1.0 - t) * _thirtyDegrees;
|
||||
// The center of the top left neck arc.
|
||||
final Offset neckLeftCenter = new Offset(
|
||||
-neckTriangleBase, _topLobeCenter.dy + math.cos(theta) * _neckTriangleHypotenuse);
|
||||
final Offset topLobeShift = new Offset(halfWidthNeeded, 0.0);
|
||||
final double neckArcAngle = _ninetyDegrees - theta;
|
||||
_addArc(
|
||||
path,
|
||||
neckLeftCenter,
|
||||
_topNeckRadius,
|
||||
0.0,
|
||||
-neckArcAngle,
|
||||
);
|
||||
_addArc(path, _topLobeCenter - topLobeShift, _topLobeRadius, _ninetyDegrees + theta,
|
||||
_twoSeventyDegrees);
|
||||
_addArc(path, _topLobeCenter + topLobeShift, _topLobeRadius, _twoSeventyDegrees,
|
||||
_twoSeventyDegrees + math.pi - theta);
|
||||
final Offset neckRightCenter = new Offset(-neckLeftCenter.dx, neckLeftCenter.dy);
|
||||
_addArc(
|
||||
path,
|
||||
neckRightCenter,
|
||||
_topNeckRadius,
|
||||
math.pi + neckArcAngle,
|
||||
math.pi,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
// Draw the label.
|
||||
canvas.save();
|
||||
canvas.translate(0.0, -_distanceBetweenTopBottomCenters);
|
||||
canvas.scale(inverseTextScale, inverseTextScale);
|
||||
labelPainter.paint(canvas, Offset.zero - new Offset(labelHalfWidth, labelPainter.height / 2.0));
|
||||
canvas.restore();
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
bool isDiscrete,
|
||||
Offset thumbCenter,
|
||||
Animation<double> activationAnimation,
|
||||
Animation<double> enableAnimation,
|
||||
TextPainter labelPainter,
|
||||
SliderThemeData sliderTheme,
|
||||
TextDirection textDirection,
|
||||
double textScaleFactor,
|
||||
double value,
|
||||
) {
|
||||
assert(labelPainter != null);
|
||||
final ColorTween colorTween =
|
||||
new ColorTween(begin: Colors.transparent, end: sliderTheme.valueIndicatorColor);
|
||||
final ColorTween enableColor = new ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor, end: colorTween.evaluate(activationAnimation));
|
||||
_drawValueIndicator(
|
||||
context.canvas,
|
||||
thumbCenter,
|
||||
new Paint()..color = enableColor.evaluate(enableAnimation),
|
||||
_slideUpTween.evaluate(activationAnimation),
|
||||
labelPainter,
|
||||
textScaleFactor,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import 'colors.dart';
|
|||
import 'ink_splash.dart';
|
||||
import 'ink_well.dart' show InteractiveInkFeatureFactory;
|
||||
import 'input_decorator.dart';
|
||||
import 'slider_theme.dart';
|
||||
import 'typography.dart';
|
||||
|
||||
/// Describes the contrast needs of a color.
|
||||
|
@ -52,8 +53,8 @@ const Color _kDarkThemeSplashColor = const Color(0x40CCCCCC);
|
|||
///
|
||||
/// To obtain the current theme, use [Theme.of].
|
||||
@immutable
|
||||
class ThemeData {
|
||||
/// Create a ThemeData given a set of preferred values.
|
||||
class ThemeData extends Diagnosticable {
|
||||
/// Create a [ThemeData] given a set of preferred values.
|
||||
///
|
||||
/// Default values will be derived for arguments that are omitted.
|
||||
///
|
||||
|
@ -78,6 +79,8 @@ class ThemeData {
|
|||
MaterialColor primarySwatch,
|
||||
Color primaryColor,
|
||||
Brightness primaryColorBrightness,
|
||||
Color primaryColorLight,
|
||||
Color primaryColorDark,
|
||||
Color accentColor,
|
||||
Brightness accentColorBrightness,
|
||||
Color canvasColor,
|
||||
|
@ -109,13 +112,16 @@ class ThemeData {
|
|||
IconThemeData iconTheme,
|
||||
IconThemeData primaryIconTheme,
|
||||
IconThemeData accentIconTheme,
|
||||
TargetPlatform platform
|
||||
SliderThemeData sliderTheme,
|
||||
TargetPlatform platform,
|
||||
}) {
|
||||
brightness ??= Brightness.light;
|
||||
final bool isDark = brightness == Brightness.dark;
|
||||
primarySwatch ??= Colors.blue;
|
||||
primaryColor ??= isDark ? Colors.grey[900] : primarySwatch[500];
|
||||
primaryColorBrightness ??= estimateBrightnessForColor(primaryColor);
|
||||
primaryColorLight ??= isDark ? Colors.grey[500] : primarySwatch[100];
|
||||
primaryColorDark ??= isDark ? Colors.black : primarySwatch[700];
|
||||
final bool primaryIsDark = primaryColorBrightness == Brightness.dark;
|
||||
accentColor ??= isDark ? Colors.tealAccent[200] : primarySwatch[500];
|
||||
accentColorBrightness ??= estimateBrightnessForColor(accentColor);
|
||||
|
@ -143,9 +149,15 @@ class ThemeData {
|
|||
hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000);
|
||||
errorColor ??= Colors.red[700];
|
||||
inputDecorationTheme ??= const InputDecorationTheme();
|
||||
iconTheme ??= isDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
|
||||
primaryIconTheme ??= primaryIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
|
||||
accentIconTheme ??= accentIsDark ? const IconThemeData(color: Colors.white) : const IconThemeData(color: Colors.black);
|
||||
iconTheme ??= isDark
|
||||
? const IconThemeData(color: Colors.white)
|
||||
: const IconThemeData(color: Colors.black);
|
||||
primaryIconTheme ??= primaryIsDark
|
||||
? const IconThemeData(color: Colors.white)
|
||||
: const IconThemeData(color: Colors.black);
|
||||
accentIconTheme ??= accentIsDark
|
||||
? const IconThemeData(color: Colors.white)
|
||||
: const IconThemeData(color: Colors.black);
|
||||
platform ??= defaultTargetPlatform;
|
||||
final Typography typography = new Typography(platform: platform);
|
||||
textTheme ??= isDark ? typography.white : typography.black;
|
||||
|
@ -156,10 +168,17 @@ class ThemeData {
|
|||
primaryTextTheme = primaryTextTheme.apply(fontFamily: fontFamily);
|
||||
accentTextTheme = accentTextTheme.apply(fontFamily: fontFamily);
|
||||
}
|
||||
sliderTheme ??= new SliderThemeData.materialDefaults(
|
||||
primaryColor: primaryColor,
|
||||
primaryColorLight: primaryColorLight,
|
||||
primaryColorDark: primaryColorDark,
|
||||
);
|
||||
return new ThemeData.raw(
|
||||
brightness: brightness,
|
||||
primaryColor: primaryColor,
|
||||
primaryColorBrightness: primaryColorBrightness,
|
||||
primaryColorLight: primaryColorLight,
|
||||
primaryColorDark: primaryColorDark,
|
||||
accentColor: accentColor,
|
||||
accentColorBrightness: accentColorBrightness,
|
||||
canvasColor: canvasColor,
|
||||
|
@ -190,11 +209,12 @@ class ThemeData {
|
|||
iconTheme: iconTheme,
|
||||
primaryIconTheme: primaryIconTheme,
|
||||
accentIconTheme: accentIconTheme,
|
||||
platform: platform
|
||||
sliderTheme: sliderTheme,
|
||||
platform: platform,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a ThemeData given a set of exact values. All the values
|
||||
/// Create a [ThemeData] given a set of exact values. All the values
|
||||
/// must be specified.
|
||||
///
|
||||
/// This will rarely be used directly. It is used by [lerp] to
|
||||
|
@ -204,6 +224,8 @@ class ThemeData {
|
|||
@required this.brightness,
|
||||
@required this.primaryColor,
|
||||
@required this.primaryColorBrightness,
|
||||
@required this.primaryColorLight,
|
||||
@required this.primaryColorDark,
|
||||
@required this.accentColor,
|
||||
@required this.accentColorBrightness,
|
||||
@required this.canvasColor,
|
||||
|
@ -234,10 +256,13 @@ class ThemeData {
|
|||
@required this.iconTheme,
|
||||
@required this.primaryIconTheme,
|
||||
@required this.accentIconTheme,
|
||||
@required this.platform
|
||||
@required this.sliderTheme,
|
||||
@required this.platform,
|
||||
}) : assert(brightness != null),
|
||||
assert(primaryColor != null),
|
||||
assert(primaryColorBrightness != null),
|
||||
assert(primaryColorLight != null),
|
||||
assert(primaryColorDark != null),
|
||||
assert(accentColor != null),
|
||||
assert(accentColorBrightness != null),
|
||||
assert(canvasColor != null),
|
||||
|
@ -267,6 +292,7 @@ class ThemeData {
|
|||
assert(iconTheme != null),
|
||||
assert(primaryIconTheme != null),
|
||||
assert(accentIconTheme != null),
|
||||
assert(sliderTheme != null),
|
||||
assert(platform != null);
|
||||
|
||||
/// A default light blue theme.
|
||||
|
@ -311,6 +337,12 @@ class ThemeData {
|
|||
/// icons placed on top of the primary color (e.g. toolbar text).
|
||||
final Brightness primaryColorBrightness;
|
||||
|
||||
/// A lighter version of the [primaryColor].
|
||||
final Color primaryColorLight;
|
||||
|
||||
/// A darker version of the [primaryColor].
|
||||
final Color primaryColorDark;
|
||||
|
||||
/// The foreground color for widgets (knobs, text, overscroll edge effect, etc).
|
||||
final Color accentColor;
|
||||
|
||||
|
@ -352,7 +384,7 @@ class ThemeData {
|
|||
///
|
||||
/// * [InkSplash.splashFactory], which defines the default splash.
|
||||
/// * [InkRipple.splashFactory], which defines a splash that spreads out
|
||||
/// more aggresively than the default.
|
||||
/// more aggressively than the default.
|
||||
final InteractiveInkFeatureFactory splashFactory;
|
||||
|
||||
/// The color used to highlight selected rows.
|
||||
|
@ -428,6 +460,11 @@ class ThemeData {
|
|||
/// An icon theme that contrasts with the accent color.
|
||||
final IconThemeData accentIconTheme;
|
||||
|
||||
/// The colors and shapes used to render [Slider].
|
||||
///
|
||||
/// This is the value returned from [SliderTheme.of].
|
||||
final SliderThemeData sliderTheme;
|
||||
|
||||
/// The platform the material widgets should adapt to target.
|
||||
///
|
||||
/// Defaults to the current platform.
|
||||
|
@ -438,6 +475,8 @@ class ThemeData {
|
|||
Brightness brightness,
|
||||
Color primaryColor,
|
||||
Brightness primaryColorBrightness,
|
||||
Color primaryColorLight,
|
||||
Color primaryColorDark,
|
||||
Color accentColor,
|
||||
Brightness accentColorBrightness,
|
||||
Color canvasColor,
|
||||
|
@ -468,12 +507,15 @@ class ThemeData {
|
|||
IconThemeData iconTheme,
|
||||
IconThemeData primaryIconTheme,
|
||||
IconThemeData accentIconTheme,
|
||||
SliderThemeData sliderTheme,
|
||||
TargetPlatform platform,
|
||||
}) {
|
||||
return new ThemeData.raw(
|
||||
brightness: brightness ?? this.brightness,
|
||||
primaryColor: primaryColor ?? this.primaryColor,
|
||||
primaryColorBrightness: primaryColorBrightness ?? this.primaryColorBrightness,
|
||||
primaryColorLight: primaryColorLight ?? this.primaryColorLight,
|
||||
primaryColorDark: primaryColorDark ?? this.primaryColorDark,
|
||||
accentColor: accentColor ?? this.accentColor,
|
||||
accentColorBrightness: accentColorBrightness ?? this.accentColorBrightness,
|
||||
canvasColor: canvasColor ?? this.canvasColor,
|
||||
|
@ -504,6 +546,7 @@ class ThemeData {
|
|||
iconTheme: iconTheme ?? this.iconTheme,
|
||||
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
|
||||
accentIconTheme: accentIconTheme ?? this.accentIconTheme,
|
||||
sliderTheme: sliderTheme ?? this.sliderTheme,
|
||||
platform: platform ?? this.platform,
|
||||
);
|
||||
}
|
||||
|
@ -515,7 +558,8 @@ class ThemeData {
|
|||
static const int _localizedThemeDataCacheSize = 5;
|
||||
|
||||
/// Caches localized themes to speed up the [localize] method.
|
||||
static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache = new _FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
|
||||
static final _FifoCache<_IdentityThemeDataCacheKey, ThemeData> _localizedThemeDataCache =
|
||||
new _FifoCache<_IdentityThemeDataCacheKey, ThemeData>(_localizedThemeDataCacheSize);
|
||||
|
||||
/// Returns a new theme built by merging the text geometry provided by the
|
||||
/// [localTextGeometry] theme with the [baseTheme].
|
||||
|
@ -567,7 +611,7 @@ class ThemeData {
|
|||
// Design spec shows for its color palette on
|
||||
// <https://material.io/guidelines/style/color.html#color-color-palette>.
|
||||
const double kThreshold = 0.15;
|
||||
if ((relativeLuminance + 0.05) * (relativeLuminance + 0.05) > kThreshold )
|
||||
if ((relativeLuminance + 0.05) * (relativeLuminance + 0.05) > kThreshold)
|
||||
return Brightness.light;
|
||||
return Brightness.dark;
|
||||
}
|
||||
|
@ -595,6 +639,8 @@ class ThemeData {
|
|||
brightness: t < 0.5 ? a.brightness : b.brightness,
|
||||
primaryColor: Color.lerp(a.primaryColor, b.primaryColor, t),
|
||||
primaryColorBrightness: t < 0.5 ? a.primaryColorBrightness : b.primaryColorBrightness,
|
||||
primaryColorLight: Color.lerp(a.primaryColorLight, b.primaryColorLight, t),
|
||||
primaryColorDark: Color.lerp(a.primaryColorDark, b.primaryColorDark, t),
|
||||
canvasColor: Color.lerp(a.canvasColor, b.canvasColor, t),
|
||||
scaffoldBackgroundColor: Color.lerp(a.scaffoldBackgroundColor, b.scaffoldBackgroundColor, t),
|
||||
bottomAppBarColor: Color.lerp(a.bottomAppBarColor, b.bottomAppBarColor, t),
|
||||
|
@ -625,6 +671,7 @@ class ThemeData {
|
|||
iconTheme: IconThemeData.lerp(a.iconTheme, b.iconTheme, t),
|
||||
primaryIconTheme: IconThemeData.lerp(a.primaryIconTheme, b.primaryIconTheme, t),
|
||||
accentIconTheme: IconThemeData.lerp(a.accentIconTheme, b.accentIconTheme, t),
|
||||
sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t),
|
||||
platform: t < 0.5 ? a.platform : b.platform,
|
||||
);
|
||||
}
|
||||
|
@ -667,53 +714,120 @@ class ThemeData {
|
|||
(otherData.iconTheme == iconTheme) &&
|
||||
(otherData.primaryIconTheme == primaryIconTheme) &&
|
||||
(otherData.accentIconTheme == accentIconTheme) &&
|
||||
(otherData.sliderTheme == sliderTheme) &&
|
||||
(otherData.platform == platform);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return hashValues(
|
||||
brightness,
|
||||
primaryColor,
|
||||
primaryColorBrightness,
|
||||
canvasColor,
|
||||
scaffoldBackgroundColor,
|
||||
bottomAppBarColor,
|
||||
cardColor,
|
||||
dividerColor,
|
||||
highlightColor,
|
||||
splashColor,
|
||||
splashFactory,
|
||||
selectedRowColor,
|
||||
unselectedWidgetColor,
|
||||
disabledColor,
|
||||
buttonColor,
|
||||
buttonTheme,
|
||||
secondaryHeaderColor,
|
||||
textSelectionColor,
|
||||
textSelectionHandleColor,
|
||||
hashValues( // Too many values.
|
||||
backgroundColor,
|
||||
accentColor,
|
||||
accentColorBrightness,
|
||||
indicatorColor,
|
||||
dialogBackgroundColor,
|
||||
hintColor,
|
||||
errorColor,
|
||||
textTheme,
|
||||
primaryTextTheme,
|
||||
accentTextTheme,
|
||||
iconTheme,
|
||||
inputDecorationTheme,
|
||||
primaryIconTheme,
|
||||
accentIconTheme,
|
||||
platform,
|
||||
)
|
||||
brightness,
|
||||
primaryColor,
|
||||
primaryColorBrightness,
|
||||
canvasColor,
|
||||
scaffoldBackgroundColor,
|
||||
bottomAppBarColor,
|
||||
cardColor,
|
||||
dividerColor,
|
||||
highlightColor,
|
||||
splashColor,
|
||||
splashFactory,
|
||||
selectedRowColor,
|
||||
unselectedWidgetColor,
|
||||
disabledColor,
|
||||
buttonColor,
|
||||
buttonTheme,
|
||||
secondaryHeaderColor,
|
||||
textSelectionColor,
|
||||
textSelectionHandleColor,
|
||||
hashValues( // Too many values.
|
||||
backgroundColor,
|
||||
accentColor,
|
||||
accentColorBrightness,
|
||||
indicatorColor,
|
||||
dialogBackgroundColor,
|
||||
hintColor,
|
||||
errorColor,
|
||||
textTheme,
|
||||
primaryTextTheme,
|
||||
accentTextTheme,
|
||||
iconTheme,
|
||||
inputDecorationTheme,
|
||||
primaryIconTheme,
|
||||
accentIconTheme,
|
||||
sliderTheme,
|
||||
platform,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType(${ platform != defaultTargetPlatform ? "$platform " : ''}$brightness $primaryColor etc...)';
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
final ThemeData defaultData = new ThemeData.fallback();
|
||||
description.add(new EnumProperty<TargetPlatform>('platform', platform,
|
||||
defaultValue: defaultTargetPlatform));
|
||||
description.add(new EnumProperty<Brightness>('brightness', brightness,
|
||||
defaultValue: defaultData.brightness));
|
||||
description.add(new DiagnosticsProperty<Color>('primaryColor', primaryColor,
|
||||
defaultValue: defaultData.primaryColor));
|
||||
description.add(new EnumProperty<Brightness>('primaryColorBrightness', primaryColorBrightness,
|
||||
defaultValue: defaultData.primaryColorBrightness));
|
||||
description.add(new DiagnosticsProperty<Color>('accentColor', accentColor,
|
||||
defaultValue: defaultData.accentColor));
|
||||
description.add(new EnumProperty<Brightness>('accentColorBrightness', accentColorBrightness,
|
||||
defaultValue: defaultData.accentColorBrightness));
|
||||
description.add(new DiagnosticsProperty<Color>('canvasColor', canvasColor,
|
||||
defaultValue: defaultData.canvasColor));
|
||||
description.add(new DiagnosticsProperty<Color>(
|
||||
'scaffoldBackgroundColor', scaffoldBackgroundColor,
|
||||
defaultValue: defaultData.scaffoldBackgroundColor));
|
||||
description.add(new DiagnosticsProperty<Color>('bottomAppBarColor', bottomAppBarColor,
|
||||
defaultValue: defaultData.bottomAppBarColor));
|
||||
description.add(new DiagnosticsProperty<Color>('cardColor', cardColor,
|
||||
defaultValue: defaultData.cardColor));
|
||||
description.add(new DiagnosticsProperty<Color>('dividerColor', dividerColor,
|
||||
defaultValue: defaultData.dividerColor));
|
||||
description.add(new DiagnosticsProperty<Color>('highlightColor', highlightColor,
|
||||
defaultValue: defaultData.highlightColor));
|
||||
description.add(new DiagnosticsProperty<Color>('splashColor', splashColor,
|
||||
defaultValue: defaultData.splashColor));
|
||||
description.add(new DiagnosticsProperty<Color>('selectedRowColor', selectedRowColor,
|
||||
defaultValue: defaultData.selectedRowColor));
|
||||
description.add(new DiagnosticsProperty<Color>('unselectedWidgetColor', unselectedWidgetColor,
|
||||
defaultValue: defaultData.unselectedWidgetColor));
|
||||
description.add(new DiagnosticsProperty<Color>('disabledColor', disabledColor,
|
||||
defaultValue: defaultData.disabledColor));
|
||||
description.add(new DiagnosticsProperty<Color>('buttonColor', buttonColor,
|
||||
defaultValue: defaultData.buttonColor));
|
||||
description.add(new DiagnosticsProperty<Color>('secondaryHeaderColor', secondaryHeaderColor,
|
||||
defaultValue: defaultData.secondaryHeaderColor));
|
||||
description.add(new DiagnosticsProperty<Color>('textSelectionColor', textSelectionColor,
|
||||
defaultValue: defaultData.textSelectionColor));
|
||||
description.add(new DiagnosticsProperty<Color>(
|
||||
'textSelectionHandleColor', textSelectionHandleColor,
|
||||
defaultValue: defaultData.textSelectionHandleColor));
|
||||
description.add(new DiagnosticsProperty<Color>('backgroundColor', backgroundColor,
|
||||
defaultValue: defaultData.backgroundColor));
|
||||
description.add(new DiagnosticsProperty<Color>('dialogBackgroundColor', dialogBackgroundColor,
|
||||
defaultValue: defaultData.dialogBackgroundColor));
|
||||
description.add(new DiagnosticsProperty<Color>('indicatorColor', indicatorColor,
|
||||
defaultValue: defaultData.indicatorColor));
|
||||
description.add(new DiagnosticsProperty<Color>('hintColor', hintColor,
|
||||
defaultValue: defaultData.hintColor));
|
||||
description.add(new DiagnosticsProperty<Color>('errorColor', errorColor,
|
||||
defaultValue: defaultData.errorColor));
|
||||
description.add(new DiagnosticsProperty<ButtonThemeData>('buttonTheme', buttonTheme));
|
||||
description.add(new DiagnosticsProperty<TextTheme>('textTheme', textTheme));
|
||||
description.add(new DiagnosticsProperty<TextTheme>('primaryTextTheme', primaryTextTheme));
|
||||
description.add(new DiagnosticsProperty<TextTheme>('accentTextTheme', accentTextTheme));
|
||||
description.add(new DiagnosticsProperty<InputDecorationTheme>(
|
||||
'inputDecorationTheme', inputDecorationTheme));
|
||||
description.add(new DiagnosticsProperty<IconThemeData>('iconTheme', iconTheme));
|
||||
description.add(new DiagnosticsProperty<IconThemeData>('primaryIconTheme', primaryIconTheme));
|
||||
description.add(new DiagnosticsProperty<IconThemeData>('accentIconTheme', accentIconTheme));
|
||||
description.add(new DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme));
|
||||
}
|
||||
}
|
||||
|
||||
class _IdentityThemeDataCacheKey {
|
||||
|
@ -732,7 +846,8 @@ class _IdentityThemeDataCacheKey {
|
|||
// We are explicitly ignoring the possibility that the types might not
|
||||
// match in the interests of speed.
|
||||
final _IdentityThemeDataCacheKey otherKey = other;
|
||||
return identical(baseTheme, otherKey.baseTheme) && identical(localTextGeometry, otherKey.localTextGeometry);
|
||||
return identical(baseTheme, otherKey.baseTheme) &&
|
||||
identical(localTextGeometry, otherKey.localTextGeometry);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -742,8 +857,7 @@ class _IdentityThemeDataCacheKey {
|
|||
/// The key that was inserted before all other keys is evicted first, i.e. the
|
||||
/// one inserted least recently.
|
||||
class _FifoCache<K, V> {
|
||||
_FifoCache(this._maximumSize)
|
||||
: assert(_maximumSize != null && _maximumSize > 0);
|
||||
_FifoCache(this._maximumSize) : assert(_maximumSize != null && _maximumSize > 0);
|
||||
|
||||
/// In Dart the map literal uses a linked hash-map implementation, whose keys
|
||||
/// are stored such that [Map.keys] returns them in the order they were
|
||||
|
|
|
@ -33,7 +33,7 @@ import 'colors.dart';
|
|||
/// globally adjusted, such as the color scheme.
|
||||
/// * <http://material.google.com/style/typography.html>
|
||||
@immutable
|
||||
class TextTheme {
|
||||
class TextTheme extends Diagnosticable {
|
||||
/// Creates a text theme that uses the given values.
|
||||
///
|
||||
/// Rather than creating a new text theme, consider using [Typography.black]
|
||||
|
@ -114,7 +114,7 @@ class TextTheme {
|
|||
TextStyle body2,
|
||||
TextStyle body1,
|
||||
TextStyle caption,
|
||||
TextStyle button
|
||||
TextStyle button,
|
||||
}) {
|
||||
return new TextTheme(
|
||||
display4: display4 ?? this.display4,
|
||||
|
@ -353,6 +353,34 @@ class TextTheme {
|
|||
button,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
final TextTheme defaultTheme = new Typography(platform: defaultTargetPlatform).black;
|
||||
description.add(new DiagnosticsProperty<TextStyle>('display4', display4,
|
||||
defaultValue: defaultTheme.display4));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('display3', display3,
|
||||
defaultValue: defaultTheme.display3));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('display2', display2,
|
||||
defaultValue: defaultTheme.display2));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('display1', display1,
|
||||
defaultValue: defaultTheme.display1));
|
||||
description.add(new DiagnosticsProperty<TextStyle>('headline', headline,
|
||||
defaultValue: defaultTheme.headline));
|
||||
description
|
||||
.add(new DiagnosticsProperty<TextStyle>('title', title, defaultValue: defaultTheme.title));
|
||||
description.add(
|
||||
new DiagnosticsProperty<TextStyle>('subhead', subhead, defaultValue: defaultTheme.subhead));
|
||||
description
|
||||
.add(new DiagnosticsProperty<TextStyle>('body2', body2, defaultValue: defaultTheme.body2));
|
||||
description
|
||||
.add(new DiagnosticsProperty<TextStyle>('body1', body1, defaultValue: defaultTheme.body1));
|
||||
description.add(
|
||||
new DiagnosticsProperty<TextStyle>('caption', caption, defaultValue: defaultTheme.caption));
|
||||
description.add(
|
||||
new DiagnosticsProperty<TextStyle>('button', button, defaultValue: defaultTheme.button));
|
||||
}
|
||||
}
|
||||
|
||||
/// The two material design text themes.
|
||||
|
@ -373,7 +401,7 @@ class TextTheme {
|
|||
/// * <http://material.google.com/style/typography.html>
|
||||
class Typography {
|
||||
/// Creates the default typography for the specified platform.
|
||||
factory Typography({ @required TargetPlatform platform }) {
|
||||
factory Typography({@required TargetPlatform platform}) {
|
||||
assert(platform != null);
|
||||
switch (platform) {
|
||||
case TargetPlatform.android:
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
import 'dart:ui' show Color, hashValues;
|
||||
import 'dart:ui' as ui show lerpDouble;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Defines the color, opacity, and size of icons.
|
||||
///
|
||||
/// Used by [IconTheme] to control the color, opacity, and size of icons in a
|
||||
|
@ -13,29 +15,26 @@ import 'dart:ui' as ui show lerpDouble;
|
|||
/// To obtain the current icon theme, use [IconTheme.of]. To convert an icon
|
||||
/// theme to a version with all the fields filled in, use [new
|
||||
/// IconThemeData.fallback].
|
||||
class IconThemeData {
|
||||
class IconThemeData extends Diagnosticable {
|
||||
/// Creates an icon theme data.
|
||||
///
|
||||
/// The opacity applies to both explicit and default icon colors. The value
|
||||
/// is clamped between 0.0 and 1.0.
|
||||
const IconThemeData({ this.color, double opacity, this.size }) : _opacity = opacity;
|
||||
const IconThemeData({this.color, double opacity, this.size}) : _opacity = opacity;
|
||||
|
||||
/// Creates an icon them with some reasonable default values.
|
||||
///
|
||||
/// The [color] is black, the [opacity] is 1.0, and the [size] is 24.0.
|
||||
const IconThemeData.fallback()
|
||||
: color = const Color(0xFF000000),
|
||||
_opacity = 1.0,
|
||||
size = 24.0;
|
||||
: color = const Color(0xFF000000),
|
||||
_opacity = 1.0,
|
||||
size = 24.0;
|
||||
|
||||
/// Creates a copy of this icon theme but with the given fields replaced with
|
||||
/// the new values.
|
||||
IconThemeData copyWith({ Color color, double opacity, double size }) {
|
||||
IconThemeData copyWith({Color color, double opacity, double size}) {
|
||||
return new IconThemeData(
|
||||
color: color ?? this.color,
|
||||
opacity: opacity ?? this.opacity,
|
||||
size: size ?? this.size
|
||||
);
|
||||
color: color ?? this.color, opacity: opacity ?? this.opacity, size: size ?? this.size);
|
||||
}
|
||||
|
||||
/// Returns a new icon theme that matches this icon theme but with some values
|
||||
|
@ -44,11 +43,7 @@ class IconThemeData {
|
|||
IconThemeData merge(IconThemeData other) {
|
||||
if (other == null)
|
||||
return this;
|
||||
return copyWith(
|
||||
color: other.color,
|
||||
opacity: other.opacity,
|
||||
size: other.size
|
||||
);
|
||||
return copyWith(color: other.color, opacity: other.opacity, size: other.size);
|
||||
}
|
||||
|
||||
/// Whether all the properties of this object are non-null.
|
||||
|
@ -100,16 +95,13 @@ class IconThemeData {
|
|||
int get hashCode => hashValues(color, opacity, size);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final List<String> result = <String>[];
|
||||
if (color != null)
|
||||
result.add('color: $color');
|
||||
if (_opacity != null)
|
||||
result.add('opacity: $_opacity');
|
||||
if (size != null)
|
||||
result.add('size: $size');
|
||||
if (result.isEmpty)
|
||||
return '<no theme>';
|
||||
return result.join(', ');
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
if (color == null && _opacity == null && size == null) {
|
||||
return;
|
||||
}
|
||||
description.add(new DiagnosticsProperty<Color>('color', color));
|
||||
description.add(new DoubleProperty('opacity', _opacity));
|
||||
description.add(new DoubleProperty('size', size));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,8 +150,7 @@ void main() {
|
|||
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
|
||||
});
|
||||
|
||||
testWidgets('Slider can be given zero values',
|
||||
(WidgetTester tester) async {
|
||||
testWidgets('Slider can be given zero values', (WidgetTester tester) async {
|
||||
final List<double> log = <double>[];
|
||||
await tester.pumpWidget(new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
|
@ -160,7 +159,9 @@ void main() {
|
|||
value: 0.0,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
onChanged: (double newValue) { log.add(newValue); },
|
||||
onChanged: (double newValue) {
|
||||
log.add(newValue);
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
|
@ -176,7 +177,9 @@ void main() {
|
|||
value: 0.0,
|
||||
min: 0.0,
|
||||
max: 0.0,
|
||||
onChanged: (double newValue) { log.add(newValue); },
|
||||
onChanged: (double newValue) {
|
||||
log.add(newValue);
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
|
@ -186,11 +189,27 @@ void main() {
|
|||
log.clear();
|
||||
});
|
||||
|
||||
testWidgets('Slider has a customizable active color',
|
||||
testWidgets('Slider uses the right theme colors for the right components',
|
||||
(WidgetTester tester) async {
|
||||
const Color customColor = const Color(0xFF4CD964);
|
||||
final ThemeData theme = new ThemeData(platform: TargetPlatform.android);
|
||||
Widget buildApp(Color activeColor) {
|
||||
const Color customColor1 = const Color(0xcafefeed);
|
||||
const Color customColor2 = const Color(0xdeadbeef);
|
||||
final ThemeData theme = new ThemeData(
|
||||
platform: TargetPlatform.android,
|
||||
primarySwatch: Colors.blue,
|
||||
);
|
||||
final SliderThemeData sliderTheme = theme.sliderTheme;
|
||||
double value = 0.45;
|
||||
Widget buildApp({
|
||||
Color activeColor,
|
||||
Color inactiveColor,
|
||||
int divisions,
|
||||
bool enabled: true,
|
||||
}) {
|
||||
final ValueChanged<double> onChanged = !enabled
|
||||
? null
|
||||
: (double d) {
|
||||
value = d;
|
||||
};
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new Material(
|
||||
|
@ -198,47 +217,12 @@ void main() {
|
|||
child: new Theme(
|
||||
data: theme,
|
||||
child: new Slider(
|
||||
value: 0.5,
|
||||
value: value,
|
||||
label: '$value',
|
||||
divisions: divisions,
|
||||
activeColor: activeColor,
|
||||
onChanged: (double newValue) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp(null));
|
||||
|
||||
final RenderBox sliderBox =
|
||||
tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
|
||||
expect(sliderBox, paints..rect(color: theme.accentColor)..rect(color: theme.unselectedWidgetColor));
|
||||
expect(sliderBox, paints..circle(color: theme.accentColor));
|
||||
expect(sliderBox, isNot(paints..circle(color: customColor)));
|
||||
expect(sliderBox, isNot(paints..circle(color: theme.unselectedWidgetColor)));
|
||||
await tester.pumpWidget(buildApp(customColor));
|
||||
expect(sliderBox, paints..rect(color: customColor)..rect(color: theme.unselectedWidgetColor));
|
||||
expect(sliderBox, paints..circle(color: customColor));
|
||||
expect(sliderBox, isNot(paints..circle(color: theme.accentColor)));
|
||||
expect(sliderBox, isNot(paints..circle(color: theme.unselectedWidgetColor)));
|
||||
});
|
||||
|
||||
testWidgets('Slider has a customizable inactive color',
|
||||
(WidgetTester tester) async {
|
||||
const Color customColor = const Color(0xFF4CD964);
|
||||
final ThemeData theme = new ThemeData(platform: TargetPlatform.android);
|
||||
Widget buildApp(Color inactiveColor) {
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new Material(
|
||||
child: new Center(
|
||||
child: new Theme(
|
||||
data: theme,
|
||||
child: new Slider(
|
||||
value: 0.5,
|
||||
inactiveColor: inactiveColor,
|
||||
onChanged: (double newValue) {},
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -246,78 +230,168 @@ void main() {
|
|||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp(null));
|
||||
await tester.pumpWidget(buildApp());
|
||||
|
||||
final RenderBox sliderBox =
|
||||
tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
|
||||
expect(sliderBox, paints..rect(color: theme.accentColor)..rect(color: theme.unselectedWidgetColor));
|
||||
expect(sliderBox, paints..circle(color: theme.accentColor));
|
||||
await tester.pumpWidget(buildApp(customColor));
|
||||
expect(sliderBox, paints..rect(color: theme.accentColor)..rect(color: customColor));
|
||||
expect(sliderBox, paints..circle(color: theme.accentColor));
|
||||
// Check default theme for enabled widget.
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..rect(color: sliderTheme.activeRailColor)
|
||||
..rect(color: sliderTheme.inactiveRailColor));
|
||||
expect(sliderBox, paints..circle(color: sliderTheme.thumbColor));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor)));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
|
||||
|
||||
// Test setting only the activeColor.
|
||||
await tester.pumpWidget(buildApp(activeColor: customColor1));
|
||||
expect(
|
||||
sliderBox, paints..rect(color: customColor1)..rect(color: sliderTheme.inactiveRailColor));
|
||||
expect(sliderBox, paints..circle(color: customColor1));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
|
||||
|
||||
// Test setting only the inactiveColor.
|
||||
await tester.pumpWidget(buildApp(inactiveColor: customColor1));
|
||||
expect(sliderBox, paints..rect(color: sliderTheme.activeRailColor)..rect(color: customColor1));
|
||||
expect(sliderBox, paints..circle(color: sliderTheme.thumbColor));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
|
||||
|
||||
// Test setting both activeColor and inactiveColor.
|
||||
await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2));
|
||||
expect(sliderBox, paints..rect(color: customColor1)..rect(color: customColor2));
|
||||
expect(sliderBox, paints..circle(color: customColor1));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
|
||||
|
||||
// Test colors for discrete slider.
|
||||
await tester.pumpWidget(buildApp(divisions: 3));
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..rect(color: sliderTheme.activeRailColor)
|
||||
..rect(color: sliderTheme.inactiveRailColor));
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..circle(color: sliderTheme.activeTickMarkColor)
|
||||
..circle(color: sliderTheme.activeTickMarkColor)
|
||||
..circle(color: sliderTheme.inactiveTickMarkColor)
|
||||
..circle(color: sliderTheme.inactiveTickMarkColor)
|
||||
..circle(color: sliderTheme.thumbColor));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
|
||||
|
||||
// Test colors for discrete slider with inactiveColor and activeColor set.
|
||||
await tester
|
||||
.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2, divisions: 3));
|
||||
expect(sliderBox, paints..rect(color: customColor1)..rect(color: customColor2));
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..circle(color: customColor2)
|
||||
..circle(color: customColor2)
|
||||
..circle(color: customColor1)
|
||||
..circle(color: customColor1)
|
||||
..circle(color: customColor1));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveRailColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor)));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
|
||||
|
||||
// Test default theme for disabled widget.
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pump(const Duration(seconds: 1)); // wait for disable animation to finish.
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..rect(color: sliderTheme.disabledActiveRailColor)
|
||||
..rect(color: sliderTheme.disabledInactiveRailColor));
|
||||
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeRailColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveRailColor)));
|
||||
|
||||
// Test setting the activeColor and inactiveColor for disabled widget.
|
||||
await tester.pumpWidget(
|
||||
buildApp(activeColor: customColor1, inactiveColor: customColor2, enabled: false));
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..rect(color: sliderTheme.disabledActiveRailColor)
|
||||
..rect(color: sliderTheme.disabledInactiveRailColor));
|
||||
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor));
|
||||
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeRailColor)));
|
||||
expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveRailColor)));
|
||||
|
||||
// Test that the default value indicator has the right colors.
|
||||
await tester.pumpWidget(buildApp(divisions: 3));
|
||||
Offset center = tester.getCenter(find.byType(Slider));
|
||||
TestGesture gesture = await tester.startGesture(center);
|
||||
await tester.pump();
|
||||
await tester
|
||||
.pump(const Duration(milliseconds: 500)); // wait for value indicator animation to finish.
|
||||
expect(value, equals(2.0 / 3.0));
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..rect(color: sliderTheme.activeRailColor)
|
||||
..rect(color: sliderTheme.inactiveRailColor)
|
||||
..circle(color: sliderTheme.overlayColor)
|
||||
..circle(color: sliderTheme.activeTickMarkColor)
|
||||
..circle(color: sliderTheme.activeTickMarkColor)
|
||||
..circle(color: sliderTheme.inactiveTickMarkColor)
|
||||
..circle(color: sliderTheme.inactiveTickMarkColor)
|
||||
..path(color: sliderTheme.valueIndicatorColor)
|
||||
..circle(color: sliderTheme.thumbColor),
|
||||
);
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
await tester
|
||||
.pump(const Duration(milliseconds: 500)); // wait for value indicator animation to finish.
|
||||
|
||||
// Testing the custom colors are used for the indicator.
|
||||
await tester.pumpWidget(buildApp(
|
||||
divisions: 3,
|
||||
activeColor: customColor1,
|
||||
inactiveColor: customColor2,
|
||||
));
|
||||
center = tester.getCenter(find.byType(Slider));
|
||||
gesture = await tester.startGesture(center);
|
||||
await tester.pump();
|
||||
await tester
|
||||
.pump(const Duration(milliseconds: 500)); // wait for value indicator animation to finish.
|
||||
expect(value, equals(2.0 / 3.0));
|
||||
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),
|
||||
);
|
||||
await gesture.up();
|
||||
});
|
||||
|
||||
testWidgets('Slider can draw an open thumb at min (LTR)',
|
||||
(WidgetTester tester) async {
|
||||
Widget buildApp(bool thumbOpenAtMin) {
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new Material(
|
||||
child: new Center(
|
||||
child: new Slider(
|
||||
value: 0.0,
|
||||
thumbOpenAtMin: thumbOpenAtMin,
|
||||
onChanged: (double newValue) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp(false));
|
||||
|
||||
final RenderBox sliderBox =
|
||||
tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
|
||||
expect(sliderBox, paints..circle(style: PaintingStyle.fill));
|
||||
expect(sliderBox, isNot(paints..circle()..circle()));
|
||||
await tester.pumpWidget(buildApp(true));
|
||||
expect(sliderBox, paints..circle(style: PaintingStyle.stroke));
|
||||
expect(sliderBox, isNot(paints..circle()..circle()));
|
||||
});
|
||||
|
||||
testWidgets('Slider can draw an open thumb at min (RTL)',
|
||||
(WidgetTester tester) async {
|
||||
Widget buildApp(bool thumbOpenAtMin) {
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: new Material(
|
||||
child: new Center(
|
||||
child: new Slider(
|
||||
value: 0.0,
|
||||
thumbOpenAtMin: thumbOpenAtMin,
|
||||
onChanged: (double newValue) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp(false));
|
||||
|
||||
final RenderBox sliderBox =
|
||||
tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
|
||||
expect(sliderBox, paints..circle(style: PaintingStyle.fill));
|
||||
expect(sliderBox, isNot(paints..circle()..circle()));
|
||||
await tester.pumpWidget(buildApp(true));
|
||||
expect(sliderBox, paints..circle(style: PaintingStyle.stroke));
|
||||
expect(sliderBox, isNot(paints..circle()..circle()));
|
||||
});
|
||||
|
||||
testWidgets('Slider can tap in vertical scroller',
|
||||
(WidgetTester tester) async {
|
||||
testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async {
|
||||
double value = 0.0;
|
||||
await tester.pumpWidget(new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
|
@ -425,7 +499,8 @@ void main() {
|
|||
),
|
||||
),
|
||||
));
|
||||
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 600.0));
|
||||
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size,
|
||||
const Size(144.0 + 2.0 * 16.0, 600.0));
|
||||
|
||||
await tester.pumpWidget(const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
|
@ -442,14 +517,18 @@ void main() {
|
|||
),
|
||||
),
|
||||
));
|
||||
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 16.0, 32.0));
|
||||
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size,
|
||||
const Size(144.0 + 2.0 * 16.0, 32.0));
|
||||
});
|
||||
|
||||
testWidgets('discrete Slider respects textScaleFactor', (WidgetTester tester) async {
|
||||
testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async {
|
||||
final Key sliderKey = new UniqueKey();
|
||||
double value = 0.0;
|
||||
|
||||
Widget buildSlider({ double textScaleFactor }) {
|
||||
Widget buildSlider(
|
||||
{double textScaleFactor,
|
||||
bool isDiscrete: true,
|
||||
ShowValueIndicator show: ShowValueIndicator.onlyForDiscrete}) {
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new StatefulBuilder(
|
||||
|
@ -457,22 +536,27 @@ void main() {
|
|||
return new MediaQuery(
|
||||
data: new MediaQueryData(textScaleFactor: textScaleFactor),
|
||||
child: new Material(
|
||||
child: new Center(
|
||||
child: new OverflowBox(
|
||||
maxWidth: double.INFINITY,
|
||||
maxHeight: double.INFINITY,
|
||||
child: new Slider(
|
||||
key: sliderKey,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
divisions: 10,
|
||||
label: '${value.round()}',
|
||||
value: value,
|
||||
onChanged: (double newValue) {
|
||||
setState(() {
|
||||
value = newValue;
|
||||
});
|
||||
},
|
||||
child: new Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
sliderTheme:
|
||||
Theme.of(context).sliderTheme.copyWith(showValueIndicator: show)),
|
||||
child: new Center(
|
||||
child: new OverflowBox(
|
||||
maxWidth: double.INFINITY,
|
||||
maxHeight: double.INFINITY,
|
||||
child: new Slider(
|
||||
key: sliderKey,
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
divisions: isDiscrete ? 10 : null,
|
||||
label: '${value.round()}',
|
||||
value: value,
|
||||
onChanged: (double newValue) {
|
||||
setState(() {
|
||||
value = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -486,12 +570,10 @@ void main() {
|
|||
await tester.pumpWidget(buildSlider(textScaleFactor: 1.0));
|
||||
Offset center = tester.getCenter(find.byType(Slider));
|
||||
TestGesture gesture = await tester.startGesture(center);
|
||||
await gesture.moveBy(const Offset(10.0, 0.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(
|
||||
tester.renderObject(find.byType(Slider)),
|
||||
paints..circle(radius: 6.0, x: 16.0, y: 44.0)
|
||||
);
|
||||
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
@ -499,12 +581,41 @@ void main() {
|
|||
await tester.pumpWidget(buildSlider(textScaleFactor: 2.0));
|
||||
center = tester.getCenter(find.byType(Slider));
|
||||
gesture = await tester.startGesture(center);
|
||||
await gesture.moveBy(const Offset(10.0, 0.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(
|
||||
tester.renderObject(find.byType(Slider)),
|
||||
paints..circle(radius: 12.0, x: 16.0, y: 44.0)
|
||||
);
|
||||
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// Check continuous
|
||||
await tester.pumpWidget(buildSlider(
|
||||
textScaleFactor: 1.0,
|
||||
isDiscrete: false,
|
||||
show: ShowValueIndicator.onlyForContinuous,
|
||||
));
|
||||
center = tester.getCenter(find.byType(Slider));
|
||||
gesture = await tester.startGesture(center);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
await tester.pumpWidget(buildSlider(
|
||||
textScaleFactor: 2.0,
|
||||
isDiscrete: false,
|
||||
show: ShowValueIndicator.onlyForContinuous,
|
||||
));
|
||||
center = tester.getCenter(find.byType(Slider));
|
||||
gesture = await tester.startGesture(center);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
@ -523,18 +634,18 @@ void main() {
|
|||
),
|
||||
));
|
||||
|
||||
expect(semantics, hasSemantics(
|
||||
new TestSemantics.root(
|
||||
children: <TestSemantics>[
|
||||
expect(
|
||||
semantics,
|
||||
hasSemantics(
|
||||
new TestSemantics.root(children: <TestSemantics>[
|
||||
new TestSemantics.rootChild(
|
||||
id: 1,
|
||||
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
|
||||
),
|
||||
]
|
||||
),
|
||||
ignoreRect: true,
|
||||
ignoreTransform: true,
|
||||
));
|
||||
]),
|
||||
ignoreRect: true,
|
||||
ignoreTransform: true,
|
||||
));
|
||||
|
||||
// Disable slider
|
||||
await tester.pumpWidget(const Directionality(
|
||||
|
@ -547,12 +658,92 @@ void main() {
|
|||
),
|
||||
));
|
||||
|
||||
expect(semantics, hasSemantics(
|
||||
new TestSemantics.root(),
|
||||
ignoreRect: true,
|
||||
ignoreTransform: true,
|
||||
));
|
||||
expect(
|
||||
semantics,
|
||||
hasSemantics(
|
||||
new TestSemantics.root(),
|
||||
ignoreRect: true,
|
||||
ignoreTransform: true,
|
||||
));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Value indicator appears when it should', (WidgetTester tester) async {
|
||||
final ThemeData baseTheme = new ThemeData(
|
||||
platform: TargetPlatform.android,
|
||||
primarySwatch: Colors.blue,
|
||||
);
|
||||
SliderThemeData theme = baseTheme.sliderTheme;
|
||||
double value = 0.45;
|
||||
Widget buildApp({SliderThemeData sliderTheme, int divisions, bool enabled: true}) {
|
||||
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new Material(
|
||||
child: new Center(
|
||||
child: new Theme(
|
||||
data: baseTheme,
|
||||
child: new SliderTheme(
|
||||
data: sliderTheme,
|
||||
child: new Slider(
|
||||
value: value,
|
||||
label: '$value',
|
||||
divisions: divisions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Null> expectValueIndicator(
|
||||
{bool isVisible, SliderThemeData theme, int divisions, bool enabled: true}) async {
|
||||
// discrete enabled widget.
|
||||
await tester.pumpWidget(buildApp(sliderTheme: theme, divisions: divisions, enabled: enabled));
|
||||
final Offset center = tester.getCenter(find.byType(Slider));
|
||||
final TestGesture gesture = await tester.startGesture(center);
|
||||
await tester.pump();
|
||||
await tester
|
||||
.pump(const Duration(milliseconds: 500)); // wait for value indicator animation to finish.
|
||||
|
||||
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
expect(
|
||||
sliderBox,
|
||||
isVisible
|
||||
? (paints..path(color: theme.valueIndicatorColor))
|
||||
: isNot(paints..path(color: theme.valueIndicatorColor)),
|
||||
);
|
||||
await gesture.up();
|
||||
}
|
||||
|
||||
// Default (showValueIndicator set to onlyForDiscrete).
|
||||
await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, enabled: true);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, enabled: true);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
|
||||
|
||||
// With showValueIndicator set to onlyForContinuous.
|
||||
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.onlyForContinuous);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: true);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
|
||||
await expectValueIndicator(isVisible: true, theme: theme, enabled: true);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
|
||||
|
||||
// discrete enabled widget with showValueIndicator set to always.
|
||||
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.always);
|
||||
await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, enabled: true);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
|
||||
await expectValueIndicator(isVisible: true, theme: theme, enabled: true);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
|
||||
|
||||
// discrete enabled widget with showValueIndicator set to never.
|
||||
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.never);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: true);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, enabled: true);
|
||||
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
|
||||
});
|
||||
}
|
||||
|
|
294
packages/flutter/test/material/slider_theme_test.dart
Normal file
294
packages/flutter/test/material/slider_theme_test.dart
Normal file
|
@ -0,0 +1,294 @@
|
|||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Slider theme is built by ThemeData', (WidgetTester tester) async {
|
||||
final ThemeData theme = new ThemeData(
|
||||
platform: TargetPlatform.android,
|
||||
primarySwatch: Colors.red,
|
||||
);
|
||||
final SliderThemeData sliderTheme = theme.sliderTheme;
|
||||
|
||||
expect(sliderTheme.activeRailColor.value, equals(Colors.red.value));
|
||||
expect(sliderTheme.inactiveRailColor.value, equals(Colors.red.withAlpha(0x3d).value));
|
||||
});
|
||||
|
||||
testWidgets('Slider uses ThemeData slider theme if present', (WidgetTester tester) async {
|
||||
final ThemeData theme = new ThemeData(
|
||||
platform: TargetPlatform.android,
|
||||
primarySwatch: Colors.red,
|
||||
);
|
||||
final SliderThemeData sliderTheme = theme.sliderTheme;
|
||||
|
||||
Widget buildSlider(SliderThemeData data) {
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new Material(
|
||||
child: new Center(
|
||||
child: new Theme(
|
||||
data: theme,
|
||||
child: const Slider(
|
||||
value: 0.5,
|
||||
label: '0.5',
|
||||
onChanged: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildSlider(sliderTheme));
|
||||
|
||||
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..rect(color: sliderTheme.disabledActiveRailColor)
|
||||
..rect(color: sliderTheme.disabledInactiveRailColor));
|
||||
});
|
||||
|
||||
testWidgets('Slider overrides ThemeData theme if SliderTheme present',
|
||||
(WidgetTester tester) async {
|
||||
final ThemeData theme = new ThemeData(
|
||||
platform: TargetPlatform.android,
|
||||
primarySwatch: Colors.red,
|
||||
);
|
||||
final SliderThemeData sliderTheme = theme.sliderTheme;
|
||||
final SliderThemeData customTheme = sliderTheme.copyWith(
|
||||
activeRailColor: Colors.purple,
|
||||
inactiveRailColor: Colors.purple.withAlpha(0x3d),
|
||||
);
|
||||
|
||||
Widget buildSlider(SliderThemeData data) {
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new Material(
|
||||
child: new Center(
|
||||
child: new Theme(
|
||||
data: theme,
|
||||
child: new SliderTheme(
|
||||
data: customTheme,
|
||||
child: const Slider(
|
||||
value: 0.5,
|
||||
label: '0.5',
|
||||
onChanged: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildSlider(sliderTheme));
|
||||
|
||||
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..rect(color: customTheme.disabledActiveRailColor)
|
||||
..rect(color: customTheme.disabledInactiveRailColor));
|
||||
});
|
||||
|
||||
testWidgets('SliderThemeData generates correct opacities for materialDefaults',
|
||||
(WidgetTester tester) async {
|
||||
const Color customColor1 = const Color(0xcafefeed);
|
||||
const Color customColor2 = const Color(0xdeadbeef);
|
||||
const Color customColor3 = const Color(0xdecaface);
|
||||
|
||||
final SliderThemeData sliderTheme = new SliderThemeData.materialDefaults(
|
||||
primaryColor: customColor1,
|
||||
primaryColorDark: customColor2,
|
||||
primaryColorLight: customColor3,
|
||||
);
|
||||
|
||||
expect(sliderTheme.activeRailColor, equals(customColor1.withAlpha(0xff)));
|
||||
expect(sliderTheme.inactiveRailColor, equals(customColor1.withAlpha(0x3d)));
|
||||
expect(sliderTheme.disabledActiveRailColor, equals(customColor2.withAlpha(0x52)));
|
||||
expect(sliderTheme.disabledInactiveRailColor, equals(customColor2.withAlpha(0x1f)));
|
||||
expect(sliderTheme.activeTickMarkColor, equals(customColor3.withAlpha(0x8a)));
|
||||
expect(sliderTheme.inactiveTickMarkColor, equals(customColor1.withAlpha(0x8a)));
|
||||
expect(sliderTheme.disabledActiveTickMarkColor, equals(customColor3.withAlpha(0x1f)));
|
||||
expect(sliderTheme.disabledInactiveTickMarkColor, equals(customColor2.withAlpha(0x1f)));
|
||||
expect(sliderTheme.thumbColor, equals(customColor1.withAlpha(0xff)));
|
||||
expect(sliderTheme.disabledThumbColor, equals(customColor2.withAlpha(0x52)));
|
||||
expect(sliderTheme.overlayColor, equals(customColor1.withAlpha(0x29)));
|
||||
expect(sliderTheme.valueIndicatorColor, equals(customColor1.withAlpha(0xff)));
|
||||
expect(sliderTheme.thumbShape, equals(const isInstanceOf<RoundSliderThumbShape>()));
|
||||
expect(sliderTheme.valueIndicatorShape,
|
||||
equals(const isInstanceOf<PaddleSliderValueIndicatorShape>()));
|
||||
expect(sliderTheme.showValueIndicator, equals(ShowValueIndicator.onlyForDiscrete));
|
||||
});
|
||||
|
||||
testWidgets('SliderThemeData lerps correctly', (WidgetTester tester) async {
|
||||
final SliderThemeData sliderThemeBlack = new SliderThemeData.materialDefaults(
|
||||
primaryColor: Colors.black,
|
||||
primaryColorDark: Colors.black,
|
||||
primaryColorLight: Colors.black,
|
||||
);
|
||||
final SliderThemeData sliderThemeWhite = new SliderThemeData.materialDefaults(
|
||||
primaryColor: Colors.white,
|
||||
primaryColorDark: Colors.white,
|
||||
primaryColorLight: Colors.white,
|
||||
);
|
||||
final SliderThemeData lerp = SliderThemeData.lerp(sliderThemeBlack, sliderThemeWhite, 0.5);
|
||||
const Color middleGrey = const Color(0xff7f7f7f);
|
||||
expect(lerp.activeRailColor, equals(middleGrey.withAlpha(0xff)));
|
||||
expect(lerp.inactiveRailColor, equals(middleGrey.withAlpha(0x3d)));
|
||||
expect(lerp.disabledActiveRailColor, equals(middleGrey.withAlpha(0x52)));
|
||||
expect(lerp.disabledInactiveRailColor, equals(middleGrey.withAlpha(0x1f)));
|
||||
expect(lerp.activeTickMarkColor, equals(middleGrey.withAlpha(0x8a)));
|
||||
expect(lerp.inactiveTickMarkColor, equals(middleGrey.withAlpha(0x8a)));
|
||||
expect(lerp.disabledActiveTickMarkColor, equals(middleGrey.withAlpha(0x1f)));
|
||||
expect(lerp.disabledInactiveTickMarkColor, equals(middleGrey.withAlpha(0x1f)));
|
||||
expect(lerp.thumbColor, equals(middleGrey.withAlpha(0xff)));
|
||||
expect(lerp.disabledThumbColor, equals(middleGrey.withAlpha(0x52)));
|
||||
expect(lerp.overlayColor, equals(middleGrey.withAlpha(0x29)));
|
||||
expect(lerp.valueIndicatorColor, equals(middleGrey.withAlpha(0xff)));
|
||||
});
|
||||
|
||||
testWidgets('Default slider thumb shape draws correctly', (WidgetTester tester) async {
|
||||
final ThemeData theme = new ThemeData(
|
||||
platform: TargetPlatform.android,
|
||||
primarySwatch: Colors.blue,
|
||||
);
|
||||
final SliderThemeData sliderTheme = theme.sliderTheme.copyWith(thumbColor: Colors.red.shade500);
|
||||
double value = 0.45;
|
||||
Widget buildApp({
|
||||
int divisions,
|
||||
bool enabled: true,
|
||||
}) {
|
||||
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new Material(
|
||||
child: new Center(
|
||||
child: new SliderTheme(
|
||||
data: sliderTheme,
|
||||
child: new Slider(
|
||||
value: value,
|
||||
label: '$value',
|
||||
divisions: divisions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp());
|
||||
|
||||
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
|
||||
expect(sliderBox, paints..circle(color: sliderTheme.thumbColor, radius: 6.0));
|
||||
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pump(const Duration(milliseconds: 500)); // wait for disable animation
|
||||
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor, radius: 4.0));
|
||||
|
||||
await tester.pumpWidget(buildApp(divisions: 3));
|
||||
await tester.pump(const Duration(milliseconds: 500)); // 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));
|
||||
|
||||
await tester.pumpWidget(buildApp(divisions: 3, enabled: false));
|
||||
await tester.pump(const Duration(milliseconds: 500)); // 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));
|
||||
});
|
||||
|
||||
testWidgets('Default slider value indicator shape draws correctly', (WidgetTester tester) async {
|
||||
final ThemeData theme = new ThemeData(
|
||||
platform: TargetPlatform.android,
|
||||
primarySwatch: Colors.blue,
|
||||
);
|
||||
final SliderThemeData sliderTheme = theme.sliderTheme
|
||||
.copyWith(thumbColor: Colors.red.shade500, showValueIndicator: ShowValueIndicator.always);
|
||||
Widget buildApp(String value) {
|
||||
return new Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: new Material(
|
||||
child: new Center(
|
||||
child: new SliderTheme(
|
||||
data: sliderTheme,
|
||||
child: new Slider(
|
||||
value: 0.5,
|
||||
label: '$value',
|
||||
divisions: 3,
|
||||
onChanged: (double d) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp('1'));
|
||||
|
||||
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
|
||||
|
||||
Offset center = tester.getCenter(find.byType(Slider));
|
||||
TestGesture gesture = await tester.startGesture(center);
|
||||
await tester.pump();
|
||||
// Wait for value indicator animation to finish.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..path(
|
||||
color: sliderTheme.valueIndicatorColor,
|
||||
includes: <Offset>[
|
||||
const Offset(0.0, -40.0),
|
||||
const Offset(15.9, -40.0),
|
||||
const Offset(-15.9, -40.0),
|
||||
],
|
||||
excludes: <Offset>[const Offset(16.1, -40.0), const Offset(-16.1, -40.0)],
|
||||
));
|
||||
|
||||
await gesture.up();
|
||||
|
||||
// Test that it expands with a larger label.
|
||||
await tester.pumpWidget(buildApp('1000'));
|
||||
center = tester.getCenter(find.byType(Slider));
|
||||
gesture = await tester.startGesture(center);
|
||||
await tester.pump();
|
||||
// Wait for value indicator animation to finish.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(
|
||||
sliderBox,
|
||||
paints
|
||||
..path(
|
||||
color: sliderTheme.valueIndicatorColor,
|
||||
includes: <Offset>[
|
||||
const Offset(0.0, -40.0),
|
||||
const Offset(35.9, -40.0),
|
||||
const Offset(-35.9, -40.0),
|
||||
],
|
||||
excludes: <Offset>[const Offset(36.1, -40.0), const Offset(-36.1, -40.0)],
|
||||
));
|
||||
await gesture.up();
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue