blob: cbcc03b66cd9d00c640211e88edc299b02d1bd37 [file] [log] [blame]
// 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 '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.
///
/// ## Sample code
///
/// ```dart
/// class Launch extends StatefulWidget {
/// @override
/// State createState() => new LaunchState();
/// }
///
/// class LaunchState extends State<Launch> {
/// double _rocketThrust;
///
/// @override
/// Widget build(BuildContext context) {
/// return new SliderTheme(
/// data: SliderTheme.of(context).copyWith(activeTrackColor: const Color(0xff804040)),
/// 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 oldWidget) => data != oldWidget.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 "track", which is the line that the slider thumb slides along.
/// * The "value indicator", which is a shape that pops up when the user
/// is dragging the thumb to indicate the value being selected.
/// * The "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.fromPrimaryColors].
///
/// ## Sample code
///
/// ```dart
/// class Blissful extends StatefulWidget {
/// @override
/// State createState() => new BlissfulState();
/// }
///
/// class BlissfulState extends State<Blissful> {
/// double _bliss;
///
/// @override
/// Widget build(BuildContext context) {
/// return new SliderTheme(
/// data: SliderTheme.of(context).copyWith(activeTrackColor: const Color(0xff404080)),
/// child: new Slider(
/// onChanged: (double value) { setState(() { _bliss = value; }); },
/// value: _bliss,
/// ),
/// );
/// }
/// }
/// ```
const SliderThemeData({
@required this.activeTrackColor,
@required this.inactiveTrackColor,
@required this.disabledActiveTrackColor,
@required this.disabledInactiveTrackColor,
@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,
@required this.valueIndicatorTextStyle,
}) : assert(activeTrackColor != null),
assert(inactiveTrackColor != null),
assert(disabledActiveTrackColor != null),
assert(disabledInactiveTrackColor != null),
assert(activeTickMarkColor != null),
assert(inactiveTickMarkColor != null),
assert(disabledActiveTickMarkColor != null),
assert(disabledInactiveTickMarkColor != null),
assert(thumbColor != null),
assert(disabledThumbColor != null),
assert(overlayColor != null),
assert(valueIndicatorColor != null),
assert(thumbShape != null),
assert(valueIndicatorShape != null),
assert(valueIndicatorTextStyle != null),
assert(showValueIndicator != null);
/// 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.fromPrimaryColors({
@required Color primaryColor,
@required Color primaryColorDark,
@required Color primaryColorLight,
@required TextStyle valueIndicatorTextStyle,
}) {
assert(primaryColor != null);
assert(primaryColorDark != null);
assert(primaryColorLight != null);
assert(valueIndicatorTextStyle != null);
// These are Material Design defaults, and are used to derive
// component Colors (with opacity) from base colors.
const int activeTrackAlpha = 0xff;
const int inactiveTrackAlpha = 0x3d; // 24% opacity
const int disabledActiveTrackAlpha = 0x52; // 32% opacity
const int disabledInactiveTrackAlpha = 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(
activeTrackColor: primaryColor.withAlpha(activeTrackAlpha),
inactiveTrackColor: primaryColor.withAlpha(inactiveTrackAlpha),
disabledActiveTrackColor: primaryColorDark.withAlpha(disabledActiveTrackAlpha),
disabledInactiveTrackColor: primaryColorDark.withAlpha(disabledInactiveTrackAlpha),
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(),
valueIndicatorTextStyle: valueIndicatorTextStyle,
showValueIndicator: ShowValueIndicator.onlyForDiscrete,
);
}
/// The color of the [Slider] track between the [Slider.min] position and the
/// current thumb position.
final Color activeTrackColor;
/// The color of the [Slider] track between the current thumb position and the
/// [Slider.max] position.
final Color inactiveTrackColor;
/// The color of the [Slider] track between the [Slider.min] position and the
/// current thumb position when the [Slider] is disabled.
final Color disabledActiveTrackColor;
/// The color of the [Slider] track between the current thumb position and the
/// [Slider.max] position when the [Slider] is disabled.
final Color disabledInactiveTrackColor;
/// The color of the track's tick marks that are drawn between the [Slider.min]
/// position and the current thumb position.
final Color activeTickMarkColor;
/// The color of the track's tick marks that are drawn between the current
/// thumb position and the [Slider.max] position.
final Color inactiveTickMarkColor;
/// The color of the track's tick marks that are drawn between the [Slider.min]
/// position and the current thumb position when the [Slider] is disabled.
final Color disabledActiveTickMarkColor;
/// The color of the track's tick marks that are drawn between the current
/// thumb position and the [Slider.max] position when the [Slider] is
/// disabled.
final Color disabledInactiveTickMarkColor;
/// The color given to the [thumbShape] to draw itself with.
final Color thumbColor;
/// The color given to the [thumbShape] to draw itself with when the
/// [Slider] is disabled.
final Color disabledThumbColor;
/// The color of the overlay drawn around the slider thumb when it is pressed.
///
/// This is typically a semi-transparent color.
final Color overlayColor;
/// The color given to the [valueIndicatorShape] to draw itself with.
final Color valueIndicatorColor;
/// The shape and behavior that will be used to draw the [Slider]'s thumb.
///
/// This can be customized by implementing a subclass of
/// [SliderComponentShape].
final SliderComponentShape thumbShape;
/// The shape and behavior that will be used to draw the [Slider]'s value
/// indicator.
///
/// This can be customized by implementing a subclass of
/// [SliderComponentShape].
final SliderComponentShape valueIndicatorShape;
/// Whether the value indicator should be shown for different types of
/// sliders.
///
/// By default, [showValueIndicator] is set to
/// [ShowValueIndicator.onlyForDiscrete]. The value indicator is only shown
/// when the thumb is being touched.
final ShowValueIndicator showValueIndicator;
/// The text style for the text on the value indicator.
///
/// By default this is the [ThemeData.accentTextTheme.body2] text theme.
final TextStyle valueIndicatorTextStyle;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
SliderThemeData copyWith({
Color activeTrackColor,
Color inactiveTrackColor,
Color disabledActiveTrackColor,
Color disabledInactiveTrackColor,
Color activeTickMarkColor,
Color inactiveTickMarkColor,
Color disabledActiveTickMarkColor,
Color disabledInactiveTickMarkColor,
Color thumbColor,
Color disabledThumbColor,
Color overlayColor,
Color valueIndicatorColor,
SliderComponentShape thumbShape,
SliderComponentShape valueIndicatorShape,
ShowValueIndicator showValueIndicator,
TextStyle valueIndicatorTextStyle,
}) {
return new SliderThemeData(
activeTrackColor: activeTrackColor ?? this.activeTrackColor,
inactiveTrackColor: inactiveTrackColor ?? this.inactiveTrackColor,
disabledActiveTrackColor: disabledActiveTrackColor ?? this.disabledActiveTrackColor,
disabledInactiveTrackColor: disabledInactiveTrackColor ?? this.disabledInactiveTrackColor,
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,
valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle,
);
}
/// 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(
activeTrackColor: Color.lerp(a.activeTrackColor, b.activeTrackColor, t),
inactiveTrackColor: Color.lerp(a.inactiveTrackColor, b.inactiveTrackColor, t),
disabledActiveTrackColor: Color.lerp(a.disabledActiveTrackColor, b.disabledActiveTrackColor, t),
disabledInactiveTrackColor: Color.lerp(a.disabledInactiveTrackColor, b.disabledInactiveTrackColor, 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,
valueIndicatorTextStyle: TextStyle.lerp(a.valueIndicatorTextStyle, b.valueIndicatorTextStyle, t),
);
}
@override
int get hashCode {
return hashValues(
activeTrackColor,
inactiveTrackColor,
disabledActiveTrackColor,
disabledInactiveTrackColor,
activeTickMarkColor,
inactiveTickMarkColor,
disabledActiveTickMarkColor,
disabledInactiveTickMarkColor,
thumbColor,
disabledThumbColor,
overlayColor,
valueIndicatorColor,
thumbShape,
valueIndicatorShape,
showValueIndicator,
valueIndicatorTextStyle,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
final SliderThemeData otherData = other;
return otherData.activeTrackColor == activeTrackColor &&
otherData.inactiveTrackColor == inactiveTrackColor &&
otherData.disabledActiveTrackColor == disabledActiveTrackColor &&
otherData.disabledInactiveTrackColor == disabledInactiveTrackColor &&
otherData.activeTickMarkColor == activeTickMarkColor &&
otherData.inactiveTickMarkColor == inactiveTickMarkColor &&
otherData.disabledActiveTickMarkColor == disabledActiveTickMarkColor &&
otherData.disabledInactiveTickMarkColor == disabledInactiveTickMarkColor &&
otherData.thumbColor == thumbColor &&
otherData.disabledThumbColor == disabledThumbColor &&
otherData.overlayColor == overlayColor &&
otherData.valueIndicatorColor == valueIndicatorColor &&
otherData.thumbShape == thumbShape &&
otherData.valueIndicatorShape == valueIndicatorShape &&
otherData.showValueIndicator == showValueIndicator &&
otherData.valueIndicatorTextStyle == valueIndicatorTextStyle;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
final ThemeData defaultTheme = new ThemeData.fallback();
final SliderThemeData defaultData = new SliderThemeData.fromPrimaryColors(
primaryColor: defaultTheme.primaryColor,
primaryColorDark: defaultTheme.primaryColorDark,
primaryColorLight: defaultTheme.primaryColorLight,
valueIndicatorTextStyle: defaultTheme.accentTextTheme.body2,
);
properties.add(new DiagnosticsProperty<Color>('activeTrackColor', activeTrackColor, defaultValue: defaultData.activeTrackColor));
properties.add(new DiagnosticsProperty<Color>('inactiveTrackColor', inactiveTrackColor, defaultValue: defaultData.inactiveTrackColor));
properties.add(new DiagnosticsProperty<Color>('disabledActiveTrackColor', disabledActiveTrackColor, defaultValue: defaultData.disabledActiveTrackColor, level: DiagnosticLevel.debug));
properties.add(new DiagnosticsProperty<Color>('disabledInactiveTrackColor', disabledInactiveTrackColor, defaultValue: defaultData.disabledInactiveTrackColor, level: DiagnosticLevel.debug));
properties.add(new DiagnosticsProperty<Color>('activeTickMarkColor', activeTickMarkColor, defaultValue: defaultData.activeTickMarkColor, level: DiagnosticLevel.debug));
properties.add(new DiagnosticsProperty<Color>('inactiveTickMarkColor', inactiveTickMarkColor, defaultValue: defaultData.inactiveTickMarkColor, level: DiagnosticLevel.debug));
properties.add(new DiagnosticsProperty<Color>('disabledActiveTickMarkColor', disabledActiveTickMarkColor, defaultValue: defaultData.disabledActiveTickMarkColor, level: DiagnosticLevel.debug));
properties.add(new DiagnosticsProperty<Color>('disabledInactiveTickMarkColor', disabledInactiveTickMarkColor, defaultValue: defaultData.disabledInactiveTickMarkColor, level: DiagnosticLevel.debug));
properties.add(new DiagnosticsProperty<Color>('thumbColor', thumbColor, defaultValue: defaultData.thumbColor));
properties.add(new DiagnosticsProperty<Color>('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor, level: DiagnosticLevel.debug));
properties.add(new DiagnosticsProperty<Color>('overlayColor', overlayColor, defaultValue: defaultData.overlayColor, level: DiagnosticLevel.debug));
properties.add(new DiagnosticsProperty<Color>('valueIndicatorColor', valueIndicatorColor, defaultValue: defaultData.valueIndicatorColor));
properties.add(new DiagnosticsProperty<SliderComponentShape>('thumbShape', thumbShape, defaultValue: defaultData.thumbShape, level: DiagnosticLevel.debug));
properties.add(new DiagnosticsProperty<SliderComponentShape>('valueIndicatorShape', valueIndicatorShape, defaultValue: defaultData.valueIndicatorShape, level: DiagnosticLevel.debug));
properties.add(new EnumProperty<ShowValueIndicator>('showValueIndicator', showValueIndicator, defaultValue: defaultData.showValueIndicator));
properties.add(new DiagnosticsProperty<TextStyle>('valueIndicatorTextStyle', valueIndicatorTextStyle, defaultValue: defaultData.valueIndicatorTextStyle));
}
}
/// 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.
///
/// [value] is the current parametric value (from 0.0 to 1.0) of the slider.
///
/// If [labelPainter] is non-null, then [labelPainter.paint] should be
/// called with the location that the label should appear. If the labelPainter
/// passed is null, then no label was supplied to the [Slider].
void paint(
PaintingContext context,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
});
}
/// This is the default shape to a [Slider]'s thumb if no
/// 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 {
/// Create a slider thumb that draws a circle.
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,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = new Tween<double>(
begin: _disabledThumbRadius,
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 {
/// Create a slider value indicator in the shape of an upside-down pear.
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;
// Designed size of the label text. This is the size that the value indicator
// was designed to contain. We scale it from here to fit other sizes.
static const double _labelTextDesignSize = 14.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);
// Set to true if you want a rectangle to be drawn around the label bubble.
// This helps with building tests that check that the label draws in the right
// place (because it prints the rect in the failed test output). It should not
// be checked in while set to "true".
static const bool _debuggingLabelLocation = false;
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;
}
// Determines the "best" offset to keep the bubble on the screen. The calling
// code will bound that with the available movement in the paddle shape.
double _getIdealOffset(
RenderBox parentBox,
double halfWidthNeeded,
double scale,
Offset center,
) {
const double edgeMargin = 4.0;
final Rect topLobeRect = new Rect.fromLTWH(
-_topLobeRadius - halfWidthNeeded,
-_topLobeRadius - _distanceBetweenTopBottomCenters,
2.0 * (_topLobeRadius + halfWidthNeeded),
2.0 * _topLobeRadius,
);
// We can just multiply by scale instead of a transform, since we're scaling
// around (0, 0).
final Offset topLeft = (topLobeRect.topLeft * scale) + center;
final Offset bottomRight = (topLobeRect.bottomRight * scale) + center;
double shift = 0.0;
if (topLeft.dx < edgeMargin) {
shift = edgeMargin - topLeft.dx;
}
if (bottomRight.dx > parentBox.size.width - edgeMargin) {
shift = parentBox.size.width - bottomRight.dx - edgeMargin;
}
shift = scale == 0.0 ? 0.0 : shift / scale;
return shift;
}
void _drawValueIndicator(
RenderBox parentBox,
Canvas canvas,
Offset center,
Paint paint,
double scale,
TextPainter labelPainter,
) {
canvas.save();
canvas.translate(center.dx, center.dy);
// The entire value indicator should scale with the size of the label,
// to keep it large enough to encompass the label text.
final double textScaleFactor = labelPainter.height / _labelTextDesignSize;
final double overallScale = scale * textScaleFactor;
canvas.scale(overallScale, overallScale);
final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
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),
);
double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center);
double leftWidthNeeded;
double rightWidthNeeded;
if (shift < 0.0) {
// shifting to the left
shift = math.max(shift, -halfWidthNeeded);
} else {
// shifting to the right
shift = math.min(shift, halfWidthNeeded);
}
rightWidthNeeded = halfWidthNeeded + shift;
leftWidthNeeded = halfWidthNeeded - shift;
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 leftAmount = math.max(0.0, math.min(1.0, leftWidthNeeded / neckTriangleBase));
final double rightAmount = math.max(0.0, math.min(1.0, rightWidthNeeded / neckTriangleBase));
// The angle between the top neck arc's center and the top lobe's center
// and vertical.
final double leftTheta = (1.0 - leftAmount) * _thirtyDegrees;
final double rightTheta = (1.0 - rightAmount) * _thirtyDegrees;
// The center of the top left neck arc.
final Offset neckLeftCenter = new Offset(
-neckTriangleBase,
_topLobeCenter.dy + math.cos(leftTheta) * _neckTriangleHypotenuse,
);
final Offset neckRightCenter = new Offset(
neckTriangleBase,
_topLobeCenter.dy + math.cos(rightTheta) * _neckTriangleHypotenuse,
);
final double leftNeckArcAngle = _ninetyDegrees - leftTheta;
final double rightNeckArcAngle = math.pi + _ninetyDegrees - rightTheta;
// The distance between the end of the bottom neck arc and the beginning of
// the top neck arc. We use this to shrink/expand it based on the scale
// factor of the value indicator.
final double neckStretchBaseline = bottomLobeEnd.dy - math.max(neckLeftCenter.dy, neckRightCenter.dy);
final double t = math.pow(inverseTextScale, 3.0);
final double stretch = (neckStretchBaseline * t).clamp(0.0, 10.0 * neckStretchBaseline);
final Offset neckStretch = new Offset(0.0, neckStretchBaseline - stretch);
assert(!_debuggingLabelLocation ||
() {
final Offset leftCenter = _topLobeCenter - new Offset(leftWidthNeeded, 0.0) + neckStretch;
final Offset rightCenter = _topLobeCenter + new Offset(rightWidthNeeded, 0.0) + neckStretch;
final Rect valueRect = new Rect.fromLTRB(
leftCenter.dx - _topLobeRadius,
leftCenter.dy - _topLobeRadius,
rightCenter.dx + _topLobeRadius,
rightCenter.dy + _topLobeRadius,
);
final Paint outlinePaint = new Paint()
..color = const Color(0xffff0000)
..style = PaintingStyle.stroke
..strokeWidth = 1.0;
canvas.drawRect(valueRect, outlinePaint);
return true;
}());
_addArc(
path,
neckLeftCenter + neckStretch,
_topNeckRadius,
0.0,
-leftNeckArcAngle,
);
_addArc(
path,
_topLobeCenter - new Offset(leftWidthNeeded, 0.0) + neckStretch,
_topLobeRadius,
_ninetyDegrees + leftTheta,
_twoSeventyDegrees,
);
_addArc(
path,
_topLobeCenter + new Offset(rightWidthNeeded, 0.0) + neckStretch,
_topLobeRadius,
_twoSeventyDegrees,
_twoSeventyDegrees + math.pi - rightTheta,
);
_addArc(
path,
neckRightCenter + neckStretch,
_topNeckRadius,
rightNeckArcAngle,
math.pi,
);
canvas.drawPath(path, paint);
// Draw the label.
canvas.save();
canvas.translate(shift, -_distanceBetweenTopBottomCenters + neckStretch.dy);
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,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isDiscrete,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
final ColorTween enableColor = new ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
);
_drawValueIndicator(
parentBox,
context.canvas,
thumbCenter,
new Paint()..color = enableColor.evaluate(enableAnimation),
activationAnimation.value,
labelPainter,
);
}
}