mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
856 lines
27 KiB
Dart
856 lines
27 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:fluent_ui/fluent_ui.dart';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/material.dart' as m;
|
|
|
|
/// A slider is a control that lets the user select from a
|
|
/// range of values by moving a thumb control along a track.
|
|
///
|
|
/// A slider is a good choice when you know that users think
|
|
/// of the value as a relative quantity, not a numeric value.
|
|
/// For example, users think about setting their audio volume
|
|
/// to low or medium—not about setting the value to 2 or 5.
|
|
///
|
|
/// 
|
|
///
|
|
/// See also:
|
|
/// - [RatingBar]
|
|
class Slider extends StatefulWidget {
|
|
const Slider({
|
|
Key? key,
|
|
required this.value,
|
|
required this.onChanged,
|
|
this.onChangeStart,
|
|
this.onChangeEnd,
|
|
this.min = 0.0,
|
|
this.max = 100.0,
|
|
this.divisions,
|
|
this.style,
|
|
this.label,
|
|
this.focusNode,
|
|
this.vertical = false,
|
|
this.autofocus = false,
|
|
this.mouseCursor = MouseCursor.defer,
|
|
}) : assert(value >= min && value <= max),
|
|
assert(divisions == null || divisions > 0),
|
|
super(key: key);
|
|
|
|
/// The currently selected value for this slider.
|
|
///
|
|
/// The slider's thumb is drawn at a position that corresponds to this value.
|
|
final double value;
|
|
|
|
/// Called during a drag when the user is selecting a new value for the slider
|
|
/// by dragging.
|
|
///
|
|
/// The slider passes the new value to the callback but does not actually
|
|
/// change state until the parent widget rebuilds the slider with the new
|
|
/// value.
|
|
///
|
|
/// If null, the slider will be displayed as disabled.
|
|
///
|
|
/// The callback provided to onChanged should update the state of the parent
|
|
/// [StatefulWidget] using the [State.setState] method, so that the parent
|
|
/// gets rebuilt; for example:
|
|
///
|
|
/// {@tool snippet}
|
|
///
|
|
/// ```dart
|
|
/// Slider(
|
|
/// value: _duelCommandment.toDouble(),
|
|
/// min: 1.0,
|
|
/// max: 10.0,
|
|
/// divisions: 10,
|
|
/// label: '$_duelCommandment',
|
|
/// onChanged: (double newValue) {
|
|
/// setState(() {
|
|
/// _duelCommandment = newValue.round();
|
|
/// });
|
|
/// },
|
|
/// )
|
|
/// ```
|
|
/// {@end-tool}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [onChangeStart] for a callback that is called when the user starts
|
|
/// changing the value.
|
|
/// * [onChangeEnd] for a callback that is called when the user stops
|
|
/// changing the value.
|
|
final ValueChanged<double>? onChanged;
|
|
|
|
/// Called when the user starts selecting a new value for the slider.
|
|
///
|
|
/// This callback shouldn't be used to update the slider [value] (use
|
|
/// [onChanged] for that), but rather to be notified when the user has started
|
|
/// selecting a new value by starting a drag or with a tap.
|
|
///
|
|
/// The value passed will be the last [value] that the slider had before the
|
|
/// change began.
|
|
///
|
|
/// {@tool snippet}
|
|
///
|
|
/// ```dart
|
|
/// Slider(
|
|
/// value: _duelCommandment.toDouble(),
|
|
/// min: 1.0,
|
|
/// max: 10.0,
|
|
/// divisions: 10,
|
|
/// label: '$_duelCommandment',
|
|
/// onChanged: (double newValue) {
|
|
/// setState(() {
|
|
/// _duelCommandment = newValue.round();
|
|
/// });
|
|
/// },
|
|
/// onChangeStart: (double startValue) {
|
|
/// print('Started change at $startValue');
|
|
/// },
|
|
/// )
|
|
/// ```
|
|
/// {@end-tool}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [onChangeEnd] for a callback that is called when the value change is
|
|
/// complete.
|
|
final ValueChanged<double>? onChangeStart;
|
|
|
|
/// Called when the user is done selecting a new value for the slider.
|
|
///
|
|
/// This callback shouldn't be used to update the slider [value] (use
|
|
/// [onChanged] for that), but rather to know when the user has completed
|
|
/// selecting a new [value] by ending a drag or a click.
|
|
///
|
|
/// {@tool snippet}
|
|
///
|
|
/// ```dart
|
|
/// Slider(
|
|
/// value: _duelCommandment.toDouble(),
|
|
/// min: 1.0,
|
|
/// max: 10.0,
|
|
/// divisions: 10,
|
|
/// label: '$_duelCommandment',
|
|
/// onChanged: (double newValue) {
|
|
/// setState(() {
|
|
/// _duelCommandment = newValue.round();
|
|
/// });
|
|
/// },
|
|
/// onChangeEnd: (double newValue) {
|
|
/// print('Ended change on $newValue');
|
|
/// },
|
|
/// )
|
|
/// ```
|
|
/// {@end-tool}
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [onChangeStart] for a callback that is called when a value change
|
|
/// begins.
|
|
final ValueChanged<double>? onChangeEnd;
|
|
|
|
/// The maximum value the user can select.
|
|
///
|
|
/// Defaults to 1.0. Must be greater than or equal to [min].
|
|
///
|
|
/// If the [max] is equal to the [min], then the slider is disabled.
|
|
final double min;
|
|
|
|
/// The minimum value the user can select.
|
|
///
|
|
/// Defaults to 0.0. Must be less than or equal to [max].
|
|
///
|
|
/// If the [max] is equal to the [min], then the slider is disabled.
|
|
final double max;
|
|
|
|
/// The number of discrete divisions.
|
|
///
|
|
/// Typically used with [label] to show the current discrete value.
|
|
///
|
|
/// If null, the slider is continuous.
|
|
final int? divisions;
|
|
|
|
/// The style used in this slider. It's mescled with [ThemeData.sliderThemeData]
|
|
final SliderThemeData? style;
|
|
|
|
/// A label to show above the slider, or at the left
|
|
/// of the slider if [vertical] is `true` when the slider is active.
|
|
final String? label;
|
|
|
|
/// {@macro flutter.widgets.Focus.focusNode}
|
|
final FocusNode? focusNode;
|
|
|
|
/// {@macro flutter.widgets.Focus.autofocus}
|
|
final bool autofocus;
|
|
|
|
/// Whether the slider is vertical or not
|
|
///
|
|
/// Use a vertical slider if the slider represents a
|
|
/// real-world value that is normally shown vertically
|
|
/// (such as temperature).
|
|
final bool vertical;
|
|
|
|
/// {@macro fluent_ui.controls.inputs.HoverButton.mouseCursor}
|
|
final MouseCursor mouseCursor;
|
|
|
|
@override
|
|
_SliderState createState() => _SliderState();
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties
|
|
..add(DoubleProperty('value', value))
|
|
..add(DoubleProperty('min', min))
|
|
..add(DoubleProperty('max', max))
|
|
..add(IntProperty('divisions', divisions))
|
|
..add(StringProperty('label', label))
|
|
..add(ObjectFlagProperty<FocusNode>.has('focusNode', focusNode))
|
|
..add(DiagnosticsProperty<SliderThemeData>('style', style))
|
|
..add(FlagProperty('vertical', value: vertical, ifFalse: 'horizontal'));
|
|
}
|
|
}
|
|
|
|
class _SliderState extends m.State<Slider> {
|
|
bool _showFocusHighlight = false;
|
|
|
|
late FocusNode _focusNode;
|
|
bool _sliding = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_focusNode = widget.focusNode ?? FocusNode();
|
|
_focusNode.addListener(_handleFocusChanged);
|
|
}
|
|
|
|
void _handleFocusChanged() {
|
|
setState(() {});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_focusNode.removeListener(_handleFocusChanged);
|
|
// Only dispose the focus node manually created
|
|
if (widget.focusNode == null) _focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
assert(debugCheckHasFluentTheme(context));
|
|
assert(debugCheckHasDirectionality(context));
|
|
final style = SliderTheme.of(context).merge(widget.style);
|
|
final direction = Directionality.of(context);
|
|
Widget child = HoverButton(
|
|
onPressed: widget.onChanged == null ? null : () {},
|
|
margin: style.margin ?? EdgeInsets.zero,
|
|
cursor: widget.mouseCursor,
|
|
builder: (context, states) => m.Material(
|
|
type: m.MaterialType.transparency,
|
|
child: TweenAnimationBuilder<double>(
|
|
duration: FluentTheme.of(context).fastAnimationDuration,
|
|
tween: Tween<double>(
|
|
begin: 1.0,
|
|
end: states.isPressing || _sliding
|
|
? 0.45
|
|
: states.isHovering
|
|
? 0.66
|
|
: 0.5,
|
|
),
|
|
builder: (context, innerFactor, child) => m.SliderTheme(
|
|
data: m.SliderThemeData(
|
|
showValueIndicator: m.ShowValueIndicator.always,
|
|
thumbColor: style.thumbColor ?? style.activeColor,
|
|
overlayShape: const m.RoundSliderOverlayShape(overlayRadius: 0),
|
|
thumbShape: SliderThumbShape(
|
|
elevation: 0,
|
|
pressedElevation: 0,
|
|
useBall: style.useThumbBall ?? true,
|
|
innerFactor: innerFactor,
|
|
brightness: FluentTheme.of(context).brightness,
|
|
),
|
|
valueIndicatorShape: _RectangularSliderValueIndicatorShape(
|
|
backgroundColor: style.labelBackgroundColor,
|
|
vertical: widget.vertical,
|
|
ltr: direction == TextDirection.ltr,
|
|
),
|
|
trackHeight: 1.75,
|
|
trackShape: _CustomTrackShape(),
|
|
disabledThumbColor: style.disabledThumbColor,
|
|
disabledInactiveTrackColor: style.disabledInactiveColor,
|
|
disabledActiveTrackColor: style.disabledActiveColor,
|
|
),
|
|
child: child!,
|
|
),
|
|
child: m.Slider(
|
|
value: widget.value,
|
|
max: widget.max,
|
|
min: widget.min,
|
|
onChanged: widget.onChanged,
|
|
onChangeEnd: (v) {
|
|
widget.onChangeEnd?.call(v);
|
|
setState(() => _sliding = false);
|
|
},
|
|
onChangeStart: (v) {
|
|
widget.onChangeStart?.call(v);
|
|
setState(() => _sliding = true);
|
|
},
|
|
activeColor: style.activeColor,
|
|
inactiveColor: style.inactiveColor,
|
|
divisions: widget.divisions,
|
|
label: widget.label,
|
|
focusNode: _focusNode,
|
|
autofocus: widget.autofocus,
|
|
mouseCursor: MouseCursor.defer,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
child = FocusableActionDetector(
|
|
onShowFocusHighlight: (v) => setState(() => _showFocusHighlight = v),
|
|
child: FocusBorder(
|
|
focused: _showFocusHighlight && (_focusNode.hasPrimaryFocus),
|
|
useStackApproach: true,
|
|
child: child,
|
|
),
|
|
);
|
|
if (widget.vertical) {
|
|
return RotatedBox(
|
|
quarterTurns: direction == TextDirection.ltr ? 3 : 5,
|
|
child: child,
|
|
);
|
|
}
|
|
return child;
|
|
}
|
|
}
|
|
|
|
/// This is used to remove the padding the Material Slider adds automatically
|
|
class _CustomTrackShape extends m.RoundedRectSliderTrackShape {
|
|
@override
|
|
Rect getPreferredRect({
|
|
required RenderBox parentBox,
|
|
Offset offset = Offset.zero,
|
|
required m.SliderThemeData sliderTheme,
|
|
bool isEnabled = false,
|
|
bool isDiscrete = false,
|
|
}) {
|
|
final double trackHeight = sliderTheme.trackHeight!;
|
|
final double trackLeft = offset.dx;
|
|
final double trackTop =
|
|
offset.dy + (parentBox.size.height - trackHeight) / 2;
|
|
final double trackWidth = parentBox.size.width;
|
|
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
|
|
}
|
|
}
|
|
|
|
/// The default shape of a [Slider]'s thumb.
|
|
///
|
|
/// There is a shadow for the resting, pressed, hovered, and focused state.
|
|
class SliderThumbShape extends m.SliderComponentShape {
|
|
/// Create a fluent-styled slider thumb;
|
|
const SliderThumbShape({
|
|
this.enabledThumbRadius = 10.0,
|
|
this.disabledThumbRadius,
|
|
this.elevation = 1.0,
|
|
this.pressedElevation = 6.0,
|
|
this.useBall = true,
|
|
this.innerFactor = 1.0,
|
|
this.brightness = Brightness.light,
|
|
});
|
|
|
|
final double innerFactor;
|
|
|
|
final Brightness brightness;
|
|
|
|
/// Whether to draw a ball instead of a line
|
|
final bool useBall;
|
|
|
|
/// The preferred radius of the round thumb shape when the slider is enabled.
|
|
///
|
|
/// If it is not provided, then the material 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 == true ? 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 m.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);
|
|
|
|
if (!useBall) {
|
|
canvas.drawLine(
|
|
center - const Offset(0, 6),
|
|
center + const Offset(0, 6),
|
|
Paint()
|
|
..color = color
|
|
..style = PaintingStyle.stroke
|
|
..strokeJoin = StrokeJoin.round
|
|
..strokeCap = StrokeCap.round
|
|
..strokeWidth = 8.0,
|
|
);
|
|
} else {
|
|
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);
|
|
canvas.drawShadow(path, Colors.black, evaluatedElevation, true);
|
|
canvas.drawCircle(
|
|
center,
|
|
radius,
|
|
Paint()
|
|
..color = brightness == Brightness.light
|
|
? Colors.white
|
|
: const Color(0xFF454545),
|
|
);
|
|
canvas.drawCircle(
|
|
center,
|
|
radius * innerFactor,
|
|
Paint()..color = color,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An inherited widget that defines the configuration for
|
|
/// [Slider]s in this widget's subtree.
|
|
///
|
|
/// Values specified here are used for [Slider] properties that are not
|
|
/// given an explicit non-null value.
|
|
class SliderTheme extends InheritedTheme {
|
|
/// Creates a slider theme that controls the configurations for
|
|
/// [Slider].
|
|
const SliderTheme({
|
|
Key? key,
|
|
required this.data,
|
|
required Widget child,
|
|
}) : super(key: key, child: child);
|
|
|
|
/// The properties for descendant [Slider] widgets.
|
|
final SliderThemeData data;
|
|
|
|
/// Creates a button theme that controls how descendant [Slider]s should
|
|
/// look like, and merges in the current slider theme, if any.
|
|
static Widget merge({
|
|
Key? key,
|
|
required SliderThemeData data,
|
|
required Widget child,
|
|
}) {
|
|
return Builder(builder: (BuildContext context) {
|
|
return SliderTheme(
|
|
key: key,
|
|
data: _getInheritedThemeData(context).merge(data),
|
|
child: child,
|
|
);
|
|
});
|
|
}
|
|
|
|
static SliderThemeData _getInheritedThemeData(BuildContext context) {
|
|
final theme = context.dependOnInheritedWidgetOfExactType<SliderTheme>();
|
|
return theme?.data ?? FluentTheme.of(context).sliderTheme;
|
|
}
|
|
|
|
/// Returns the [data] from the closest [SliderTheme] ancestor. If there is
|
|
/// no ancestor, it returns [ThemeData.sliderTheme]. Applications can assume
|
|
/// that the returned value will not be null.
|
|
///
|
|
/// Typical usage is as follows:
|
|
///
|
|
/// ```dart
|
|
/// SliderThemeData theme = SliderTheme.of(context);
|
|
/// ```
|
|
static SliderThemeData of(BuildContext context) {
|
|
return SliderThemeData.standard(FluentTheme.of(context)).merge(
|
|
_getInheritedThemeData(context),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget wrap(BuildContext context, Widget child) {
|
|
return SliderTheme(data: data, child: child);
|
|
}
|
|
|
|
@override
|
|
bool updateShouldNotify(SliderTheme oldWidget) => data != oldWidget.data;
|
|
}
|
|
|
|
@immutable
|
|
class SliderThemeData with Diagnosticable {
|
|
final Color? thumbColor;
|
|
final Color? disabledThumbColor;
|
|
final Color? labelBackgroundColor;
|
|
|
|
final bool? useThumbBall;
|
|
|
|
final Color? activeColor;
|
|
final Color? inactiveColor;
|
|
|
|
final Color? disabledActiveColor;
|
|
final Color? disabledInactiveColor;
|
|
|
|
final EdgeInsetsGeometry? margin;
|
|
|
|
const SliderThemeData({
|
|
this.margin,
|
|
this.thumbColor,
|
|
this.disabledThumbColor,
|
|
this.activeColor,
|
|
this.disabledActiveColor,
|
|
this.inactiveColor,
|
|
this.disabledInactiveColor,
|
|
this.labelBackgroundColor,
|
|
this.useThumbBall,
|
|
});
|
|
|
|
factory SliderThemeData.standard(ThemeData? style) {
|
|
final def = SliderThemeData(
|
|
thumbColor: style?.accentColor,
|
|
activeColor: style?.accentColor,
|
|
inactiveColor: style?.disabledColor.withOpacity(1),
|
|
margin: EdgeInsets.zero,
|
|
disabledActiveColor: style?.disabledColor.withOpacity(1),
|
|
disabledThumbColor: style?.disabledColor.withOpacity(1),
|
|
disabledInactiveColor: style?.disabledColor,
|
|
useThumbBall: true,
|
|
);
|
|
|
|
return def;
|
|
}
|
|
|
|
static SliderThemeData lerp(
|
|
SliderThemeData? a, SliderThemeData? b, double t) {
|
|
return SliderThemeData(
|
|
margin: EdgeInsetsGeometry.lerp(a?.margin, b?.margin, t),
|
|
thumbColor: Color.lerp(a?.thumbColor, b?.thumbColor, t),
|
|
activeColor: Color.lerp(a?.activeColor, b?.activeColor, t),
|
|
inactiveColor: Color.lerp(a?.inactiveColor, b?.inactiveColor, t),
|
|
disabledActiveColor:
|
|
Color.lerp(a?.disabledActiveColor, b?.disabledActiveColor, t),
|
|
disabledInactiveColor:
|
|
Color.lerp(a?.disabledInactiveColor, b?.disabledInactiveColor, t),
|
|
disabledThumbColor:
|
|
Color.lerp(a?.disabledThumbColor, b?.disabledThumbColor, t),
|
|
labelBackgroundColor:
|
|
Color.lerp(a?.labelBackgroundColor, b?.labelBackgroundColor, t),
|
|
useThumbBall: t < 0.5 ? a?.useThumbBall : b?.useThumbBall,
|
|
);
|
|
}
|
|
|
|
SliderThemeData merge(SliderThemeData? style) {
|
|
return SliderThemeData(
|
|
margin: style?.margin ?? margin,
|
|
thumbColor: style?.thumbColor ?? thumbColor,
|
|
activeColor: style?.activeColor ?? activeColor,
|
|
inactiveColor: style?.inactiveColor ?? inactiveColor,
|
|
disabledActiveColor: style?.disabledActiveColor ?? disabledActiveColor,
|
|
disabledInactiveColor:
|
|
style?.disabledInactiveColor ?? disabledInactiveColor,
|
|
disabledThumbColor: style?.disabledThumbColor ?? disabledThumbColor,
|
|
labelBackgroundColor: style?.labelBackgroundColor ?? labelBackgroundColor,
|
|
useThumbBall: style?.useThumbBall ?? useThumbBall,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties.add(DiagnosticsProperty<EdgeInsetsGeometry?>('margin', margin));
|
|
properties.add(ColorProperty('thumbColor', thumbColor));
|
|
properties.add(ColorProperty('activeColor', activeColor));
|
|
properties.add(ColorProperty('inactiveColor', inactiveColor));
|
|
properties.add(ColorProperty('disabledActiveColor', disabledActiveColor));
|
|
properties
|
|
.add(ColorProperty('disabledInactiveColor', disabledInactiveColor));
|
|
properties.add(ColorProperty('disabledThumbColor', disabledThumbColor));
|
|
properties.add(ColorProperty('labelBackgroundColor', labelBackgroundColor));
|
|
}
|
|
}
|
|
|
|
class _RectangularSliderValueIndicatorShape extends m.SliderComponentShape {
|
|
final Color? backgroundColor;
|
|
final bool vertical;
|
|
final bool ltr;
|
|
|
|
/// Create a slider value indicator that resembles a rectangular tooltip.
|
|
const _RectangularSliderValueIndicatorShape({
|
|
this.backgroundColor,
|
|
this.vertical = false,
|
|
this.ltr = false,
|
|
});
|
|
|
|
get _pathPainter => _RectangularSliderValueIndicatorPathPainter(
|
|
vertical,
|
|
ltr,
|
|
);
|
|
|
|
@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 m.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: backgroundColor ?? sliderTheme.valueIndicatorColor!,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RectangularSliderValueIndicatorPathPainter {
|
|
final bool vertical;
|
|
|
|
/// Whether the current [Directionality] is [TextDirection.ltr]
|
|
final bool ltr;
|
|
|
|
const _RectangularSliderValueIndicatorPathPainter([
|
|
this.vertical = false,
|
|
this.ltr = false,
|
|
]);
|
|
|
|
static const double _triangleHeight = 8.0;
|
|
static const double _labelPadding = 8.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;
|
|
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()..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.
|
|
final double verticalFactor = ltr ? 20.0 : 10.0;
|
|
canvas.translate(
|
|
center.dx +
|
|
(vertical
|
|
? ltr
|
|
? -verticalFactor
|
|
: verticalFactor * 2
|
|
: 0),
|
|
center.dy -
|
|
_bottomTipYOffset +
|
|
(vertical
|
|
? ltr
|
|
? -verticalFactor
|
|
: -verticalFactor * 2
|
|
: 0),
|
|
);
|
|
canvas.scale(scale, scale);
|
|
// Rotate the label if it's vertical
|
|
if (vertical) canvas.rotate((ltr ? 1 : -1) * math.pi / 2);
|
|
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();
|
|
}
|
|
}
|