blob: 492de070e72882d320b4dd3f1c40c658fc91f83c [file] [log] [blame]
// Copyright 2014 The Flutter 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, lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'material_state.dart';
import 'slider.dart';
import 'theme.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.
///
/// The slider is as big as the largest of
/// the [SliderComponentShape.getPreferredSize] of the thumb shape,
/// the [SliderComponentShape.getPreferredSize] of the overlay shape,
/// and the [SliderTickMarkShape.getPreferredSize] of the tick mark shape.
///
/// See also:
///
/// * [SliderThemeData], which describes the actual configuration of a slider
/// theme.
/// * [SliderComponentShape], which can be used to create custom shapes for
/// the [Slider]'s thumb, overlay, and value indicator and the
/// [RangeSlider]'s overlay.
/// * [SliderTrackShape], which can be used to create custom shapes for the
/// [Slider]'s track.
/// * [SliderTickMarkShape], which can be used to create custom shapes for the
/// [Slider]'s tick marks.
/// * [RangeSliderThumbShape], which can be used to create custom shapes for
/// the [RangeSlider]'s thumb.
/// * [RangeSliderValueIndicatorShape], which can be used to create custom
/// shapes for the [RangeSlider]'s value indicator.
/// * [RangeSliderTrackShape], which can be used to create custom shapes for
/// the [RangeSlider]'s track.
/// * [RangeSliderTickMarkShape], which can be used to create custom shapes for
/// the [RangeSlider]'s tick marks.
class SliderTheme extends InheritedTheme {
/// Applies the given theme [data] to [child].
///
/// The [data] and [child] arguments must not be null.
const SliderTheme({
super.key,
required this.data,
required super.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.
///
/// {@tool snippet}
///
/// ```dart
/// class Launch extends StatefulWidget {
/// const Launch({super.key});
///
/// @override
/// State createState() => LaunchState();
/// }
///
/// class LaunchState extends State<Launch> {
/// double _rocketThrust = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// return SliderTheme(
/// data: SliderTheme.of(context).copyWith(activeTrackColor: const Color(0xff804040)),
/// child: Slider(
/// onChanged: (double value) { setState(() { _rocketThrust = value; }); },
/// value: _rocketThrust,
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [SliderThemeData], which describes the actual configuration of a slider
/// theme.
static SliderThemeData of(BuildContext context) {
final SliderTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<SliderTheme>();
return inheritedTheme != null ? inheritedTheme.data : Theme.of(context).sliderTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return SliderTheme(data: data, child: child);
}
@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,
}
/// Identifier for a thumb.
///
/// There are 2 thumbs in a [RangeSlider], [start] and [end].
///
/// For [TextDirection.ltr], the [start] thumb is the left-most thumb and the
/// [end] thumb is the right-most thumb. For [TextDirection.rtl] the [start]
/// thumb is the right-most thumb, and the [end] thumb is the left-most thumb.
enum Thumb {
/// Left-most thumb for [TextDirection.ltr], otherwise, right-most thumb.
start,
/// Right-most thumb for [TextDirection.ltr], otherwise, left-most thumb.
end,
}
/// 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].
///
/// This theme is for both the [Slider] and the [RangeSlider]. The properties
/// that are only for the [Slider] are: [tickMarkShape], [thumbShape],
/// [trackShape], and [valueIndicatorShape]. The properties that are only for
/// the [RangeSlider] are [rangeTickMarkShape], [rangeThumbShape],
/// [rangeTrackShape], [rangeValueIndicatorShape],
/// [overlappingShapeStrokeColor], [minThumbSeparation], and [thumbSelector].
/// All other properties are used by both the [Slider] and the [RangeSlider].
///
/// 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 "tick marks", which are regularly spaced marks that are drawn when
/// using discrete divisions.
/// * The "value indicator", which appears when the user is dragging the thumb
/// to indicate the value being selected.
/// * The "overlay", which appears around the thumb, and is shown when the
/// thumb is pressed, focused, or hovered. It is painted underneath the
/// thumb, so it must extend beyond the bounds of the thumb itself to
/// actually be visible.
/// * The "active" side of the slider is the side between the thumb and the
/// minimum value.
/// * The "inactive" side of the slider is the side between the thumb and the
/// maximum value.
/// * The [Slider] is disabled when it is not accepting user input. See
/// [Slider] for details on when this happens.
///
/// The thumb, track, tick marks, value indicator, and overlay can be customized
/// by creating subclasses of [SliderTrackShape],
/// [SliderComponentShape], and/or [SliderTickMarkShape]. See
/// [RoundSliderThumbShape], [RectangularSliderTrackShape],
/// [RoundSliderTickMarkShape], [RectangularSliderValueIndicatorShape], and
/// [RoundSliderOverlayShape] for examples.
///
/// The track painting can be skipped by specifying 0 for [trackHeight].
/// The thumb painting can be skipped by specifying
/// [SliderComponentShape.noThumb] for [SliderThemeData.thumbShape].
/// The overlay painting can be skipped by specifying
/// [SliderComponentShape.noOverlay] for [SliderThemeData.overlayShape].
/// The tick mark painting can be skipped by specifying
/// [SliderTickMarkShape.noTickMark] for [SliderThemeData.tickMarkShape].
/// The value indicator painting can be skipped by specifying the
/// appropriate [ShowValueIndicator] for [SliderThemeData.showValueIndicator].
///
/// 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], which can be used to create custom shapes for
/// the [Slider]'s thumb, overlay, and value indicator and the
/// [RangeSlider]'s overlay.
/// * [SliderTrackShape], which can be used to create custom shapes for the
/// [Slider]'s track.
/// * [SliderTickMarkShape], which can be used to create custom shapes for the
/// [Slider]'s tick marks.
/// * [RangeSliderThumbShape], which can be used to create custom shapes for
/// the [RangeSlider]'s thumb.
/// * [RangeSliderValueIndicatorShape], which can be used to create custom
/// shapes for the [RangeSlider]'s value indicator.
/// * [RangeSliderTrackShape], which can be used to create custom shapes for
/// the [RangeSlider]'s track.
/// * [RangeSliderTickMarkShape], which can be used to create custom shapes for
/// the [RangeSlider]'s tick marks.
@immutable
class SliderThemeData with Diagnosticable {
/// Create a [SliderThemeData] given a set of exact values.
///
/// 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].
///
/// {@tool snippet}
///
/// ```dart
/// class Blissful extends StatefulWidget {
/// const Blissful({super.key});
///
/// @override
/// State createState() => BlissfulState();
/// }
///
/// class BlissfulState extends State<Blissful> {
/// double _bliss = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// return SliderTheme(
/// data: SliderTheme.of(context).copyWith(activeTrackColor: const Color(0xff404080)),
/// child: Slider(
/// onChanged: (double value) { setState(() { _bliss = value; }); },
/// value: _bliss,
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
const SliderThemeData({
this.trackHeight,
this.activeTrackColor,
this.inactiveTrackColor,
this.secondaryActiveTrackColor,
this.disabledActiveTrackColor,
this.disabledInactiveTrackColor,
this.disabledSecondaryActiveTrackColor,
this.activeTickMarkColor,
this.inactiveTickMarkColor,
this.disabledActiveTickMarkColor,
this.disabledInactiveTickMarkColor,
this.thumbColor,
this.overlappingShapeStrokeColor,
this.disabledThumbColor,
this.overlayColor,
this.valueIndicatorColor,
this.overlayShape,
this.tickMarkShape,
this.thumbShape,
this.trackShape,
this.valueIndicatorShape,
this.rangeTickMarkShape,
this.rangeThumbShape,
this.rangeTrackShape,
this.rangeValueIndicatorShape,
this.showValueIndicator,
this.valueIndicatorTextStyle,
this.minThumbSeparation,
this.thumbSelector,
this.mouseCursor,
this.allowedInteraction,
});
/// 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,
}) {
// 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 secondaryActiveTrackAlpha = 0x8a; // 54% opacity
const int disabledActiveTrackAlpha = 0x52; // 32% opacity
const int disabledInactiveTrackAlpha = 0x1f; // 12% opacity
const int disabledSecondaryActiveTrackAlpha = 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 overlayAlpha = 0x1f; // 12% opacity
const int valueIndicatorAlpha = 0xff;
return SliderThemeData(
trackHeight: 2.0,
activeTrackColor: primaryColor.withAlpha(activeTrackAlpha),
inactiveTrackColor: primaryColor.withAlpha(inactiveTrackAlpha),
secondaryActiveTrackColor: primaryColor.withAlpha(secondaryActiveTrackAlpha),
disabledActiveTrackColor: primaryColorDark.withAlpha(disabledActiveTrackAlpha),
disabledInactiveTrackColor: primaryColorDark.withAlpha(disabledInactiveTrackAlpha),
disabledSecondaryActiveTrackColor: primaryColorDark.withAlpha(disabledSecondaryActiveTrackAlpha),
activeTickMarkColor: primaryColorLight.withAlpha(activeTickMarkAlpha),
inactiveTickMarkColor: primaryColor.withAlpha(inactiveTickMarkAlpha),
disabledActiveTickMarkColor: primaryColorLight.withAlpha(disabledActiveTickMarkAlpha),
disabledInactiveTickMarkColor: primaryColorDark.withAlpha(disabledInactiveTickMarkAlpha),
thumbColor: primaryColor.withAlpha(thumbAlpha),
overlappingShapeStrokeColor: Colors.white,
disabledThumbColor: primaryColorDark.withAlpha(disabledThumbAlpha),
overlayColor: primaryColor.withAlpha(overlayAlpha),
valueIndicatorColor: primaryColor.withAlpha(valueIndicatorAlpha),
overlayShape: const RoundSliderOverlayShape(),
tickMarkShape: const RoundSliderTickMarkShape(),
thumbShape: const RoundSliderThumbShape(),
trackShape: const RoundedRectSliderTrackShape(),
valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
rangeTickMarkShape: const RoundRangeSliderTickMarkShape(),
rangeThumbShape: const RoundRangeSliderThumbShape(),
rangeTrackShape: const RoundedRectRangeSliderTrackShape(),
rangeValueIndicatorShape: const PaddleRangeSliderValueIndicatorShape(),
valueIndicatorTextStyle: valueIndicatorTextStyle,
showValueIndicator: ShowValueIndicator.onlyForDiscrete,
);
}
/// The height of the [Slider] track.
final double? trackHeight;
/// The color of the [Slider] track between the [Slider.min] position and the
/// current thumb position.
final Color? activeTrackColor;
/// 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 current thumb position and the
/// [Slider.secondaryTrackValue] position.
final Color? secondaryActiveTrackColor;
/// 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.secondaryTrackValue] position when the [Slider] is disabled.
final Color? disabledSecondaryActiveTrackColor;
/// 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 perimeter of the top [rangeThumbShape] when the
/// thumbs are overlapping and the top [rangeValueIndicatorShape] when the
/// value indicators are overlapping.
final Color? overlappingShapeStrokeColor;
/// 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, focused, or hovered.
///
/// 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 that will be used to draw the [Slider]'s overlay.
///
/// Both the [overlayColor] and a non default [overlayShape] may be specified.
/// The default [overlayShape] refers to the [overlayColor].
///
/// The default value is [RoundSliderOverlayShape].
final SliderComponentShape? overlayShape;
/// The shape that will be used to draw the [Slider]'s tick marks.
///
/// The [SliderTickMarkShape.getPreferredSize] is used to help determine the
/// location of each tick mark on the track. The slider's minimum size will
/// be at least this big.
///
/// The default value is [RoundSliderTickMarkShape].
///
/// See also:
///
/// * [RoundRangeSliderTickMarkShape], which is the default tick mark
/// shape for the range slider.
final SliderTickMarkShape? tickMarkShape;
/// The shape that will be used to draw the [Slider]'s thumb.
///
/// The default value is [RoundSliderThumbShape].
///
/// See also:
///
/// * [RoundRangeSliderThumbShape], which is the default thumb shape for
/// the [RangeSlider].
final SliderComponentShape? thumbShape;
/// The shape that will be used to draw the [Slider]'s track.
///
/// The [SliderTrackShape.getPreferredRect] method is used to map
/// slider-relative gesture coordinates to the correct thumb position on the
/// track. It is also used to horizontally position tick marks, when the
/// slider is discrete.
///
/// The default value is [RoundedRectSliderTrackShape].
///
/// See also:
///
/// * [RoundedRectRangeSliderTrackShape], which is the default track
/// shape for the [RangeSlider].
final SliderTrackShape? trackShape;
/// The shape that will be used to draw the [Slider]'s value
/// indicator.
///
/// The default value is [PaddleSliderValueIndicatorShape].
///
/// See also:
///
/// * [PaddleRangeSliderValueIndicatorShape], which is the default value
/// indicator shape for the [RangeSlider].
final SliderComponentShape? valueIndicatorShape;
/// The shape that will be used to draw the [RangeSlider]'s tick marks.
///
/// The [RangeSliderTickMarkShape.getPreferredSize] is used to help determine
/// the location of each tick mark on the track. The slider's minimum size
/// will be at least this big.
///
/// The default value is [RoundRangeSliderTickMarkShape].
///
/// See also:
///
/// * [RoundSliderTickMarkShape], which is the default tick mark shape
/// for the [Slider].
final RangeSliderTickMarkShape? rangeTickMarkShape;
/// The shape that will be used for the [RangeSlider]'s thumbs.
///
/// By default the same shape is used for both thumbs, but strokes the top
/// thumb when it overlaps the bottom thumb. The top thumb is always the last
/// selected thumb.
///
/// The default value is [RoundRangeSliderThumbShape].
///
/// See also:
///
/// * [RoundSliderThumbShape], which is the default thumb shape for the
/// [Slider].
final RangeSliderThumbShape? rangeThumbShape;
/// The shape that will be used to draw the [RangeSlider]'s track.
///
/// The [SliderTrackShape.getPreferredRect] method is used to map
/// slider-relative gesture coordinates to the correct thumb position on the
/// track. It is also used to horizontally position the tick marks, when the
/// slider is discrete.
///
/// The default value is [RoundedRectRangeSliderTrackShape].
///
/// See also:
///
/// * [RoundedRectSliderTrackShape], which is the default track
/// shape for the [Slider].
final RangeSliderTrackShape? rangeTrackShape;
/// The shape that will be used for the [RangeSlider]'s value indicators.
///
/// The default shape uses the same value indicator for each thumb, but
/// strokes the top value indicator when it overlaps the bottom value
/// indicator. The top indicator corresponds to the top thumb, which is always
/// the most recently selected thumb.
///
/// The default value is [PaddleRangeSliderValueIndicatorShape].
///
/// See also:
///
/// * [PaddleSliderValueIndicatorShape], which is the default value
/// indicator shape for the [Slider].
final RangeSliderValueIndicatorShape? rangeValueIndicatorShape;
/// 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.
final TextStyle? valueIndicatorTextStyle;
/// Limits the thumb's separation distance.
///
/// Use this only if you want to control the visual appearance of the thumbs
/// in terms of a logical pixel value. This can be done when you want a
/// specific look for thumbs when they are close together. To limit with the
/// real values, rather than logical pixels, the values can be restricted by
/// the parent.
final double? minThumbSeparation;
/// Determines which thumb should be selected when the slider is interacted
/// with.
///
/// If null, the default thumb selector finds the closest thumb, excluding
/// taps that are between the thumbs and not within any one touch target.
/// When the selection is within the touch target bounds of both thumbs, no
/// thumb is selected until the selection is moved.
///
/// Override this for custom thumb selection.
final RangeThumbSelector? thumbSelector;
/// {@macro flutter.material.slider.mouseCursor}
///
/// If specified, overrides the default value of [Slider.mouseCursor].
final MaterialStateProperty<MouseCursor?>? mouseCursor;
/// Allowed way for the user to interact with the [Slider].
///
/// If specified, overrides the default value of [Slider.allowedInteraction].
final SliderInteraction? allowedInteraction;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
SliderThemeData copyWith({
double? trackHeight,
Color? activeTrackColor,
Color? inactiveTrackColor,
Color? secondaryActiveTrackColor,
Color? disabledActiveTrackColor,
Color? disabledInactiveTrackColor,
Color? disabledSecondaryActiveTrackColor,
Color? activeTickMarkColor,
Color? inactiveTickMarkColor,
Color? disabledActiveTickMarkColor,
Color? disabledInactiveTickMarkColor,
Color? thumbColor,
Color? overlappingShapeStrokeColor,
Color? disabledThumbColor,
Color? overlayColor,
Color? valueIndicatorColor,
SliderComponentShape? overlayShape,
SliderTickMarkShape? tickMarkShape,
SliderComponentShape? thumbShape,
SliderTrackShape? trackShape,
SliderComponentShape? valueIndicatorShape,
RangeSliderTickMarkShape? rangeTickMarkShape,
RangeSliderThumbShape? rangeThumbShape,
RangeSliderTrackShape? rangeTrackShape,
RangeSliderValueIndicatorShape? rangeValueIndicatorShape,
ShowValueIndicator? showValueIndicator,
TextStyle? valueIndicatorTextStyle,
double? minThumbSeparation,
RangeThumbSelector? thumbSelector,
MaterialStateProperty<MouseCursor?>? mouseCursor,
SliderInteraction? allowedInteraction,
}) {
return SliderThemeData(
trackHeight: trackHeight ?? this.trackHeight,
activeTrackColor: activeTrackColor ?? this.activeTrackColor,
inactiveTrackColor: inactiveTrackColor ?? this.inactiveTrackColor,
secondaryActiveTrackColor: secondaryActiveTrackColor ?? this.secondaryActiveTrackColor,
disabledActiveTrackColor: disabledActiveTrackColor ?? this.disabledActiveTrackColor,
disabledInactiveTrackColor: disabledInactiveTrackColor ?? this.disabledInactiveTrackColor,
disabledSecondaryActiveTrackColor: disabledSecondaryActiveTrackColor ?? this.disabledSecondaryActiveTrackColor,
activeTickMarkColor: activeTickMarkColor ?? this.activeTickMarkColor,
inactiveTickMarkColor: inactiveTickMarkColor ?? this.inactiveTickMarkColor,
disabledActiveTickMarkColor: disabledActiveTickMarkColor ?? this.disabledActiveTickMarkColor,
disabledInactiveTickMarkColor: disabledInactiveTickMarkColor ?? this.disabledInactiveTickMarkColor,
thumbColor: thumbColor ?? this.thumbColor,
overlappingShapeStrokeColor: overlappingShapeStrokeColor ?? this.overlappingShapeStrokeColor,
disabledThumbColor: disabledThumbColor ?? this.disabledThumbColor,
overlayColor: overlayColor ?? this.overlayColor,
valueIndicatorColor: valueIndicatorColor ?? this.valueIndicatorColor,
overlayShape: overlayShape ?? this.overlayShape,
tickMarkShape: tickMarkShape ?? this.tickMarkShape,
thumbShape: thumbShape ?? this.thumbShape,
trackShape: trackShape ?? this.trackShape,
valueIndicatorShape: valueIndicatorShape ?? this.valueIndicatorShape,
rangeTickMarkShape: rangeTickMarkShape ?? this.rangeTickMarkShape,
rangeThumbShape: rangeThumbShape ?? this.rangeThumbShape,
rangeTrackShape: rangeTrackShape ?? this.rangeTrackShape,
rangeValueIndicatorShape: rangeValueIndicatorShape ?? this.rangeValueIndicatorShape,
showValueIndicator: showValueIndicator ?? this.showValueIndicator,
valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle,
minThumbSeparation: minThumbSeparation ?? this.minThumbSeparation,
thumbSelector: thumbSelector ?? this.thumbSelector,
mouseCursor: mouseCursor ?? this.mouseCursor,
allowedInteraction: allowedInteraction ?? this.allowedInteraction,
);
}
/// Linearly interpolate between two slider themes.
///
/// The arguments must not be null.
///
/// {@macro dart.ui.shadow.lerp}
static SliderThemeData lerp(SliderThemeData a, SliderThemeData b, double t) {
if (identical(a, b)) {
return a;
}
return SliderThemeData(
trackHeight: lerpDouble(a.trackHeight, b.trackHeight, t),
activeTrackColor: Color.lerp(a.activeTrackColor, b.activeTrackColor, t),
inactiveTrackColor: Color.lerp(a.inactiveTrackColor, b.inactiveTrackColor, t),
secondaryActiveTrackColor: Color.lerp(a.secondaryActiveTrackColor, b.secondaryActiveTrackColor, t),
disabledActiveTrackColor: Color.lerp(a.disabledActiveTrackColor, b.disabledActiveTrackColor, t),
disabledInactiveTrackColor: Color.lerp(a.disabledInactiveTrackColor, b.disabledInactiveTrackColor, t),
disabledSecondaryActiveTrackColor: Color.lerp(a.disabledSecondaryActiveTrackColor, b.disabledSecondaryActiveTrackColor, 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),
overlappingShapeStrokeColor: Color.lerp(a.overlappingShapeStrokeColor, b.overlappingShapeStrokeColor, 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),
overlayShape: t < 0.5 ? a.overlayShape : b.overlayShape,
tickMarkShape: t < 0.5 ? a.tickMarkShape : b.tickMarkShape,
thumbShape: t < 0.5 ? a.thumbShape : b.thumbShape,
trackShape: t < 0.5 ? a.trackShape : b.trackShape,
valueIndicatorShape: t < 0.5 ? a.valueIndicatorShape : b.valueIndicatorShape,
rangeTickMarkShape: t < 0.5 ? a.rangeTickMarkShape : b.rangeTickMarkShape,
rangeThumbShape: t < 0.5 ? a.rangeThumbShape : b.rangeThumbShape,
rangeTrackShape: t < 0.5 ? a.rangeTrackShape : b.rangeTrackShape,
rangeValueIndicatorShape: t < 0.5 ? a.rangeValueIndicatorShape : b.rangeValueIndicatorShape,
showValueIndicator: t < 0.5 ? a.showValueIndicator : b.showValueIndicator,
valueIndicatorTextStyle: TextStyle.lerp(a.valueIndicatorTextStyle, b.valueIndicatorTextStyle, t),
minThumbSeparation: lerpDouble(a.minThumbSeparation, b.minThumbSeparation, t),
thumbSelector: t < 0.5 ? a.thumbSelector : b.thumbSelector,
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
allowedInteraction: t < 0.5 ? a.allowedInteraction : b.allowedInteraction,
);
}
@override
int get hashCode => Object.hash(
trackHeight,
activeTrackColor,
inactiveTrackColor,
secondaryActiveTrackColor,
disabledActiveTrackColor,
disabledInactiveTrackColor,
disabledSecondaryActiveTrackColor,
activeTickMarkColor,
inactiveTickMarkColor,
disabledActiveTickMarkColor,
disabledInactiveTickMarkColor,
thumbColor,
overlappingShapeStrokeColor,
disabledThumbColor,
overlayColor,
valueIndicatorColor,
overlayShape,
tickMarkShape,
thumbShape,
Object.hash(
trackShape,
valueIndicatorShape,
rangeTickMarkShape,
rangeThumbShape,
rangeTrackShape,
rangeValueIndicatorShape,
showValueIndicator,
valueIndicatorTextStyle,
minThumbSeparation,
thumbSelector,
mouseCursor,
allowedInteraction,
),
);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is SliderThemeData
&& other.trackHeight == trackHeight
&& other.activeTrackColor == activeTrackColor
&& other.inactiveTrackColor == inactiveTrackColor
&& other.secondaryActiveTrackColor == secondaryActiveTrackColor
&& other.disabledActiveTrackColor == disabledActiveTrackColor
&& other.disabledInactiveTrackColor == disabledInactiveTrackColor
&& other.disabledSecondaryActiveTrackColor == disabledSecondaryActiveTrackColor
&& other.activeTickMarkColor == activeTickMarkColor
&& other.inactiveTickMarkColor == inactiveTickMarkColor
&& other.disabledActiveTickMarkColor == disabledActiveTickMarkColor
&& other.disabledInactiveTickMarkColor == disabledInactiveTickMarkColor
&& other.thumbColor == thumbColor
&& other.overlappingShapeStrokeColor == overlappingShapeStrokeColor
&& other.disabledThumbColor == disabledThumbColor
&& other.overlayColor == overlayColor
&& other.valueIndicatorColor == valueIndicatorColor
&& other.overlayShape == overlayShape
&& other.tickMarkShape == tickMarkShape
&& other.thumbShape == thumbShape
&& other.trackShape == trackShape
&& other.valueIndicatorShape == valueIndicatorShape
&& other.rangeTickMarkShape == rangeTickMarkShape
&& other.rangeThumbShape == rangeThumbShape
&& other.rangeTrackShape == rangeTrackShape
&& other.rangeValueIndicatorShape == rangeValueIndicatorShape
&& other.showValueIndicator == showValueIndicator
&& other.valueIndicatorTextStyle == valueIndicatorTextStyle
&& other.minThumbSeparation == minThumbSeparation
&& other.thumbSelector == thumbSelector
&& other.mouseCursor == mouseCursor
&& other.allowedInteraction == allowedInteraction;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
const SliderThemeData defaultData = SliderThemeData();
properties.add(DoubleProperty('trackHeight', trackHeight, defaultValue: defaultData.trackHeight));
properties.add(ColorProperty('activeTrackColor', activeTrackColor, defaultValue: defaultData.activeTrackColor));
properties.add(ColorProperty('inactiveTrackColor', inactiveTrackColor, defaultValue: defaultData.inactiveTrackColor));
properties.add(ColorProperty('secondaryActiveTrackColor', secondaryActiveTrackColor, defaultValue: defaultData.secondaryActiveTrackColor));
properties.add(ColorProperty('disabledActiveTrackColor', disabledActiveTrackColor, defaultValue: defaultData.disabledActiveTrackColor));
properties.add(ColorProperty('disabledInactiveTrackColor', disabledInactiveTrackColor, defaultValue: defaultData.disabledInactiveTrackColor));
properties.add(ColorProperty('disabledSecondaryActiveTrackColor', disabledSecondaryActiveTrackColor, defaultValue: defaultData.disabledSecondaryActiveTrackColor));
properties.add(ColorProperty('activeTickMarkColor', activeTickMarkColor, defaultValue: defaultData.activeTickMarkColor));
properties.add(ColorProperty('inactiveTickMarkColor', inactiveTickMarkColor, defaultValue: defaultData.inactiveTickMarkColor));
properties.add(ColorProperty('disabledActiveTickMarkColor', disabledActiveTickMarkColor, defaultValue: defaultData.disabledActiveTickMarkColor));
properties.add(ColorProperty('disabledInactiveTickMarkColor', disabledInactiveTickMarkColor, defaultValue: defaultData.disabledInactiveTickMarkColor));
properties.add(ColorProperty('thumbColor', thumbColor, defaultValue: defaultData.thumbColor));
properties.add(ColorProperty('overlappingShapeStrokeColor', overlappingShapeStrokeColor, defaultValue: defaultData.overlappingShapeStrokeColor));
properties.add(ColorProperty('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor));
properties.add(ColorProperty('overlayColor', overlayColor, defaultValue: defaultData.overlayColor));
properties.add(ColorProperty('valueIndicatorColor', valueIndicatorColor, defaultValue: defaultData.valueIndicatorColor));
properties.add(DiagnosticsProperty<SliderComponentShape>('overlayShape', overlayShape, defaultValue: defaultData.overlayShape));
properties.add(DiagnosticsProperty<SliderTickMarkShape>('tickMarkShape', tickMarkShape, defaultValue: defaultData.tickMarkShape));
properties.add(DiagnosticsProperty<SliderComponentShape>('thumbShape', thumbShape, defaultValue: defaultData.thumbShape));
properties.add(DiagnosticsProperty<SliderTrackShape>('trackShape', trackShape, defaultValue: defaultData.trackShape));
properties.add(DiagnosticsProperty<SliderComponentShape>('valueIndicatorShape', valueIndicatorShape, defaultValue: defaultData.valueIndicatorShape));
properties.add(DiagnosticsProperty<RangeSliderTickMarkShape>('rangeTickMarkShape', rangeTickMarkShape, defaultValue: defaultData.rangeTickMarkShape));
properties.add(DiagnosticsProperty<RangeSliderThumbShape>('rangeThumbShape', rangeThumbShape, defaultValue: defaultData.rangeThumbShape));
properties.add(DiagnosticsProperty<RangeSliderTrackShape>('rangeTrackShape', rangeTrackShape, defaultValue: defaultData.rangeTrackShape));
properties.add(DiagnosticsProperty<RangeSliderValueIndicatorShape>('rangeValueIndicatorShape', rangeValueIndicatorShape, defaultValue: defaultData.rangeValueIndicatorShape));
properties.add(EnumProperty<ShowValueIndicator>('showValueIndicator', showValueIndicator, defaultValue: defaultData.showValueIndicator));
properties.add(DiagnosticsProperty<TextStyle>('valueIndicatorTextStyle', valueIndicatorTextStyle, defaultValue: defaultData.valueIndicatorTextStyle));
properties.add(DoubleProperty('minThumbSeparation', minThumbSeparation, defaultValue: defaultData.minThumbSeparation));
properties.add(DiagnosticsProperty<RangeThumbSelector>('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
properties.add(EnumProperty<SliderInteraction>('allowedInteraction', allowedInteraction, defaultValue: defaultData.allowedInteraction));
}
}
/// Base class for slider thumb, thumb overlay, and value indicator shapes.
///
/// Create a subclass of this if you would like a custom shape.
///
/// All shapes are painted to the same canvas and ordering is important.
/// The overlay is painted first, then the value indicator, then the thumb.
///
/// The thumb painting can be skipped by specifying [noThumb] for
/// [SliderThemeData.thumbShape].
///
/// The overlay painting can be skipped by specifying [noOverlay] for
/// [SliderThemeData.overlayShape].
///
/// See also:
///
/// * [RoundSliderThumbShape], which is the default [Slider]'s thumb shape that
/// paints a solid circle.
/// * [RoundSliderOverlayShape], which is the default [Slider] and
/// [RangeSlider]'s overlay shape that paints a transparent circle.
/// * [PaddleSliderValueIndicatorShape], which is the default [Slider]'s value
/// indicator shape that paints a custom path with text in it.
abstract class SliderComponentShape {
/// This abstract const 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.
///
/// {@template flutter.material.SliderComponentShape.paint.context}
/// The `context` argument is the same as the one that includes the [Slider]'s
/// render box.
/// {@endtemplate}
///
/// {@template flutter.material.SliderComponentShape.paint.center}
/// The `center` argument is the offset for where this shape's center should be
/// painted. This offset is relative to the origin of the [context] canvas.
/// {@endtemplate}
///
/// The `activationAnimation` argument is an animation triggered when the user
/// begins to interact with the slider. It reverses when the user stops interacting
/// with the slider.
///
/// {@template flutter.material.SliderComponentShape.paint.enableAnimation}
/// The `enableAnimation` argument is an animation triggered when the [Slider]
/// is enabled, and it reverses when the slider is disabled. The [Slider] is
/// enabled when [Slider.onChanged] is not null.Use this to paint intermediate
/// frames for this shape when the slider changes enabled state.
/// {@endtemplate}
///
/// {@template flutter.material.SliderComponentShape.paint.isDiscrete}
/// The `isDiscrete` argument is true if [Slider.divisions] is non-null. When
/// true, the slider will render tick marks on top of the track.
/// {@endtemplate}
///
/// If the `labelPainter` argument is non-null, then [TextPainter.paint]
/// should be called on the `labelPainter` with the location that the label
/// should appear. If the `labelPainter` argument is null, then no label was
/// supplied to the [Slider].
///
/// {@template flutter.material.SliderComponentShape.paint.parentBox}
/// The `parentBox` argument is the [RenderBox] of the [Slider]. Its attributes,
/// such as size, can be used to assist in painting this shape.
/// {@endtemplate}
///
/// {@template flutter.material.SliderComponentShape.paint.sliderTheme}
/// the `sliderTheme` argument is the theme assigned to the [Slider] that this
/// shape belongs to.
/// {@endtemplate}
///
/// The `textDirection` argument can be used to determine how any extra text
/// or graphics (besides the text painted by the `labelPainter`) should be
/// positioned. The `labelPainter` already has the [textDirection] set.
///
/// The `value` argument is the current parametric value (from 0.0 to 1.0) of
/// the slider.
///
/// {@template flutter.material.SliderComponentShape.paint.textScaleFactor}
/// The `textScaleFactor` argument can be used to determine whether the
/// component should paint larger or smaller, depending on whether
/// [textScaleFactor] is greater than 1 for larger, and between 0 and 1 for
/// smaller. It usually comes from [MediaQueryData.textScaleFactor].
/// {@endtemplate}
///
/// {@template flutter.material.SliderComponentShape.paint.sizeWithOverflow}
/// The `sizeWithOverflow` argument can be used to determine the bounds the
/// drawing of the components that are outside of the regular slider bounds.
/// It's the size of the box, whose center is aligned with the slider's
/// bounds, that the value indicators must be drawn within. Typically, it is
/// bigger than the slider.
/// {@endtemplate}
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
});
/// Special instance of [SliderComponentShape] to skip the thumb drawing.
///
/// See also:
///
/// * [SliderThemeData.thumbShape], which is the shape that the [Slider]
/// uses when painting the thumb.
static final SliderComponentShape noThumb = _EmptySliderComponentShape();
/// Special instance of [SliderComponentShape] to skip the overlay drawing.
///
/// See also:
///
/// * [SliderThemeData.overlayShape], which is the shape that the [Slider]
/// uses when painting the overlay.
static final SliderComponentShape noOverlay = _EmptySliderComponentShape();
}
/// Base class for [Slider] tick mark shapes.
///
/// Create a subclass of this if you would like a custom slider tick mark shape.
///
/// The tick mark painting can be skipped by specifying [noTickMark] for
/// [SliderThemeData.tickMarkShape].
///
/// See also:
///
/// * [RoundSliderTickMarkShape], which is the default [Slider]'s tick mark
/// shape that paints a solid circle.
/// * [SliderTrackShape], which can be used to create custom shapes for the
/// [Slider]'s track.
/// * [SliderComponentShape], which can be used to create custom shapes for
/// the [Slider]'s thumb, overlay, and value indicator and the
/// [RangeSlider]'s overlay.
abstract class SliderTickMarkShape {
/// This abstract const constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const SliderTickMarkShape();
/// Returns the preferred size of the shape.
///
/// It is used to help position the tick marks within the slider.
///
/// {@macro flutter.material.SliderComponentShape.paint.sliderTheme}
///
/// {@template flutter.material.SliderTickMarkShape.getPreferredSize.isEnabled}
/// The `isEnabled` argument is false when [Slider.onChanged] is null and true
/// otherwise. When true, the slider will respond to input.
/// {@endtemplate}
Size getPreferredSize({
required SliderThemeData sliderTheme,
required bool isEnabled,
});
/// Paints the slider track.
///
/// {@macro flutter.material.SliderComponentShape.paint.context}
///
/// {@macro flutter.material.SliderComponentShape.paint.center}
///
/// {@macro flutter.material.SliderComponentShape.paint.parentBox}
///
/// {@macro flutter.material.SliderComponentShape.paint.sliderTheme}
///
/// {@macro flutter.material.SliderComponentShape.paint.enableAnimation}
///
/// {@macro flutter.material.SliderTickMarkShape.getPreferredSize.isEnabled}
///
/// The `textDirection` argument can be used to determine how the tick marks
/// are painting depending on whether they are on an active track segment or
/// not. The track segment between the start of the slider and the thumb is
/// the active track segment. The track segment between the thumb and the end
/// of the slider is the inactive track segment. In LTR text direction, the
/// start of the slider is on the left, and in RTL text direction, the start
/// of the slider is on the right.
void paint(
PaintingContext context,
Offset center, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset thumbCenter,
required bool isEnabled,
required TextDirection textDirection,
});
/// Special instance of [SliderTickMarkShape] to skip the tick mark painting.
///
/// See also:
///
/// * [SliderThemeData.tickMarkShape], which is the shape that the [Slider]
/// uses when painting tick marks.
static final SliderTickMarkShape noTickMark = _EmptySliderTickMarkShape();
}
/// Base class for slider track shapes.
///
/// The slider's thumb moves along the track. A discrete slider's tick marks
/// are drawn after the track, but before the thumb, and are aligned with the
/// track.
///
/// The [getPreferredRect] helps position the slider thumb and tick marks
/// relative to the track.
///
/// See also:
///
/// * [RoundedRectSliderTrackShape] for the default [Slider]'s track shape that
/// paints a stadium-like track.
/// * [SliderTickMarkShape], which can be used to create custom shapes for the
/// [Slider]'s tick marks.
/// * [SliderComponentShape], which can be used to create custom shapes for
/// the [Slider]'s thumb, overlay, and value indicator and the
/// [RangeSlider]'s overlay.
abstract class SliderTrackShape {
/// This abstract const constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const SliderTrackShape();
/// Returns the preferred bounds of the shape.
///
/// It is used to provide horizontal boundaries for the thumb's position, and
/// to help position the slider thumb and tick marks relative to the track.
///
/// The `parentBox` argument can be used to help determine the preferredRect relative to
/// attributes of the render box of the slider itself, such as size.
///
/// The `offset` argument is relative to the caller's bounding box. It can be used to
/// convert gesture coordinates from global to slider-relative coordinates.
///
/// {@macro flutter.material.SliderComponentShape.paint.sliderTheme}
///
/// {@macro flutter.material.SliderTickMarkShape.getPreferredSize.isEnabled}
///
/// {@macro flutter.material.SliderComponentShape.paint.isDiscrete}
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled,
bool isDiscrete,
});
/// Paints the track shape based on the state passed to it.
///
/// {@macro flutter.material.SliderComponentShape.paint.context}
///
/// The `offset` argument the offset of the origin of the `parentBox` to the
/// origin of its `context` canvas. This shape must be painted relative to
/// this offset. See [PaintingContextCallback].
///
/// {@macro flutter.material.SliderComponentShape.paint.parentBox}
///
/// {@macro flutter.material.SliderComponentShape.paint.sliderTheme}
///
/// {@macro flutter.material.SliderComponentShape.paint.enableAnimation}
///
/// The `thumbCenter` argument is the offset of the center of the thumb
/// relative to the origin of the [PaintingContext.canvas]. It can be used as
/// the point that divides the track into 2 segments.
///
/// The `secondaryOffset` argument is the offset of the secondary value
/// relative to the origin of the [PaintingContext.canvas].
///
/// If not null, the track is divided into 3 segments.
///
/// {@macro flutter.material.SliderTickMarkShape.getPreferredSize.isEnabled}
///
/// {@macro flutter.material.SliderComponentShape.paint.isDiscrete}
///
/// The `textDirection` argument can be used to determine how the track
/// segments are painted depending on whether they are active or not.
///
/// {@template flutter.material.SliderTrackShape.paint.trackSegment}
/// The track segment between the start of the slider and the thumb is the
/// active track segment. The track segment between the thumb and the end of the
/// slider is the inactive track segment. In [TextDirection.ltr], the start of
/// the slider is on the left, and in [TextDirection.rtl], the start of the
/// slider is on the right.
/// {@endtemplate}
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset thumbCenter,
Offset? secondaryOffset,
bool isEnabled,
bool isDiscrete,
required TextDirection textDirection,
});
}
/// Base class for [RangeSlider] thumb shapes.
///
/// See also:
///
/// * [RoundRangeSliderThumbShape] for the default [RangeSlider]'s thumb shape
/// that paints a solid circle.
/// * [RangeSliderTickMarkShape], which can be used to create custom shapes for
/// the [RangeSlider]'s tick marks.
/// * [RangeSliderTrackShape], which can be used to create custom shapes for
/// the [RangeSlider]'s track.
/// * [RangeSliderValueIndicatorShape], which can be used to create custom
/// shapes for the [RangeSlider]'s value indicator.
/// * [SliderComponentShape], which can be used to create custom shapes for
/// the [Slider]'s thumb, overlay, and value indicator and the
/// [RangeSlider]'s overlay.
abstract class RangeSliderThumbShape {
/// This abstract const constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const RangeSliderThumbShape();
/// Returns the preferred size of the shape, based on the given conditions.
///
/// {@template flutter.material.RangeSliderThumbShape.getPreferredSize.isDiscrete}
/// The `isDiscrete` argument is true if [RangeSlider.divisions] is non-null.
/// When true, the slider will render tick marks on top of the track.
/// {@endtemplate}
///
/// {@template flutter.material.RangeSliderThumbShape.getPreferredSize.isEnabled}
/// The `isEnabled` argument is false when [RangeSlider.onChanged] is null and
/// true otherwise. When true, the slider will respond to input.
/// {@endtemplate}
Size getPreferredSize(bool isEnabled, bool isDiscrete);
/// Paints the thumb shape based on the state passed to it.
///
/// {@template flutter.material.RangeSliderThumbShape.paint.context}
/// The `context` argument represents the [RangeSlider]'s render box.
/// {@endtemplate}
///
/// {@macro flutter.material.SliderComponentShape.paint.center}
///
/// {@template flutter.material.RangeSliderThumbShape.paint.activationAnimation}
/// The `activationAnimation` argument is an animation triggered when the user
/// begins to interact with the [RangeSlider]. It reverses when the user stops
/// interacting with the slider.
/// {@endtemplate}
///
/// {@template flutter.material.RangeSliderThumbShape.paint.enableAnimation}
/// The `enableAnimation` argument is an animation triggered when the
/// [RangeSlider] is enabled, and it reverses when the slider is disabled. The
/// [RangeSlider] is enabled when [RangeSlider.onChanged] is not null. Use
/// this to paint intermediate frames for this shape when the slider changes
/// enabled state.
/// {@endtemplate}
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isDiscrete}
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isEnabled}
///
/// If the `isOnTop` argument is true, this thumb is painted on top of the
/// other slider thumb because this thumb is the one that was most recently
/// selected.
///
/// {@template flutter.material.RangeSliderThumbShape.paint.sliderTheme}
/// The `sliderTheme` argument is the theme assigned to the [RangeSlider] that
/// this shape belongs to.
/// {@endtemplate}
///
/// The `textDirection` argument can be used to determine how the orientation
/// of either slider thumb should be changed, such as drawing different
/// shapes for the left and right thumb.
///
/// {@template flutter.material.RangeSliderThumbShape.paint.thumb}
/// The `thumb` argument is the specifier for which of the two thumbs this
/// method should paint (start or end).
/// {@endtemplate}
///
/// The `isPressed` argument can be used to give the selected thumb
/// additional selected or pressed state visual feedback, such as a larger
/// shadow.
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
bool isDiscrete,
bool isEnabled,
bool isOnTop,
TextDirection textDirection,
required SliderThemeData sliderTheme,
Thumb thumb,
bool isPressed,
});
}
/// Base class for [RangeSlider] value indicator shapes.
///
/// See also:
///
/// * [PaddleRangeSliderValueIndicatorShape] for the default [RangeSlider]'s
/// value indicator shape that paints a custom path with text in it.
/// * [RangeSliderTickMarkShape], which can be used to create custom shapes for
/// the [RangeSlider]'s tick marks.
/// * [RangeSliderThumbShape], which can be used to create custom shapes for
/// the [RangeSlider]'s thumb.
/// * [RangeSliderTrackShape], which can be used to create custom shapes for
/// the [RangeSlider]'s track.
/// * [SliderComponentShape], which can be used to create custom shapes for
/// the [Slider]'s thumb, overlay, and value indicator and the
/// [RangeSlider]'s overlay.
abstract class RangeSliderValueIndicatorShape {
/// This abstract const constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const RangeSliderValueIndicatorShape();
/// Returns the preferred size of the shape, based on the given conditions.
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isEnabled}
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isDiscrete}
///
/// The `labelPainter` argument helps determine the width of the shape. It is
/// variable width because it is derived from a formatted string.
///
/// {@macro flutter.material.SliderComponentShape.paint.textScaleFactor}
Size getPreferredSize(
bool isEnabled,
bool isDiscrete, {
required TextPainter labelPainter,
required double textScaleFactor,
});
/// Determines the best offset to keep this shape on the screen.
///
/// Override this method when the center of the value indicator should be
/// shifted from the vertical center of the thumb.
double getHorizontalShift({
RenderBox? parentBox,
Offset? center,
TextPainter? labelPainter,
Animation<double>? activationAnimation,
double? textScaleFactor,
Size? sizeWithOverflow,
}) {
return 0;
}
/// Paints the value indicator shape based on the state passed to it.
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.context}
///
/// {@macro flutter.material.SliderComponentShape.paint.center}
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.activationAnimation}
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.enableAnimation}
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isDiscrete}
///
/// The `isOnTop` argument is the top-most value indicator between the two value
/// indicators, which is always the indicator for the most recently selected thumb. In
/// the default case, this is used to paint a stroke around the top indicator
/// for better visibility between the two indicators.
///
/// {@macro flutter.material.SliderComponentShape.paint.textScaleFactor}
///
/// {@macro flutter.material.SliderComponentShape.paint.sizeWithOverflow}
///
/// {@template flutter.material.RangeSliderValueIndicatorShape.paint.parentBox}
/// The `parentBox` argument is the [RenderBox] of the [RangeSlider]. Its
/// attributes, such as size, can be used to assist in painting this shape.
/// {@endtemplate}
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.sliderTheme}
///
/// The `textDirection` argument can be used to determine how any extra text
/// or graphics, besides the text painted by the [labelPainter] should be
/// positioned. The `labelPainter` argument already has the `textDirection`
/// set.
///
/// The `value` argument is the current parametric value (from 0.0 to 1.0) of
/// the slider.
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.thumb}
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
bool isDiscrete,
bool isOnTop,
required TextPainter labelPainter,
double textScaleFactor,
Size sizeWithOverflow,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
Thumb thumb,
});
}
/// Base class for [RangeSlider] tick mark shapes.
///
/// This is a simplified version of [SliderComponentShape] with a
/// [SliderThemeData] passed when getting the preferred size.
///
/// See also:
///
/// * [RoundRangeSliderTickMarkShape] for the default [RangeSlider]'s tick mark
/// shape that paints a solid circle.
/// * [RangeSliderThumbShape], which can be used to create custom shapes for
/// the [RangeSlider]'s thumb.
/// * [RangeSliderTrackShape], which can be used to create custom shapes for
/// the [RangeSlider]'s track.
/// * [RangeSliderValueIndicatorShape], which can be used to create custom
/// shapes for the [RangeSlider]'s value indicator.
/// * [SliderComponentShape], which can be used to create custom shapes for
/// the [Slider]'s thumb, overlay, and value indicator and the
/// [RangeSlider]'s overlay.
abstract class RangeSliderTickMarkShape {
/// This abstract const constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const RangeSliderTickMarkShape();
/// Returns the preferred size of the shape.
///
/// It is used to help position the tick marks within the slider.
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.sliderTheme}
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isEnabled}
Size getPreferredSize({
required SliderThemeData sliderTheme,
bool isEnabled,
});
/// Paints the slider track.
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.context}
///
/// {@macro flutter.material.SliderComponentShape.paint.center}
///
/// {@macro flutter.material.RangeSliderValueIndicatorShape.paint.parentBox}
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.sliderTheme}
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.enableAnimation}
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isEnabled}
///
/// The `textDirection` argument can be used to determine how the tick marks
/// are painted depending on whether they are on an active track segment or not.
///
/// {@template flutter.material.RangeSliderTickMarkShape.paint.trackSegment}
/// The track segment between the two thumbs is the active track segment. The
/// track segments between the thumb and each end of the slider are the inactive
/// track segments. In [TextDirection.ltr], the start of the slider is on the
/// left, and in [TextDirection.rtl], the start of the slider is on the right.
/// {@endtemplate}
void paint(
PaintingContext context,
Offset center, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset startThumbCenter,
required Offset endThumbCenter,
bool isEnabled,
required TextDirection textDirection,
});
}
/// Base class for [RangeSlider] track shapes.
///
/// The slider's thumbs move along the track. A discrete slider's tick marks
/// are drawn after the track, but before the thumb, and are aligned with the
/// track.
///
/// The [getPreferredRect] helps position the slider thumbs and tick marks
/// relative to the track.
///
/// See also:
///
/// * [RoundedRectRangeSliderTrackShape] for the default [RangeSlider]'s track
/// shape that paints a stadium-like track.
/// * [RangeSliderTickMarkShape], which can be used to create custom shapes for
/// the [RangeSlider]'s tick marks.
/// * [RangeSliderThumbShape], which can be used to create custom shapes for
/// the [RangeSlider]'s thumb.
/// * [RangeSliderValueIndicatorShape], which can be used to create custom
/// shapes for the [RangeSlider]'s value indicator.
/// * [SliderComponentShape], which can be used to create custom shapes for
/// the [Slider]'s thumb, overlay, and value indicator and the
/// [RangeSlider]'s overlay.
abstract class RangeSliderTrackShape {
/// This abstract const constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const RangeSliderTrackShape();
/// Returns the preferred bounds of the shape.
///
/// It is used to provide horizontal boundaries for the position of the
/// thumbs, and to help position the slider thumbs and tick marks relative to
/// the track.
///
/// The `parentBox` argument can be used to help determine the preferredRect
/// relative to attributes of the render box of the slider itself, such as
/// size.
///
/// The `offset` argument is relative to the caller's bounding box. It can be
/// used to convert gesture coordinates from global to slider-relative
/// coordinates.
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.sliderTheme}
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isEnabled}
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isDiscrete}
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled,
bool isDiscrete,
});
/// Paints the track shape based on the state passed to it.
///
/// {@macro flutter.material.SliderComponentShape.paint.context}
///
/// The `offset` argument is the offset of the origin of the `parentBox` to
/// the origin of its `context` canvas. This shape must be painted relative
/// to this offset. See [PaintingContextCallback].
///
/// {@macro flutter.material.RangeSliderValueIndicatorShape.paint.parentBox}
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.sliderTheme}
///
/// {@macro flutter.material.RangeSliderThumbShape.paint.enableAnimation}
///
/// The `startThumbCenter` argument is the offset of the center of the start
/// thumb relative to the origin of the [PaintingContext.canvas]. It can be
/// used as one point that divides the track between inactive and active.
///
/// The `endThumbCenter` argument is the offset of the center of the end
/// thumb relative to the origin of the [PaintingContext.canvas]. It can be
/// used as one point that divides the track between inactive and active.
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isEnabled}
///
/// {@macro flutter.material.RangeSliderThumbShape.getPreferredSize.isDiscrete}
///
/// The `textDirection` argument can be used to determine how the track
/// segments are painted depending on whether they are on an active track
/// segment or not.
///
/// {@macro flutter.material.RangeSliderTickMarkShape.paint.trackSegment}
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset startThumbCenter,
required Offset endThumbCenter,
bool isEnabled = false,
bool isDiscrete = false,
required TextDirection textDirection,
});
}
/// Base track shape that provides an implementation of [getPreferredRect] for
/// default sizing.
///
/// The height is set from [SliderThemeData.trackHeight] and the width of the
/// parent box less the larger of the widths of [SliderThemeData.thumbShape] and
/// [SliderThemeData.overlayShape].
///
/// See also:
///
/// * [RectangularSliderTrackShape], which is a track shape with sharp
/// rectangular edges
/// * [RoundedRectSliderTrackShape], which is a track shape with round
/// stadium-like edges.
mixin BaseSliderTrackShape {
/// Returns a rect that represents the track bounds that fits within the
/// [Slider].
///
/// The width is the width of the [Slider] or [RangeSlider], but padded by
/// the max of the overlay and thumb radius. The height is defined by the
/// [SliderThemeData.trackHeight].
///
/// The [Rect] is centered both horizontally and vertically within the slider
/// bounds.
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
final double thumbWidth = sliderTheme.thumbShape!.getPreferredSize(isEnabled, isDiscrete).width;
final double overlayWidth = sliderTheme.overlayShape!.getPreferredSize(isEnabled, isDiscrete).width;
final double trackHeight = sliderTheme.trackHeight!;
assert(overlayWidth >= 0);
assert(trackHeight >= 0);
final double trackLeft = offset.dx + math.max(overlayWidth / 2, thumbWidth / 2);
final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
final double trackRight = trackLeft + parentBox.size.width - math.max(thumbWidth, overlayWidth);
final double trackBottom = trackTop + trackHeight;
// If the parentBox'size less than slider's size the trackRight will be less than trackLeft, so switch them.
return Rect.fromLTRB(math.min(trackLeft, trackRight), trackTop, math.max(trackLeft, trackRight), trackBottom);
}
}
/// A [Slider] track that's a simple rectangle.
///
/// It paints a solid colored rectangle, vertically centered in the
/// `parentBox`. The track rectangle extends to the bounds of the `parentBox`,
/// but is padded by the [RoundSliderOverlayShape] radius. The height is defined
/// by the [SliderThemeData.trackHeight]. The color is determined by the
/// [Slider]'s enabled state and the track segment's active state which are
/// defined by:
/// [SliderThemeData.activeTrackColor],
/// [SliderThemeData.inactiveTrackColor],
/// [SliderThemeData.disabledActiveTrackColor],
/// [SliderThemeData.disabledInactiveTrackColor].
///
/// {@macro flutter.material.SliderTrackShape.paint.trackSegment}
///
/// ![A slider widget, consisting of 5 divisions and showing the rectangular slider track shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/rectangular_slider_track_shape.png)
///
/// See also:
///
/// * [Slider], for the component that is meant to display this shape.
/// * [SliderThemeData], where an instance of this class is set to inform the
/// slider of the visual details of the its track.
/// * [SliderTrackShape], which can be used to create custom shapes for the
/// [Slider]'s track.
/// * [RoundedRectSliderTrackShape], for a similar track with rounded edges.
class RectangularSliderTrackShape extends SliderTrackShape with BaseSliderTrackShape {
/// Creates a slider track that draws 2 rectangles.
const RectangularSliderTrackShape();
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
Offset? secondaryOffset,
bool isDiscrete = false,
bool isEnabled = false,
}) {
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.thumbShape != null);
// If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// then it makes no difference whether the track is painted or not,
// therefore the painting can be a no-op.
if (sliderTheme.trackHeight! <= 0) {
return;
}
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation)!;
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
final Paint leftTrackPaint;
final Paint rightTrackPaint;
switch (textDirection) {
case TextDirection.ltr:
leftTrackPaint = activePaint;
rightTrackPaint = inactivePaint;
case TextDirection.rtl:
leftTrackPaint = inactivePaint;
rightTrackPaint = activePaint;
}
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left, trackRect.top, thumbCenter.dx, trackRect.bottom);
if (!leftTrackSegment.isEmpty) {
context.canvas.drawRect(leftTrackSegment, leftTrackPaint);
}
final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx, trackRect.top, trackRect.right, trackRect.bottom);
if (!rightTrackSegment.isEmpty) {
context.canvas.drawRect(rightTrackSegment, rightTrackPaint);
}
final bool showSecondaryTrack = (secondaryOffset != null) &&
((textDirection == TextDirection.ltr)
? (secondaryOffset.dx > thumbCenter.dx)
: (secondaryOffset.dx < thumbCenter.dx));
if (showSecondaryTrack) {
final ColorTween secondaryTrackColorTween = ColorTween(begin: sliderTheme.disabledSecondaryActiveTrackColor, end: sliderTheme.secondaryActiveTrackColor);
final Paint secondaryTrackPaint = Paint()..color = secondaryTrackColorTween.evaluate(enableAnimation)!;
final Rect secondaryTrackSegment = Rect.fromLTRB(
(textDirection == TextDirection.ltr) ? thumbCenter.dx : secondaryOffset.dx,
trackRect.top,
(textDirection == TextDirection.ltr) ? secondaryOffset.dx : thumbCenter.dx,
trackRect.bottom,
);
if (!secondaryTrackSegment.isEmpty) {
context.canvas.drawRect(secondaryTrackSegment, secondaryTrackPaint);
}
}
}
}
/// The default shape of a [Slider]'s track.
///
/// It paints a solid colored rectangle with rounded edges, vertically centered
/// in the `parentBox`. The track rectangle extends to the bounds of the
/// `parentBox`, but is padded by the larger of [RoundSliderOverlayShape]'s
/// radius and [RoundSliderThumbShape]'s radius. The height is defined by the
/// [SliderThemeData.trackHeight]. The color is determined by the [Slider]'s
/// enabled state and the track segment's active state which are defined by:
/// [SliderThemeData.activeTrackColor],
/// [SliderThemeData.inactiveTrackColor],
/// [SliderThemeData.disabledActiveTrackColor],
/// [SliderThemeData.disabledInactiveTrackColor].
///
/// {@macro flutter.material.SliderTrackShape.paint.trackSegment}
///
/// ![A slider widget, consisting of 5 divisions and showing the rounded rect slider track shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/rounded_rect_slider_track_shape.png)
///
/// See also:
///
/// * [Slider], for the component that is meant to display this shape.
/// * [SliderThemeData], where an instance of this class is set to inform the
/// slider of the visual details of the its track.
/// * [SliderTrackShape], which can be used to create custom shapes for the
/// [Slider]'s track.
/// * [RectangularSliderTrackShape], for a similar track with sharp edges.
class RoundedRectSliderTrackShape extends SliderTrackShape with BaseSliderTrackShape {
/// Create a slider track that draws two rectangles with rounded outer edges.
const RoundedRectSliderTrackShape();
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
Offset? secondaryOffset,
bool isDiscrete = false,
bool isEnabled = false,
double additionalActiveTrackHeight = 2,
}) {
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.thumbShape != null);
// If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// then it makes no difference whether the track is painted or not,
// therefore the painting can be a no-op.
if (sliderTheme.trackHeight == null || sliderTheme.trackHeight! <= 0) {
return;
}
// Assign the track segment paints, which are leading: active and
// trailing: inactive.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation)!;
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
final Paint leftTrackPaint;
final Paint rightTrackPaint;
switch (textDirection) {
case TextDirection.ltr:
leftTrackPaint = activePaint;
rightTrackPaint = inactivePaint;
case TextDirection.rtl:
leftTrackPaint = inactivePaint;
rightTrackPaint = activePaint;
}
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final Radius trackRadius = Radius.circular(trackRect.height / 2);
final Radius activeTrackRadius = Radius.circular((trackRect.height + additionalActiveTrackHeight) / 2);
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
trackRect.left,
(textDirection == TextDirection.ltr) ? trackRect.top - (additionalActiveTrackHeight / 2): trackRect.top,
thumbCenter.dx,
(textDirection == TextDirection.ltr) ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom,
topLeft: (textDirection == TextDirection.ltr) ? activeTrackRadius : trackRadius,
bottomLeft: (textDirection == TextDirection.ltr) ? activeTrackRadius: trackRadius,
),
leftTrackPaint,
);
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
thumbCenter.dx,
(textDirection == TextDirection.rtl) ? trackRect.top - (additionalActiveTrackHeight / 2) : trackRect.top,
trackRect.right,
(textDirection == TextDirection.rtl) ? trackRect.bottom + (additionalActiveTrackHeight / 2) : trackRect.bottom,
topRight: (textDirection == TextDirection.rtl) ? activeTrackRadius : trackRadius,
bottomRight: (textDirection == TextDirection.rtl) ? activeTrackRadius : trackRadius,
),
rightTrackPaint,
);
final bool showSecondaryTrack = (secondaryOffset != null) &&
((textDirection == TextDirection.ltr)
? (secondaryOffset.dx > thumbCenter.dx)
: (secondaryOffset.dx < thumbCenter.dx));
if (showSecondaryTrack) {
final ColorTween secondaryTrackColorTween = ColorTween(begin: sliderTheme.disabledSecondaryActiveTrackColor, end: sliderTheme.secondaryActiveTrackColor);
final Paint secondaryTrackPaint = Paint()..color = secondaryTrackColorTween.evaluate(enableAnimation)!;
if (textDirection == TextDirection.ltr) {
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
thumbCenter.dx,
trackRect.top,
secondaryOffset.dx,
trackRect.bottom,
topRight: trackRadius,
bottomRight: trackRadius,
),
secondaryTrackPaint,
);
} else {
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
secondaryOffset.dx,
trackRect.top,
thumbCenter.dx,
trackRect.bottom,
topLeft: trackRadius,
bottomLeft: trackRadius,
),
secondaryTrackPaint,
);
}
}
}
}
/// Base range slider track shape that provides an implementation of [getPreferredRect] for
/// default sizing.
///
/// The height is set from [SliderThemeData.trackHeight] and the width of the
/// parent box less the larger of the widths of [SliderThemeData.rangeThumbShape] and
/// [SliderThemeData.overlayShape].
///
/// See also:
///
/// * [RectangularRangeSliderTrackShape], which is a track shape with sharp
/// rectangular edges
mixin BaseRangeSliderTrackShape {
/// Returns a rect that represents the track bounds that fits within the
/// [Slider].
///
/// The width is the width of the [RangeSlider], but padded by the max
/// of the overlay and thumb radius. The height is defined by the [SliderThemeData.trackHeight].
///
/// The [Rect] is centered both horizontally and vertically within the slider
/// bounds.
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = false,
bool isDiscrete = false,
}) {
assert(sliderTheme.rangeThumbShape != null);
assert(sliderTheme.overlayShape != null);
assert(sliderTheme.trackHeight != null);
final double thumbWidth = sliderTheme.rangeThumbShape!.getPreferredSize(isEnabled, isDiscrete).width;
final double overlayWidth = sliderTheme.overlayShape!.getPreferredSize(isEnabled, isDiscrete).width;
final double trackHeight = sliderTheme.trackHeight!;
assert(overlayWidth >= 0);
assert(trackHeight >= 0);
final double trackLeft = offset.dx + math.max(overlayWidth / 2, thumbWidth / 2);
final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
final double trackRight = trackLeft + parentBox.size.width - math.max(thumbWidth, overlayWidth);
final double trackBottom = trackTop + trackHeight;
// If the parentBox'size less than slider's size the trackRight will be less than trackLeft, so switch them.
return Rect.fromLTRB(math.min(trackLeft, trackRight), trackTop, math.max(trackLeft, trackRight), trackBottom);
}
}
/// A [RangeSlider] track that's a simple rectangle.
///
/// It paints a solid colored rectangle, vertically centered in the
/// `parentBox`. The track rectangle extends to the bounds of the `parentBox`,
/// but is padded by the [RoundSliderOverlayShape] radius. The height is
/// defined by the [SliderThemeData.trackHeight]. The color is determined by the
/// [Slider]'s enabled state and the track segment's active state which are
/// defined by:
/// [SliderThemeData.activeTrackColor],
/// [SliderThemeData.inactiveTrackColor],
/// [SliderThemeData.disabledActiveTrackColor],
/// [SliderThemeData.disabledInactiveTrackColor].
///
/// {@macro flutter.material.RangeSliderTickMarkShape.paint.trackSegment}
///
/// ![A range slider widget, consisting of 5 divisions and showing the rectangular range slider track shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/rectangular_range_slider_track_shape.png)
///
/// See also:
///
/// * [RangeSlider], for the component that is meant to display this shape.
/// * [SliderThemeData], where an instance of this class is set to inform the
/// slider of the visual details of the its track.
/// * [RangeSliderTrackShape], which can be used to create custom shapes for
/// the [RangeSlider]'s track.
/// * [RoundedRectRangeSliderTrackShape], for a similar track with rounded
/// edges.
class RectangularRangeSliderTrackShape extends RangeSliderTrackShape with BaseRangeSliderTrackShape {
/// Create a slider track with rectangular outer edges.
///
/// The middle track segment is the selected range and is active, and the two
/// outer track segments are inactive.
const RectangularRangeSliderTrackShape();
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double>? enableAnimation,
required Offset startThumbCenter,
required Offset endThumbCenter,
bool isEnabled = false,
bool isDiscrete = false,
required TextDirection textDirection,
}) {
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.rangeThumbShape != null);
assert(enableAnimation != null);
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation!)!;
final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
final Offset leftThumbOffset;
final Offset rightThumbOffset;
switch (textDirection) {
case TextDirection.ltr:
leftThumbOffset = startThumbCenter;
rightThumbOffset = endThumbCenter;
case TextDirection.rtl:
leftThumbOffset = endThumbCenter;
rightThumbOffset = startThumbCenter;
}
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left, trackRect.top, leftThumbOffset.dx, trackRect.bottom);
if (!leftTrackSegment.isEmpty) {
context.canvas.drawRect(leftTrackSegment, inactivePaint);
}
final Rect middleTrackSegment = Rect.fromLTRB(leftThumbOffset.dx, trackRect.top, rightThumbOffset.dx, trackRect.bottom);
if (!middleTrackSegment.isEmpty) {
context.canvas.drawRect(middleTrackSegment, activePaint);
}
final Rect rightTrackSegment = Rect.fromLTRB(rightThumbOffset.dx, trackRect.top, trackRect.right, trackRect.bottom);
if (!rightTrackSegment.isEmpty) {
context.canvas.drawRect(rightTrackSegment, inactivePaint);
}
}
}
/// The default shape of a [RangeSlider]'s track.
///
/// It paints a solid colored rectangle with rounded edges, vertically centered
/// in the `parentBox`. The track rectangle extends to the bounds of the
/// `parentBox`, but is padded by the larger of [RoundSliderOverlayShape]'s
/// radius and [RoundRangeSliderThumbShape]'s radius. The height is defined by
/// the [SliderThemeData.trackHeight]. The color is determined by the
/// [RangeSlider]'s enabled state and the track segment's active state which are
/// defined by:
/// [SliderThemeData.activeTrackColor],
/// [SliderThemeData.inactiveTrackColor],
/// [SliderThemeData.disabledActiveTrackColor],
/// [SliderThemeData.disabledInactiveTrackColor].
///
/// {@macro flutter.material.RangeSliderTickMarkShape.paint.trackSegment}
///
/// ![A range slider widget, consisting of 5 divisions and showing the rounded rect range slider track shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/rounded_rect_range_slider_track_shape.png)
///
/// See also:
///
/// * [RangeSlider], for the component that is meant to display this shape.
/// * [SliderThemeData], where an instance of this class is set to inform the
/// slider of the visual details of the its track.
/// * [RangeSliderTrackShape], which can be used to create custom shapes for
/// the [RangeSlider]'s track.
/// * [RectangularRangeSliderTrackShape], for a similar track with sharp edges.
class RoundedRectRangeSliderTrackShape extends RangeSliderTrackShape with BaseRangeSliderTrackShape {
/// Create a slider track with rounded outer edges.
///
/// The middle track segment is the selected range and is active, and the two
/// outer track segments are inactive.
const RoundedRectRangeSliderTrackShape();
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset startThumbCenter,
required Offset endThumbCenter,
bool isEnabled = false,
bool isDiscrete = false,
required TextDirection textDirection,
double additionalActiveTrackHeight = 2,
}) {
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.rangeThumbShape != null);
if (sliderTheme.trackHeight == null || sliderTheme.trackHeight! <= 0) {
return;
}
// Assign the track segment paints, which are left: active, right: inactive,
// but reversed for right to left text.
final ColorTween activeTrackColorTween = ColorTween(
begin: sliderTheme.disabledActiveTrackColor,
end: sliderTheme.activeTrackColor,
);
final ColorTween inactiveTrackColorTween = ColorTween(
begin: sliderTheme.disabledInactiveTrackColor,
end: sliderTheme.inactiveTrackColor,
);
final Paint activePaint = Paint()
..color = activeTrackColorTween.evaluate(enableAnimation)!;
final Paint inactivePaint = Paint()
..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
final Offset leftThumbOffset;
final Offset rightThumbOffset;
switch (textDirection) {
case TextDirection.ltr:
leftThumbOffset = startThumbCenter;
rightThumbOffset = endThumbCenter;
case TextDirection.rtl:
leftThumbOffset = endThumbCenter;
rightThumbOffset = startThumbCenter;
}
final Size thumbSize = sliderTheme.rangeThumbShape!.getPreferredSize(isEnabled, isDiscrete);
final double thumbRadius = thumbSize.width / 2;
assert(thumbRadius > 0);
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final Radius trackRadius = Radius.circular(trackRect.height / 2);
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
trackRect.left,
trackRect.top,
leftThumbOffset.dx,
trackRect.bottom,
topLeft: trackRadius,
bottomLeft: trackRadius,
),
inactivePaint,
);
context.canvas.drawRect(
Rect.fromLTRB(
leftThumbOffset.dx,
trackRect.top - (additionalActiveTrackHeight / 2),
rightThumbOffset.dx,
trackRect.bottom + (additionalActiveTrackHeight / 2),
),
activePaint,
);
context.canvas.drawRRect(
RRect.fromLTRBAndCorners(
rightThumbOffset.dx,
trackRect.top,
trackRect.right,
trackRect.bottom,
topRight: trackRadius,
bottomRight: trackRadius,
),
inactivePaint,
);
}
}
/// The default shape of each [Slider] tick mark.
///
/// Tick marks are only displayed if the slider is discrete, which can be done
/// by setting the [Slider.divisions] to an integer value.
///
/// It paints a solid circle, centered in the on the track.
/// The color is determined by the [Slider]'s enabled state and track's active
/// states. These colors are defined in:
/// [SliderThemeData.activeTrackColor],
/// [SliderThemeData.inactiveTrackColor],
/// [SliderThemeData.disabledActiveTrackColor],
/// [SliderThemeData.disabledInactiveTrackColor].
///
/// ![A slider widget, consisting of 5 divisions and showing the round slider tick mark shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/rounded_slider_tick_mark_shape.png)
///
/// See also:
///
/// * [Slider], which includes tick marks defined by this shape.
/// * [SliderTheme], which can be used to configure the tick mark shape of all
/// sliders in a widget subtree.
class RoundSliderTickMarkShape extends SliderTickMarkShape {
/// Create a slider tick mark that draws a circle.
const RoundSliderTickMarkShape({
this.tickMarkRadius,
});
/// The preferred radius of the round tick mark.
///
/// If it is not provided, then 1/4 of the [SliderThemeData.trackHeight] is used.
final double? tickMarkRadius;
@override
Size getPreferredSize({
required SliderThemeData sliderTheme,
required bool isEnabled,
}) {
assert(sliderTheme.trackHeight != null);
// The tick marks are tiny circles. If no radius is provided, then the
// radius is defaulted to be a fraction of the
// [SliderThemeData.trackHeight]. The fraction is 1/4.
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight! / 4);
}
@override
void paint(
PaintingContext context,
Offset center, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
required bool isEnabled,
}) {
assert(sliderTheme.disabledActiveTickMarkColor != null);
assert(sliderTheme.disabledInactiveTickMarkColor != null);
assert(sliderTheme.activeTickMarkColor != null);
assert(sliderTheme.inactiveTickMarkColor != null);
// The paint color of the tick mark depends on its position relative
// to the thumb and the text direction.
Color? begin;
Color? end;
switch (textDirection) {
case TextDirection.ltr:
final bool isTickMarkRightOfThumb = center.dx > thumbCenter.dx;
begin = isTickMarkRightOfThumb ? sliderTheme.disabledInactiveTickMarkColor : sliderTheme.disabledActiveTickMarkColor;
end = isTickMarkRightOfThumb ? sliderTheme.inactiveTickMarkColor : sliderTheme.activeTickMarkColor;
case TextDirection.rtl:
final bool isTickMarkLeftOfThumb = center.dx < thumbCenter.dx;
begin = isTickMarkLeftOfThumb ? sliderTheme.disabledInactiveTickMarkColor : sliderTheme.disabledActiveTickMarkColor;
end = isTickMarkLeftOfThumb ? sliderTheme.inactiveTickMarkColor : sliderTheme.activeTickMarkColor;
}
final Paint paint = Paint()..color = ColorTween(begin: begin, end: end).evaluate(enableAnimation)!;
// The tick marks are tiny circles that are the same height as the track.
final double tickMarkRadius = getPreferredSize(
isEnabled: isEnabled,
sliderTheme: sliderTheme,
).width / 2;
if (tickMarkRadius > 0) {
context.canvas.drawCircle(center, tickMarkRadius, paint);
}
}
}
/// The default shape of each [RangeSlider] tick mark.
///
/// Tick marks are only displayed if the slider is discrete, which can be done
/// by setting the [RangeSlider.divisions] to an integer value.
///
/// It paints a solid circle, centered on the track.
/// The color is determined by the [Slider]'s enabled state and track's active
/// states. These colors are defined in:
/// [SliderThemeData.activeTrackColor],
/// [SliderThemeData.inactiveTrackColor],
/// [SliderThemeData.disabledActiveTrackColor],
/// [SliderThemeData.disabledInactiveTrackColor].
///
/// ![A slider widget, consisting of 5 divisions and showing the round range slider tick mark shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/round_range_slider_tick_mark_shape.png )
///
/// See also:
///
/// * [RangeSlider], which includes tick marks defined by this shape.
/// * [SliderTheme], which can be used to configure the tick mark shape of all
/// sliders in a widget subtree.
class RoundRangeSliderTickMarkShape extends RangeSliderTickMarkShape {
/// Create a range slider tick mark that draws a circle.
const RoundRangeSliderTickMarkShape({
this.tickMarkRadius,
});
/// The preferred radius of the round tick mark.
///
/// If it is not provided, then 1/4 of the [SliderThemeData.trackHeight] is used.
final double? tickMarkRadius;
@override
Size getPreferredSize({
required SliderThemeData sliderTheme,
bool isEnabled = false,
}) {
assert(sliderTheme.trackHeight != null);
return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight! / 4);
}
@override
void paint(
PaintingContext context,
Offset center, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset startThumbCenter,
required Offset endThumbCenter,
bool isEnabled = false,
required TextDirection textDirection,
}) {
assert(sliderTheme.disabledActiveTickMarkColor != null);
assert(sliderTheme.disabledInactiveTickMarkColor != null);
assert(sliderTheme.activeTickMarkColor != null);
assert(sliderTheme.inactiveTickMarkColor != null);
final bool isBetweenThumbs;
switch (textDirection) {
case TextDirection.ltr:
isBetweenThumbs = startThumbCenter.dx < center.dx && center.dx < endThumbCenter.dx;
case TextDirection.rtl:
isBetweenThumbs = endThumbCenter.dx < center.dx && center.dx < startThumbCenter.dx;
}
final Color? begin = isBetweenThumbs ? sliderTheme.disabledActiveTickMarkColor : sliderTheme.disabledInactiveTickMarkColor;
final Color? end = isBetweenThumbs ? sliderTheme.activeTickMarkColor : sliderTheme.inactiveTickMarkColor;
final Paint paint = Paint()..color = ColorTween(begin: begin, end: end).evaluate(enableAnimation)!;
// The tick marks are tiny circles that are the same height as the track.
final double tickMarkRadius = getPreferredSize(
isEnabled: isEnabled,
sliderTheme: sliderTheme,
).width / 2;
if (tickMarkRadius > 0) {
context.canvas.drawCircle(center, tickMarkRadius, paint);
}
}
}
/// A special version of [SliderTickMarkShape] that has a zero size and paints
/// nothing.
///
/// This class is used to create a special instance of a [SliderTickMarkShape]
/// that will not paint any tick mark shape. A static reference is stored in
/// [SliderTickMarkShape.noTickMark]. When this value is specified for
/// [SliderThemeData.tickMarkShape], the tick mark painting is skipped.
class _EmptySliderTickMarkShape extends SliderTickMarkShape {
@override
Size getPreferredSize({
required SliderThemeData sliderTheme,
required bool isEnabled,
}) {
return Size.zero;
}
@override
void paint(
PaintingContext context,
Offset center, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required Offset thumbCenter,
required bool isEnabled,
required TextDirection textDirection,
}) {
// no-op.
}
}
/// A special version of [SliderComponentShape] that has a zero size and paints
/// nothing.
///
/// This class is used to create a special instance of a [SliderComponentShape]
/// that will not paint any component shape. A static reference is stored in
/// [SliderTickMarkShape.noThumb] and [SliderTickMarkShape.noOverlay]. When this value
/// is specified for [SliderThemeData.thumbShape], the thumb painting is
/// skipped. When this value is specified for [SliderThemeData.overlayShape],
/// the overlay painting is skipped.
class _EmptySliderComponentShape extends SliderComponentShape {
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero;
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
// no-op.
}
}
/// The default shape of a [Slider]'s thumb.
///
/// There is a shadow for the resting, pressed, hovered, and focused state.
///
/// ![A slider widget, consisting of 5 divisions and showing the round slider thumb shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/round_slider_thumb_shape.png)
///
/// See also:
///
/// * [Slider], which includes a thumb defined by this shape.
/// * [SliderTheme], which can be used to configure the thumb shape of all
/// sliders in a widget subtree.
class RoundSliderThumbShape extends SliderComponentShape {
/// Create a slider thumb that draws a circle.
const RoundSliderThumbShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
});
/// The preferred radius of the round thumb shape when the slider is enabled.
///
/// If it is not provided, then the Material Design default of 10 is used.
final double enabledThumbRadius;
/// The preferred radius of the round thumb shape when the slider is disabled.
///
/// If no disabledRadius is provided, then it is equal to the
/// [enabledThumbRadius]
final double? disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
/// The resting elevation adds shadow to the unpressed thumb.
///
/// The default is 1.
///
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
/// example, a value of 12 will create a very large shadow.
///
final double elevation;
/// The pressed elevation adds shadow to the pressed thumb.
///
/// The default is 6.
///
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
/// example, a value of 12 will create a very large shadow.
final double pressedElevation;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled ? enabledThumbRadius : _disabledThumbRadius);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
assert(sliderTheme.disabledThumbColor != null);
assert(sliderTheme.thumbColor != null);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
end: enabledThumbRadius,
);
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final Color color = colorTween.evaluate(enableAnimation)!;
final double radius = radiusTween.evaluate(enableAnimation);
final Tween<double> elevationTween = Tween<double>(
begin: elevation,
end: pressedElevation,
);
final double evaluatedElevation = elevationTween.evaluate(activationAnimation);
final Path path = Path()
..addArc(Rect.fromCenter(center: center, width: 2 * radius, height: 2 * radius), 0, math.pi * 2);
bool paintShadows = true;
assert(() {
if (debugDisableShadows) {
_debugDrawShadow(canvas, path, evaluatedElevation);
paintShadows = false;
}
return true;
}());
if (paintShadows) {
canvas.drawShadow(path, Colors.black, evaluatedElevation, true);
}
canvas.drawCircle(
center,
radius,
Paint()..color = color,
);
}
}
/// The default shape of a [RangeSlider]'s thumbs.
///
/// There is a shadow for the resting and pressed state.
///
/// ![A slider widget, consisting of 5 divisions and showing the round range slider thumb shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/round_range_slider_thumb_shape.png)
///
/// See also:
///
/// * [RangeSlider], which includes thumbs defined by this shape.
/// * [SliderTheme], which can be used to configure the thumb shapes of all
/// range sliders in a widget subtree.
class RoundRangeSliderThumbShape extends RangeSliderThumbShape {
/// Create a slider thumb that draws a circle.
const RoundRangeSliderThumbShape({
this.enabledThumbRadius = 10.0,
this.disabledThumbRadius,
this.elevation = 1.0,
this.pressedElevation = 6.0,
});
/// The preferred radius of the round thumb shape when the slider is enabled.
///
/// If it is not provided, then the Material Design default of 10 is used.
final double enabledThumbRadius;
/// The preferred radius of the round thumb shape when the slider is disabled.
///
/// If no disabledRadius is provided, then it is equal to the
/// [enabledThumbRadius].
final double? disabledThumbRadius;
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
/// The resting elevation adds shadow to the unpressed thumb.
///
/// The default is 1.
final double elevation;
/// The pressed elevation adds shadow to the pressed thumb.
///
/// The default is 6.
final double pressedElevation;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled ? enabledThumbRadius : _disabledThumbRadius);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
bool isDiscrete = false,
bool isEnabled = false,
bool? isOnTop,
required SliderThemeData sliderTheme,
TextDirection? textDirection,
Thumb? thumb,
bool? isPressed,
}) {
assert(sliderTheme.showValueIndicator != null);
assert(sliderTheme.overlappingShapeStrokeColor != null);
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
end: enabledThumbRadius,
);
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final double radius = radiusTween.evaluate(enableAnimation);
final Tween<double> elevationTween = Tween<double>(
begin: elevation,
end: pressedElevation,
);
// Add a stroke of 1dp around the circle if this thumb would overlap
// the other thumb.
if (isOnTop ?? false) {
final Paint strokePaint = Paint()
..color = sliderTheme.overlappingShapeStrokeColor!
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
canvas.drawCircle(center, radius, strokePaint);
}
final Color color = colorTween.evaluate(enableAnimation)!;
final double evaluatedElevation = isPressed! ? elevationTween.evaluate(activationAnimation) : elevation;
final Path shadowPath = Path()
..addArc(Rect.fromCenter(center: center, width: 2 * radius, height: 2 * radius), 0, math.pi * 2);
bool paintShadows = true;
assert(() {
if (debugDisableShadows) {
_debugDrawShadow(canvas, shadowPath, evaluatedElevation);
paintShadows = false;
}
return true;
}());
if (paintShadows) {
canvas.drawShadow(shadowPath, Colors.black, evaluatedElevation, true);
}
canvas.drawCircle(
center,
radius,
Paint()..color = color,
);
}
}
/// The default shape of a [Slider]'s thumb overlay.
///
/// The shape of the overlay is a circle with the same center as the thumb, but
/// with a larger radius. It animates to full size when the thumb is pressed,
/// and animates back down to size 0 when it is released. It is painted behind
/// the thumb, and is expected to extend beyond the bounds of the thumb so that
/// it is visible.
///
/// The overlay color is defined by [SliderThemeData.overlayColor].
///
/// See also:
///
/// * [Slider], which includes an overlay defined by this shape.
/// * [SliderTheme], which can be used to configure the overlay shape of all
/// sliders in a widget subtree.
class RoundSliderOverlayShape extends SliderComponentShape {
/// Create a slider thumb overlay that draws a circle.
const RoundSliderOverlayShape({ this.overlayRadius = 24.0 });
/// The preferred radius of the round thumb shape when enabled.
///
/// If it is not provided, then half of the [SliderThemeData.trackHeight] is
/// used.
final double overlayRadius;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(overlayRadius);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: 0.0,
end: overlayRadius,
);
canvas.drawCircle(
center,
radiusTween.evaluate(activationAnimation),
Paint()..color = sliderTheme.overlayColor!,
);
}
}
/// The default shape of a [Slider]'s value indicator.
///
/// ![A slider widget, consisting of 5 divisions and showing the rectangular slider value indicator shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/rectangular_slider_value_indicator_shape.png)
///
/// See also:
///
/// * [Slider], which includes a value indicator defined by this shape.
/// * [SliderTheme], which can be used to configure the slider value indicator
/// of all sliders in a widget subtree.
class RectangularSliderValueIndicatorShape extends SliderComponentShape {
/// Create a slider value indicator that resembles a rectangular tooltip.
const RectangularSliderValueIndicatorShape();
static const _RectangularSliderValueIndicatorPathPainter _pathPainter = _RectangularSliderValueIndicatorPathPainter();
@override
Size getPreferredSize(
bool isEnabled,
bool isDiscrete, {
TextPainter? labelPainter,
double? textScaleFactor,
}) {
assert(labelPainter != null);
assert(textScaleFactor != null && textScaleFactor >= 0);
return _pathPainter.getPreferredSize(labelPainter!, textScaleFactor!);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
final double scale = activationAnimation.value;
_pathPainter.paint(
parentBox: parentBox,
canvas: canvas,
center: center,
scale: scale,
labelPainter: labelPainter,
textScaleFactor: textScaleFactor,
sizeWithOverflow: sizeWithOverflow,
backgroundPaintColor: sliderTheme.valueIndicatorColor!,
);
}
}
/// The default shape of a [RangeSlider]'s value indicators.
///
/// ![A slider widget, consisting of 5 divisions and showing the rectangular range slider value indicator shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/rectangular_range_slider_value_indicator_shape.png)
///
/// See also:
///
/// * [RangeSlider], which includes value indicators defined by this shape.
/// * [SliderTheme], which can be used to configure the range slider value
/// indicator of all sliders in a widget subtree.
class RectangularRangeSliderValueIndicatorShape
extends RangeSliderValueIndicatorShape {
/// Create a range slider value indicator that resembles a rectangular tooltip.
const RectangularRangeSliderValueIndicatorShape();
static const _RectangularSliderValueIndicatorPathPainter _pathPainter = _RectangularSliderValueIndicatorPathPainter();
@override
Size getPreferredSize(
bool isEnabled,
bool isDiscrete, {
required TextPainter labelPainter,
required double textScaleFactor,
}) {
assert(textScaleFactor >= 0);
return _pathPainter.getPreferredSize(labelPainter, textScaleFactor);
}
@override
double getHorizontalShift({
RenderBox? parentBox,
Offset? center,
TextPainter? labelPainter,
Animation<double>? activationAnimation,
double? textScaleFactor,
Size? sizeWithOverflow,
}) {
return _pathPainter.getHorizontalShift(
parentBox: parentBox!,
center: center!,
labelPainter: labelPainter!,
textScaleFactor: textScaleFactor!,
sizeWithOverflow: sizeWithOverflow!,
scale: activationAnimation!.value,
);
}
@override
void paint(
PaintingContext context,
Offset center, {
Animation<double>? activationAnimation,
Animation<double>? enableAnimation,
bool? isDiscrete,
bool? isOnTop,
TextPainter? labelPainter,
double? textScaleFactor,
Size? sizeWithOverflow,
RenderBox? parentBox,
SliderThemeData? sliderTheme,
TextDirection? textDirection,
double? value,
Thumb? thumb,
}) {
final Canvas canvas = context.canvas;
final double scale = activationAnimation!.value;
_pathPainter.paint(
parentBox: parentBox!,
canvas: canvas,
center: center,
scale: scale,
labelPainter: labelPainter!,
textScaleFactor: textScaleFactor!,
sizeWithOverflow: sizeWithOverflow!,
backgroundPaintColor: sliderTheme!.valueIndicatorColor!,
strokePaintColor: isOnTop! ? sliderTheme.overlappingShapeStrokeColor : null,
);
}
}
class _RectangularSliderValueIndicatorPathPainter {
const _RectangularSliderValueIndicatorPathPainter();
static const double _triangleHeight = 8.0;
static const double _labelPadding = 16.0;
static const double _preferredHeight = 32.0;
static const double _minLabelWidth = 16.0;
static const double _bottomTipYOffset = 14.0;
static const double _preferredHalfHeight = _preferredHeight / 2;
static const double _upperRectRadius = 4;
Size getPreferredSize(
TextPainter labelPainter,
double textScaleFactor,
) {
return Size(
_upperRectangleWidth(labelPainter, 1, textScaleFactor),
labelPainter.height + _labelPadding,
);
}
double getHorizontalShift({
required RenderBox parentBox,
required Offset center,
required TextPainter labelPainter,
required double textScaleFactor,
required Size sizeWithOverflow,
required double scale,
}) {
assert(!sizeWithOverflow.isEmpty);
const double edgePadding = 8.0;
final double rectangleWidth = _upperRectangleWidth(labelPainter, scale, textScaleFactor);
/// Value indicator draws on the Overlay and by using the global Offset
/// we are making sure we use the bounds of the Overlay instead of the Slider.
final Offset globalCenter = parentBox.localToGlobal(center);
// The rectangle must be shifted towards the center so that it minimizes the
// chance of it rendering outside the bounds of the render box. If the shift
// is negative, then the lobe is shifted from right to left, and if it is
// positive, then the lobe is shifted from left to right.
final double overflowLeft = math.max(0, rectangleWidth / 2 - globalCenter.dx + edgePadding);
final double overflowRight = math.max(0, rectangleWidth / 2 - (sizeWithOverflow.width - globalCenter.dx - edgePadding));
if (rectangleWidth < sizeWithOverflow.width) {
return overflowLeft - overflowRight;
} else if (overflowLeft - overflowRight > 0) {
return overflowLeft - (edgePadding * textScaleFactor);
} else {
return -overflowRight + (edgePadding * textScaleFactor);
}
}
double _upperRectangleWidth(TextPainter labelPainter, double scale, double textScaleFactor) {
final double unscaledWidth = math.max(_minLabelWidth * textScaleFactor, labelPainter.width) + _labelPadding * 2;
return unscaledWidth * scale;
}
void paint({
required RenderBox parentBox,
required Canvas canvas,
required Offset center,
required double scale,
required TextPainter labelPainter,
required double textScaleFactor,
required Size sizeWithOverflow,
required Color backgroundPaintColor,
Color? strokePaintColor,
}) {
if (scale == 0.0) {
// Zero scale essentially means "do not draw anything", so it's safe to just return.
return;
}
assert(!sizeWithOverflow.isEmpty);
final double rectangleWidth = _upperRectangleWidth(labelPainter, scale, textScaleFactor);
final double horizontalShift = getHorizontalShift(
parentBox: parentBox,
center: center,
labelPainter: labelPainter,
textScaleFactor: textScaleFactor,
sizeWithOverflow: sizeWithOverflow,
scale: scale,
);
final double rectHeight = labelPainter.height + _labelPadding;
final Rect upperRect = Rect.fromLTWH(
-rectangleWidth / 2 + horizontalShift,
-_triangleHeight - rectHeight,
rectangleWidth,
rectHeight,
);
final Path trianglePath = Path()
..lineTo(-_triangleHeight, -_triangleHeight)
..lineTo(_triangleHeight, -_triangleHeight)
..close();
final Paint fillPaint = Paint()..color = backgroundPaintColor;
final RRect upperRRect = RRect.fromRectAndRadius(upperRect, const Radius.circular(_upperRectRadius));
trianglePath.addRRect(upperRRect);
canvas.save();
// Prepare the canvas for the base of the tooltip, which is relative to the
// center of the thumb.
canvas.translate(center.dx, center.dy - _bottomTipYOffset);
canvas.scale(scale, scale);
if (strokePaintColor != null) {
final Paint strokePaint = Paint()
..color = strokePaintColor
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
canvas.drawPath(trianglePath, strokePaint);
}
canvas.drawPath(trianglePath, fillPaint);
// The label text is centered within the value indicator.
final double bottomTipToUpperRectTranslateY = -_preferredHalfHeight / 2 - upperRect.height;
canvas.translate(0, bottomTipToUpperRectTranslateY);
final Offset boxCenter = Offset(horizontalShift, upperRect.height / 2);
final Offset halfLabelPainterOffset = Offset(labelPainter.width / 2, labelPainter.height / 2);
final Offset labelOffset = boxCenter - halfLabelPainterOffset;
labelPainter.paint(canvas, labelOffset);
canvas.restore();
}
}
/// A variant shape of a [Slider]'s value indicator . The value indicator is in
/// the shape of an upside-down pear.
///
/// ![A slider widget, consisting of 5 divisions and showing the paddle slider value indicator shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/paddle_slider_value_indicator_shape.png)
///
/// See also:
///
/// * [Slider], which includes a value indicator defined by this shape.
/// * [SliderTheme], which can be used to configure the slider value indicator
/// of all sliders in a widget subtree.
class PaddleSliderValueIndicatorShape extends SliderComponentShape {
/// Create a slider value indicator in the shape of an upside-down pear.
const PaddleSliderValueIndicatorShape();
static const _PaddleSliderValueIndicatorPathPainter _pathPainter = _PaddleSliderValueIndicatorPathPainter();
@override
Size getPreferredSize(
bool isEnabled,
bool isDiscrete, {
TextPainter? labelPainter,
double? textScaleFactor,
}) {
assert(labelPainter != null);
assert(textScaleFactor != null && textScaleFactor >= 0);
return _pathPainter.getPreferredSize(labelPainter!, textScaleFactor!);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
assert(!sizeWithOverflow.isEmpty);
final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
);
_pathPainter.paint(
context.canvas,
center,
Paint()..color = enableColor.evaluate(enableAnimation)!,
activationAnimation.value,
labelPainter,
textScaleFactor,
sizeWithOverflow,
null,
);
}
}
/// A variant shape of a [RangeSlider]'s value indicators. The value indicator
/// is in the shape of an upside-down pear.
///
/// ![A slider widget, consisting of 5 divisions and showing the paddle range slider value indicator shape.]
/// (https://flutter.github.io/assets-for-api-docs/assets/material/paddle_range_slider_value_indicator_shape.png)
///
/// See also:
///
/// * [RangeSlider], which includes value indicators defined by this shape.
/// * [SliderTheme], which can be used to configure the range slider value
/// indicator of all sliders in a widget subtree.
class PaddleRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShape {
/// Create a slider value indicator in the shape of an upside-down pear.
const PaddleRangeSliderValueIndicatorShape();
static const _PaddleSliderValueIndicatorPathPainter _pathPainter = _PaddleSliderValueIndicatorPathPainter();
@override
Size getPreferredSize(
bool isEnabled,
bool isDiscrete, {
required TextPainter labelPainter,
required double textScaleFactor,
}) {
assert(textScaleFactor >= 0);
return _pathPainter.getPreferredSize(labelPainter, textScaleFactor);
}
@override
double getHorizontalShift({
RenderBox? parentBox,
Offset? center,
TextPainter? labelPainter,
Animation<double>? activationAnimation,
double? textScaleFactor,
Size? sizeWithOverflow,
}) {
return _pathPainter.getHorizontalShift(
center: center!,
labelPainter: labelPainter!,
scale: activationAnimation!.value,
textScaleFactor: textScaleFactor!,
sizeWithOverflow: sizeWithOverflow!,
);
}
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
bool? isDiscrete,
bool isOnTop = false,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
TextDirection? textDirection,
Thumb? thumb,
double? value,
double? textScaleFactor,
Size? sizeWithOverflow,
}) {
assert(!sizeWithOverflow!.isEmpty);
final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
);
// Add a stroke of 1dp around the top paddle.
_pathPainter.paint(
context.canvas,
center,
Paint()..color = enableColor.evaluate(enableAnimation)!,
activationAnimation.value,
labelPainter,
textScaleFactor!,
sizeWithOverflow!,
isOnTop ? sliderTheme.overlappingShapeStrokeColor : null,
);
}
}
class _PaddleSliderValueIndicatorPathPainter {
const _PaddleSliderValueIndicatorPathPainter();
// 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;
static const double _minLabelWidth = 16.0;
// Radius of the bottom lobe of the value indicator.
static const double _bottomLobeRadius = 10.0;
static const double _labelPadding = 8.0;
static const double _distanceBetweenTopBottomCenters = 40.0;
static const double _middleNeckWidth = 3.0;
static const double _bottomNeckRadius = 4.5;
// The base of the triangle between the top lobe center and the centers of
// the two top neck arcs.
static const double _neckTriangleBase = _topNeckRadius + _middleNeckWidth / 2;
static const double _rightBottomNeckCenterX = _middleNeckWidth / 2 + _bottomNeckRadius;
static const double _rightBottomNeckAngleStart = math.pi;
static const Offset _topLobeCenter = Offset(0.0, -_distanceBetweenTopBottomCenters);
static const double _topNeckRadius = 13.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 double _preferredHeight = _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;
Size getPreferredSize(
TextPainter labelPainter,
double textScaleFactor,
) {
assert(textScaleFactor >= 0);
final double width = math.max(_minLabelWidth * textScaleFactor, labelPainter.width) + _labelPadding * 2 * textScaleFactor;
return Size(width, _preferredHeight * textScaleFactor);
}
// 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) {
assert(center.isFinite);
final Rect arcRect = Rect.fromCircle(center: center, radius: radius);
path.arcTo(arcRect, startAngle, endAngle - startAngle, false);
}
double getHorizontalShift({
required Offset center,
required TextPainter labelPainter,
required double scale,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
assert(!sizeWithOverflow.isEmpty);
final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
final double labelHalfWidth = labelPainter.width / 2.0;
final double halfWidthNeeded = math.max(
0.0,
inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding),
);
final double shift = _getIdealOffset(halfWidthNeeded, textScaleFactor * scale, center, sizeWithOverflow.width);
return shift * textScaleFactor;
}
// Determines the "best" offset to keep the bubble within the slider. The
// calling code will bound that with the available movement in the paddle shape.
double _getIdealOffset(
double halfWidthNeeded,
double scale,
Offset center,
double widthWithOverflow,
) {
const double edgeMargin = 8.0;
final Rect topLobeRect = 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;
}
final double endGlobal = widthWithOverflow;
if (bottomRight.dx > endGlobal - edgeMargin) {
shift = endGlobal - edgeMargin - bottomRight.dx;
}
shift = scale == 0.0 ? 0.0 : shift / scale;
if (shift < 0.0) {
// Shifting to the left.
shift = math.max(shift, -halfWidthNeeded);
} else {
// Shifting to the right.
shift = math.min(shift, halfWidthNeeded);
}
return shift;
}
void paint(
Canvas canvas,
Offset center,
Paint paint,
double scale,
TextPainter labelPainter,
double textScaleFactor,
Size sizeWithOverflow,
Color? strokePaintColor,
) {
if (scale == 0.0) {
// Zero scale essentially means "do not draw anything", so it's safe to just return. Otherwise,
// our math below will attempt to divide by zero and send needless NaNs to the engine.
return;
}
assert(!sizeWithOverflow.isEmpty);
// The entire value indicator should scale with the size of the label,
// to keep it large enough to encompass the label text.
final double overallScale = scale * textScaleFactor;
final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
final double labelHalfWidth = labelPainter.width / 2.0;
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.scale(overallScale, overallScale);
final double bottomNeckTriangleHypotenuse = _bottomNeckRadius + _bottomLobeRadius / overallScale;
final double rightBottomNeckCenterY = -math.sqrt(math.pow(bottomNeckTriangleHypotenuse, 2) - math.pow(_rightBottomNeckCenterX, 2));
final double rightBottomNeckAngleEnd = math.pi + math.atan(rightBottomNeckCenterY / _rightBottomNeckCenterX);
final Path path = Path()..moveTo(_middleNeckWidth / 2, rightBottomNeckCenterY);
_addArc(
path,
Offset(_rightBottomNeckCenterX, rightBottomNeckCenterY),
_bottomNeckRadius,
_rightBottomNeckAngleStart,
rightBottomNeckAngleEnd,
);
_addArc(
path,
Offset.zero,
_bottomLobeRadius / overallScale,
rightBottomNeckAngleEnd - math.pi,
2 * math.pi - rightBottomNeckAngleEnd,
);
_addArc(
path,
Offset(-_rightBottomNeckCenterX, rightBottomNeckCenterY),
_bottomNeckRadius,
math.pi - rightBottomNeckAngleEnd,
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 double shift = _getIdealOffset( halfWidthNeeded, overallScale, center, sizeWithOverflow.width);
final double leftWidthNeeded = halfWidthNeeded - shift;
final double rightWidthNeeded = halfWidthNeeded + shift;
// 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. The base amount is chosen so that the neck is smooth,
// even when the lobe is shifted due to its size.
final double leftTheta = (1.0 - leftAmount) * _thirtyDegrees;
final double rightTheta = (1.0 - rightAmount) * _thirtyDegrees;
// The center of the top left neck arc.
final Offset leftTopNeckCenter = Offset(
-_neckTriangleBase,
_topLobeCenter.dy + math.cos(leftTheta) * _neckTriangleHypotenuse,
);
final Offset neckRightCenter = 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 = math.max(0.0, rightBottomNeckCenterY - math.max(leftTopNeckCenter.dy, neckRightCenter.dy));
final double t = math.pow(inverseTextScale, 3.0) as double;
final double stretch = clampDouble(neckStretchBaseline * t, 0.0, 10.0 * neckStretchBaseline);