using System; using Unity.UIWidgets.animation; using Unity.UIWidgets.async; using Unity.UIWidgets.foundation; using Unity.UIWidgets.gestures; using Unity.UIWidgets.painting; using Unity.UIWidgets.rendering; using Unity.UIWidgets.scheduler; using Unity.UIWidgets.ui; using Unity.UIWidgets.widgets; using UnityEngine; using Canvas = Unity.UIWidgets.ui.Canvas; using Color = Unity.UIWidgets.ui.Color; using Rect = Unity.UIWidgets.ui.Rect; namespace Unity.UIWidgets.material { public class Slider : StatefulWidget { public Slider( Key key = null, float? value = null, ValueChanged onChanged = null, ValueChanged onChangeStart = null, ValueChanged onChangeEnd = null, float min = 0.0f, float max = 1.0f, int? divisions = null, string label = null, Color activeColor = null, Color inactiveColor = null ) : base(key: key) { D.assert(value != null); D.assert(min <= max); D.assert(value >= min && value <= max); D.assert(divisions == null || divisions > 0); this.value = value.Value; this.onChanged = onChanged; this.onChangeStart = onChangeStart; this.onChangeEnd = onChangeEnd; this.min = min; this.max = max; this.divisions = divisions; this.label = label; this.activeColor = activeColor; this.inactiveColor = inactiveColor; } public readonly float value; public readonly ValueChanged onChanged; public readonly ValueChanged onChangeStart; public readonly ValueChanged onChangeEnd; public readonly float min; public readonly float max; public readonly int? divisions; public readonly string label; public readonly Color activeColor; public readonly Color inactiveColor; public override State createState() { return new _SliderState(); } public override void debugFillProperties(DiagnosticPropertiesBuilder properties) { base.debugFillProperties(properties); properties.add(new FloatProperty("value", this.value)); properties.add(new FloatProperty("min", this.min)); properties.add(new FloatProperty("max", this.max)); } } class _SliderState : TickerProviderStateMixin { static TimeSpan enableAnimationDuration = new TimeSpan(0, 0, 0, 0, 75); static TimeSpan valueIndicatorAnimationDuration = new TimeSpan(0, 0, 0, 0, 100); public AnimationController overlayController; public AnimationController valueIndicatorController; public AnimationController enableController; public AnimationController positionController; public Timer interactionTimer; public override void initState() { base.initState(); this.overlayController = new AnimationController( duration: Constants.kRadialReactionDuration, vsync: this ); this.valueIndicatorController = new AnimationController( duration: valueIndicatorAnimationDuration, vsync: this ); this.enableController = new AnimationController( duration: enableAnimationDuration, vsync: this ); this.positionController = new AnimationController( duration: TimeSpan.Zero, vsync: this ); this.enableController.setValue(this.widget.onChanged != null ? 1.0f : 0.0f); this.positionController.setValue(this._unlerp(this.widget.value)); } public override void dispose() { this.interactionTimer?.cancel(); this.overlayController.dispose(); this.valueIndicatorController.dispose(); this.enableController.dispose(); this.positionController.dispose(); base.dispose(); } void _handleChanged(float value) { D.assert(this.widget.onChanged != null); float lerpValue = this._lerp(value); if (lerpValue != this.widget.value) { this.widget.onChanged(lerpValue); } } void _handleDragStart(float value) { D.assert(this.widget.onChangeStart != null); this.widget.onChangeStart(this._lerp(value)); } void _handleDragEnd(float value) { D.assert(this.widget.onChangeEnd != null); this.widget.onChangeEnd(this._lerp(value)); } float _lerp(float value) { D.assert(value >= 0.0f); D.assert(value <= 1.0f); return value * (this.widget.max - this.widget.min) + this.widget.min; } float _unlerp(float value) { D.assert(value <= this.widget.max); D.assert(value >= this.widget.min); return this.widget.max > this.widget.min ? (value - this.widget.min) / (this.widget.max - this.widget.min) : 0.0f; } public override Widget build(BuildContext context) { D.assert(MaterialD.debugCheckHasMaterial(context)); D.assert(WidgetsD.debugCheckHasMediaQuery(context)); SliderThemeData sliderTheme = SliderTheme.of(context); if (this.widget.activeColor != null || this.widget.inactiveColor != null) { sliderTheme = sliderTheme.copyWith( activeTrackColor: this.widget.activeColor, inactiveTrackColor: this.widget.inactiveColor, activeTickMarkColor: this.widget.inactiveColor, inactiveTickMarkColor: this.widget.activeColor, thumbColor: this.widget.activeColor, valueIndicatorColor: this.widget.activeColor, overlayColor: this.widget.activeColor?.withAlpha(0x29) ); } return new _SliderRenderObjectWidget( value: this._unlerp(this.widget.value), divisions: this.widget.divisions, label: this.widget.label, sliderTheme: sliderTheme, mediaQueryData: MediaQuery.of(context), onChanged: (this.widget.onChanged != null) && (this.widget.max > this.widget.min) ? this._handleChanged : (ValueChanged) null, onChangeStart: this.widget.onChangeStart != null ? this._handleDragStart : (ValueChanged) null, onChangeEnd: this.widget.onChangeEnd != null ? this._handleDragEnd : (ValueChanged) null, state: this ); } } class _SliderRenderObjectWidget : LeafRenderObjectWidget { public _SliderRenderObjectWidget( Key key = null, float? value = null, int? divisions = null, string label = null, SliderThemeData sliderTheme = null, MediaQueryData mediaQueryData = null, ValueChanged onChanged = null, ValueChanged onChangeStart = null, ValueChanged onChangeEnd = null, _SliderState state = null ) : base(key: key) { this.value = value.Value; this.divisions = divisions; this.label = label; this.sliderTheme = sliderTheme; this.mediaQueryData = mediaQueryData; this.onChanged = onChanged; this.onChangeStart = onChangeStart; this.onChangeEnd = onChangeEnd; this.state = state; } public readonly float value; public readonly int? divisions; public readonly string label; public readonly SliderThemeData sliderTheme; public readonly MediaQueryData mediaQueryData; public readonly ValueChanged onChanged; public readonly ValueChanged onChangeStart; public readonly ValueChanged onChangeEnd; public readonly _SliderState state; public override RenderObject createRenderObject(BuildContext context) { return new _RenderSlider( value: this.value, divisions: this.divisions, label: this.label, sliderTheme: this.sliderTheme, theme: Theme.of(context), mediaQueryData: this.mediaQueryData, onChanged: this.onChanged, onChangeStart: this.onChangeStart, onChangeEnd: this.onChangeEnd, state: this.state, platform: Theme.of(context).platform ); } public override void updateRenderObject(BuildContext context, RenderObject renderObject) { _RenderSlider _renderObject = (_RenderSlider) renderObject; _renderObject.value = this.value; _renderObject.divisions = this.divisions; _renderObject.label = this.label; _renderObject.sliderTheme = this.sliderTheme; _renderObject.theme = Theme.of(context); _renderObject.mediaQueryData = this.mediaQueryData; _renderObject.onChanged = this.onChanged; _renderObject.onChangeStart = this.onChangeStart; _renderObject.onChangeEnd = this.onChangeEnd; _renderObject.platform = Theme.of(context).platform; } } class _RenderSlider : RenderBox { public _RenderSlider( float? value = null, int? divisions = null, string label = null, SliderThemeData sliderTheme = null, ThemeData theme = null, MediaQueryData mediaQueryData = null, RuntimePlatform? platform = null, ValueChanged onChanged = null, ValueChanged onChangeStart = null, ValueChanged onChangeEnd = null, _SliderState state = null ) { D.assert(value != null && value >= 0.0 && value <= 1.0); D.assert(state != null); this.onChangeStart = onChangeStart; this.onChangeEnd = onChangeEnd; this._platform = platform; this._label = label; this._value = value.Value; this._divisions = divisions; this._sliderTheme = sliderTheme; this._theme = theme; this._mediaQueryData = mediaQueryData; this._onChanged = onChanged; this._state = state; this._updateLabelPainter(); GestureArenaTeam team = new GestureArenaTeam(); this._drag = new HorizontalDragGestureRecognizer { team = team, onStart = this._handleDragStart, onUpdate = this._handleDragUpdate, onEnd = this._handleDragEnd, onCancel = this._endInteraction }; this._tap = new TapGestureRecognizer { team = team, onTapDown = this._handleTapDown, onTapUp = this._handleTapUp, onTapCancel = this._endInteraction }; this._overlayAnimation = new CurvedAnimation( parent: this._state.overlayController, curve: Curves.fastOutSlowIn); this._valueIndicatorAnimation = new CurvedAnimation( parent: this._state.valueIndicatorController, curve: Curves.fastOutSlowIn); this._enableAnimation = new CurvedAnimation( parent: this._state.enableController, curve: Curves.easeInOut); } const float _positionAnimationDurationMilliSeconds = 75; const float _overlayRadius = 16.0f; const float _overlayDiameter = _overlayRadius * 2.0f; const float _trackHeight = 2.0f; const float _preferredTrackWidth = 144.0f; const float _preferredTotalWidth = _preferredTrackWidth + _overlayDiameter; const float _minimumInteractionTimeMilliSeconds = 500; static readonly Animatable _overlayRadiusTween = new FloatTween(begin: 0.0f, end: _overlayRadius); _SliderState _state; Animation _overlayAnimation; Animation _valueIndicatorAnimation; Animation _enableAnimation; TextPainter _labelPainter = new TextPainter(); HorizontalDragGestureRecognizer _drag; TapGestureRecognizer _tap; bool _active = false; float _currentDragValue = 0.0f; float _trackLength { get { return this.size.width - _overlayDiameter; } } bool isInteractive { get { return this.onChanged != null; } } bool isDiscrete { get { return this.divisions != null && this.divisions.Value > 0; } } public float value { get { return this._value; } set { D.assert(value != null && value >= 0.0f && value <= 1.0f); float convertedValue = this.isDiscrete ? this._discretize(value) : value; if (convertedValue == this._value) { return; } this._value = convertedValue; if (this.isDiscrete) { float distance = (this._value - this._state.positionController.value).abs(); this._state.positionController.duration = distance != 0.0f ? new TimeSpan(0, 0, 0, 0, (int) (_positionAnimationDurationMilliSeconds * (1.0f / distance))) : TimeSpan.Zero; this._state.positionController.animateTo(convertedValue, curve: Curves.easeInOut); } else { this._state.positionController.setValue(convertedValue); } } } float _value; public RuntimePlatform? platform { get { return this._platform; } set { if (this._platform == value) { return; } this._platform = value; } } RuntimePlatform? _platform; public int? divisions { get { return this._divisions; } set { if (value == this._divisions) { return; } this._divisions = value; this.markNeedsPaint(); } } int? _divisions; public string label { get { return this._label; } set { if (value == this._label) { return; } this._label = value; this._updateLabelPainter(); } } string _label; public SliderThemeData sliderTheme { get { return this._sliderTheme; } set { if (value == this._sliderTheme) { return; } this._sliderTheme = value; this.markNeedsPaint(); } } SliderThemeData _sliderTheme; public ThemeData theme { get { return this._theme; } set { if (value == this._theme) { return; } this._theme = value; this.markNeedsPaint(); } } ThemeData _theme; public MediaQueryData mediaQueryData { get { return this._mediaQueryData; } set { if (value == this._mediaQueryData) { return; } this._mediaQueryData = value; this._updateLabelPainter(); } } MediaQueryData _mediaQueryData; public ValueChanged onChanged { get { return this._onChanged; } set { if (value == this._onChanged) { return; } bool wasInteractive = this.isInteractive; this._onChanged = value; if (wasInteractive != this.isInteractive) { if (this.isInteractive) { this._state.enableController.forward(); } else { this._state.enableController.reverse(); } this.markNeedsPaint(); } } } ValueChanged _onChanged; public ValueChanged onChangeStart; public ValueChanged onChangeEnd; public bool showValueIndicator { get { bool showValueIndicator = false; switch (this._sliderTheme.showValueIndicator) { case ShowValueIndicator.onlyForDiscrete: showValueIndicator = this.isDiscrete; break; case ShowValueIndicator.onlyForContinuous: showValueIndicator = !this.isDiscrete; break; case ShowValueIndicator.always: showValueIndicator = true; break; case ShowValueIndicator.never: showValueIndicator = false; break; } return showValueIndicator; } } float _adjustmentUnit { get { switch (this._platform) { case RuntimePlatform.IPhonePlayer: return 0.1f; default: return 0.05f; } } } void _updateLabelPainter() { if (this.label != null) { this._labelPainter.text = new TextSpan( style: this._sliderTheme.valueIndicatorTextStyle, text: this.label ); this._labelPainter.textScaleFactor = this._mediaQueryData.textScaleFactor; this._labelPainter.layout(); } else { this._labelPainter.text = null; } this.markNeedsLayout(); } public override void attach(object owner) { base.attach(owner); this._overlayAnimation.addListener(this.markNeedsPaint); this._valueIndicatorAnimation.addListener(this.markNeedsPaint); this._enableAnimation.addListener(this.markNeedsPaint); this._state.positionController.addListener(this.markNeedsPaint); } public override void detach() { this._overlayAnimation.removeListener(this.markNeedsPaint); this._valueIndicatorAnimation.removeListener(this.markNeedsPaint); this._enableAnimation.removeListener(this.markNeedsPaint); this._state.positionController.removeListener(this.markNeedsPaint); base.detach(); } float _getValueFromVisualPosition(float visualPosition) { return visualPosition; } float _getValueFromGlobalPosition(Offset globalPosition) { float visualPosition = (this.globalToLocal(globalPosition).dx - _overlayRadius) / this._trackLength; return this._getValueFromVisualPosition(visualPosition); } float _discretize(float value) { float result = value.clamp(0.0f, 1.0f); if (this.isDiscrete) { result = (result * this.divisions.Value).round() / this.divisions.Value; } return result; } void _startInteraction(Offset globalPosition) { if (this.isInteractive) { this._active = true; if (this.onChangeStart != null) { this.onChangeStart(this._discretize(this.value)); } this._currentDragValue = this._getValueFromGlobalPosition(globalPosition); this.onChanged(this._discretize(this._currentDragValue)); this._state.overlayController.forward(); if (this.showValueIndicator) { this._state.valueIndicatorController.forward(); this._state.interactionTimer?.cancel(); this._state.interactionTimer = Window.instance.run( new TimeSpan(0, 0, 0, 0, (int) (_minimumInteractionTimeMilliSeconds * SchedulerBinding.instance.timeDilation)), () => { this._state.interactionTimer = null; if (!this._active && this._state.valueIndicatorController.status == AnimationStatus.completed) { this._state.valueIndicatorController.reverse(); } }); } } } void _endInteraction() { if (this._active && this._state.mounted) { if (this.onChangeEnd != null) { this.onChangeEnd(this._discretize(this._currentDragValue)); } this._active = false; this._currentDragValue = 0.0f; this._state.overlayController.reverse(); if (this.showValueIndicator && this._state.interactionTimer == null) { this._state.valueIndicatorController.reverse(); } } } void _handleDragStart(DragStartDetails details) { this._startInteraction(details.globalPosition); } void _handleDragUpdate(DragUpdateDetails details) { if (this.isInteractive) { float valueDelta = details.primaryDelta.Value / this._trackLength; this._currentDragValue += valueDelta; this.onChanged(this._discretize(this._currentDragValue)); } } void _handleDragEnd(DragEndDetails details) { this._endInteraction(); } void _handleTapDown(TapDownDetails details) { this._startInteraction(details.globalPosition); } void _handleTapUp(TapUpDetails details) { this._endInteraction(); } protected override bool hitTestSelf(Offset position) { return true; } public override void handleEvent(PointerEvent evt, HitTestEntry entry) { D.assert(this.debugHandleEvent(evt, entry)); if (evt is PointerDownEvent && this.isInteractive) { this._drag.addPointer((PointerDownEvent) evt); this._tap.addPointer((PointerDownEvent) evt); } } protected override float computeMinIntrinsicWidth(float height) { return Mathf.Max(_overlayDiameter, this._sliderTheme.thumbShape.getPreferredSize(this.isInteractive, this.isDiscrete).width); } protected override float computeMaxIntrinsicWidth(float height) { return _preferredTotalWidth; } protected override float computeMinIntrinsicHeight(float width) { return _overlayDiameter; } protected override float computeMaxIntrinsicHeight(float width) { return _overlayDiameter; } protected override bool sizedByParent { get { return true; } } protected override void performResize() { this.size = new Size( this.constraints.hasBoundedWidth ? this.constraints.maxWidth : _preferredTotalWidth, this.constraints.hasBoundedHeight ? this.constraints.maxHeight : _overlayDiameter ); } void _paintTickMarks( Canvas canvas, Rect trackLeft, Rect trackRight, Paint leftPaint, Paint rightPaint) { if (this.isDiscrete) { const float tickRadius = _trackHeight / 2.0f; float trackWidth = trackRight.right - trackLeft.left; float dx = (trackWidth - _trackHeight) / this.divisions.Value; if (dx >= 3.0 * _trackHeight) { for (int i = 0; i <= this.divisions.Value; i += 1) { float left = trackLeft.left + i * dx; Offset center = new Offset(left + tickRadius, trackLeft.top + tickRadius); if (trackLeft.contains(center)) { canvas.drawCircle(center, tickRadius, leftPaint); } else if (trackRight.contains(center)) { canvas.drawCircle(center, tickRadius, rightPaint); } } } } } void _paintOverlay( Canvas canvas, Offset center) { Paint overlayPaint = new Paint {color = this._sliderTheme.overlayColor}; float radius = _overlayRadiusTween.evaluate(this._overlayAnimation); canvas.drawCircle(center, radius, overlayPaint); } public override void paint(PaintingContext context, Offset offset) { Canvas canvas = context.canvas; float trackLength = this.size.width - 2 * _overlayRadius; float value = this._state.positionController.value; ColorTween activeTrackEnableColor = new ColorTween(begin: this._sliderTheme.disabledActiveTrackColor, end: this._sliderTheme.activeTrackColor); ColorTween inactiveTrackEnableColor = new ColorTween(begin: this._sliderTheme.disabledInactiveTrackColor, end: this._sliderTheme.inactiveTrackColor); ColorTween activeTickMarkEnableColor = new ColorTween(begin: this._sliderTheme.disabledActiveTickMarkColor, end: this._sliderTheme.activeTickMarkColor); ColorTween inactiveTickMarkEnableColor = new ColorTween(begin: this._sliderTheme.disabledInactiveTickMarkColor, end: this._sliderTheme.inactiveTickMarkColor); Paint activeTrackPaint = new Paint {color = activeTrackEnableColor.evaluate(this._enableAnimation)}; Paint inactiveTrackPaint = new Paint {color = inactiveTrackEnableColor.evaluate(this._enableAnimation)}; Paint activeTickMarkPaint = new Paint {color = activeTickMarkEnableColor.evaluate(this._enableAnimation)}; Paint inactiveTickMarkPaint = new Paint {color = inactiveTickMarkEnableColor.evaluate(this._enableAnimation)}; float visualPosition = value; Paint leftTrackPaint = activeTrackPaint; Paint rightTrackPaint = inactiveTrackPaint; Paint leftTickMarkPaint = activeTickMarkPaint; Paint rightTickMarkPaint = inactiveTickMarkPaint; float trackRadius = _trackHeight / 2.0f; float thumbGap = 2.0f; float trackVerticalCenter = offset.dy + (this.size.height) / 2.0f; float trackLeft = offset.dx + _overlayRadius; float trackTop = trackVerticalCenter - trackRadius; float trackBottom = trackVerticalCenter + trackRadius; float trackRight = trackLeft + trackLength; float trackActive = trackLeft + trackLength * visualPosition; float thumbRadius = this._sliderTheme.thumbShape.getPreferredSize(this.isInteractive, this.isDiscrete).width / 2.0f; float trackActiveLeft = Mathf.Max(0.0f, trackActive - thumbRadius - thumbGap * (1.0f - this._enableAnimation.value)); float trackActiveRight = Mathf.Min(trackActive + thumbRadius + thumbGap * (1.0f - this._enableAnimation.value), trackRight); Rect trackLeftRect = Rect.fromLTRB(trackLeft, trackTop, trackActiveLeft, trackBottom); Rect trackRightRect = Rect.fromLTRB(trackActiveRight, trackTop, trackRight, trackBottom); Offset thumbCenter = new Offset(trackActive, trackVerticalCenter); if (visualPosition > 0.0) { canvas.drawRect(trackLeftRect, leftTrackPaint); } if (visualPosition < 1.0) { canvas.drawRect(trackRightRect, rightTrackPaint); } this._paintOverlay(canvas, thumbCenter); this._paintTickMarks( canvas, trackLeftRect, trackRightRect, leftTickMarkPaint, rightTickMarkPaint ); if (this.isInteractive && this.label != null && this._valueIndicatorAnimation.status != AnimationStatus.dismissed) { if (this.showValueIndicator) { this._sliderTheme.valueIndicatorShape.paint( context, thumbCenter, activationAnimation: this._valueIndicatorAnimation, enableAnimation: this._enableAnimation, isDiscrete: this.isDiscrete, labelPainter: this._labelPainter, parentBox: this, sliderTheme: this._sliderTheme, value: this._value ); } } this._sliderTheme.thumbShape.paint( context, thumbCenter, activationAnimation: this._valueIndicatorAnimation, enableAnimation: this._enableAnimation, isDiscrete: this.isDiscrete, labelPainter: this._labelPainter, parentBox: this, sliderTheme: this._sliderTheme, value: this._value ); } } }