|
|
|
|
|
|
using Rect = Unity.UIWidgets.ui.Rect; |
|
|
|
|
|
|
|
namespace Unity.UIWidgets.widgets { |
|
|
|
|
|
|
|
static class ScrollbarPainterUtils { |
|
|
|
|
|
|
|
public const float _kMinThumbExtent = 18.0f; |
|
|
|
public const float _kMinInteractiveSize = 48.0f; |
|
|
|
} |
|
|
|
public class ScrollbarPainter : ChangeNotifier, CustomPainter { |
|
|
|
public ScrollbarPainter( |
|
|
|
Color color, |
|
|
|
|
|
|
EdgeInsets padding = null, |
|
|
|
float minLength = _kMinThumbExtent, |
|
|
|
float minOverscrollLength = _kMinThumbExtent |
|
|
|
float minLength = ScrollbarPainterUtils._kMinThumbExtent, |
|
|
|
float? minOverscrollLength = null |
|
|
|
this.color = color; |
|
|
|
this.textDirection = textDirection; |
|
|
|
D.assert(color != null); |
|
|
|
D.assert(fadeoutOpacityAnimation != null); |
|
|
|
D.assert(minLength >= 0); |
|
|
|
D.assert(minOverscrollLength == null || minOverscrollLength.Value <= minLength); |
|
|
|
D.assert(minOverscrollLength == null || minOverscrollLength.Value >= 0); |
|
|
|
padding = padding ?? EdgeInsets.zero; |
|
|
|
|
|
|
|
D.assert(padding.isNonNegative); |
|
|
|
_color = color; |
|
|
|
_textDirection = textDirection; |
|
|
|
_padding = padding; |
|
|
|
this.thickness = thickness; |
|
|
|
this.fadeoutOpacityAnimation = fadeoutOpacityAnimation; |
|
|
|
this.mainAxisMargin = mainAxisMargin; |
|
|
|
|
|
|
this.minOverscrollLength = minOverscrollLength; |
|
|
|
this.minOverscrollLength = minOverscrollLength ?? minLength; |
|
|
|
const float _kMinThumbExtent = 18.0f; |
|
|
|
public Color color { |
|
|
|
get { return _color; } |
|
|
|
set { |
|
|
|
D.assert(value != null); |
|
|
|
if (color == value) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
_color = value; |
|
|
|
notifyListeners(); |
|
|
|
} |
|
|
|
} |
|
|
|
Color _color; |
|
|
|
public Color color; |
|
|
|
public TextDirection? textDirection; |
|
|
|
public TextDirection textDirection { |
|
|
|
get { return _textDirection; } |
|
|
|
set { |
|
|
|
if (textDirection == value) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
_textDirection = value; |
|
|
|
notifyListeners(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
TextDirection _textDirection; |
|
|
|
|
|
|
|
public Animation<float> fadeoutOpacityAnimation; |
|
|
|
public float mainAxisMargin; |
|
|
|
public float crossAxisMargin; |
|
|
|
public readonly Animation<float> fadeoutOpacityAnimation; |
|
|
|
public readonly float mainAxisMargin; |
|
|
|
public readonly float crossAxisMargin; |
|
|
|
public float minLength; |
|
|
|
public float minOverscrollLength; |
|
|
|
|
|
|
|
public EdgeInsets padding { |
|
|
|
get { return _padding; } |
|
|
|
set { |
|
|
|
D.assert(value != null); |
|
|
|
if (padding == value) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
_padding = value; |
|
|
|
notifyListeners(); |
|
|
|
} |
|
|
|
} |
|
|
|
EdgeInsets _padding; |
|
|
|
|
|
|
|
public readonly float minLength; |
|
|
|
public readonly float minOverscrollLength; |
|
|
|
Rect _thumbRect; |
|
|
|
|
|
|
|
public void update(ScrollMetrics metrics, AxisDirection axisDirection) { |
|
|
|
_lastMetrics = metrics; |
|
|
|
|
|
|
|
|
|
|
void updateThickness(float nextThickness, Radius nextRadius) { |
|
|
|
thickness = nextThickness; |
|
|
|
radius = nextRadius; |
|
|
|
notifyListeners(); |
|
|
|
} |
|
|
|
|
|
|
|
Paint _paint { |
|
|
|
get { |
|
|
|
var paint = new Paint(); |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
void _paintThumbCrossAxis(Canvas canvas, Size size, float thumbOffset, float thumbExtent, AxisDirection direction) { |
|
|
|
float x = 0; |
|
|
|
float y = 0; |
|
|
|
Size thumbSize = Size.zero; |
|
|
|
float _getThumbX(Size size) { |
|
|
|
D.assert(textDirection != null); |
|
|
|
switch (textDirection) { |
|
|
|
case TextDirection.rtl: |
|
|
|
return crossAxisMargin; |
|
|
|
case TextDirection.ltr: |
|
|
|
return size.width - thickness - crossAxisMargin; |
|
|
|
switch (direction) { |
|
|
|
case AxisDirection.down: |
|
|
|
thumbSize = new Size(thickness, thumbExtent); |
|
|
|
x = textDirection == TextDirection.rtl |
|
|
|
? crossAxisMargin + padding.left |
|
|
|
: size.width - thickness - crossAxisMargin - padding.right; |
|
|
|
y = thumbOffset; |
|
|
|
break; |
|
|
|
case AxisDirection.up: |
|
|
|
thumbSize = new Size(thickness, thumbExtent); |
|
|
|
x = textDirection == TextDirection.rtl |
|
|
|
? crossAxisMargin + padding.left |
|
|
|
: size.width - thickness - crossAxisMargin - padding.right; |
|
|
|
y = thumbOffset; |
|
|
|
break; |
|
|
|
case AxisDirection.left: |
|
|
|
thumbSize = new Size(thumbExtent, thickness); |
|
|
|
x = thumbOffset; |
|
|
|
y = size.height - thickness - crossAxisMargin - padding.bottom; |
|
|
|
break; |
|
|
|
case AxisDirection.right: |
|
|
|
thumbSize = new Size(thumbExtent, thickness); |
|
|
|
x = thumbOffset; |
|
|
|
y = size.height - thickness - crossAxisMargin - padding.bottom; |
|
|
|
break; |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
void _paintVerticalThumb(Canvas canvas, Size size, float thumbOffset, float thumbExtent) { |
|
|
|
Offset thumbOrigin = new Offset(_getThumbX(size), thumbOffset); |
|
|
|
Size thumbSize = new Size(thickness, thumbExtent); |
|
|
|
Rect thumbRect = thumbOrigin & thumbSize; |
|
|
|
_thumbRect = new Offset(x, y) & thumbSize; |
|
|
|
canvas.drawRect(thumbRect, _paint); |
|
|
|
canvas.drawRect(_thumbRect, _paint); |
|
|
|
canvas.drawRRect(RRect.fromRectAndRadius(thumbRect, radius), _paint); |
|
|
|
canvas.drawRRect(RRect.fromRectAndRadius(_thumbRect, radius), _paint); |
|
|
|
|
|
|
|
float _thumbExtent() { |
|
|
|
float fractionVisible = ((_lastMetrics.extentInside() - _mainAxisPadding) / (_totalContentExtent - _mainAxisPadding)) |
|
|
|
.clamp(0.0f, 1.0f); |
|
|
|
void _paintHorizontalThumb(Canvas canvas, Size size, float thumbOffset, float thumbExtent) { |
|
|
|
Offset thumbOrigin = new Offset(thumbOffset, size.height - thickness); |
|
|
|
Size thumbSize = new Size(thumbExtent, thickness); |
|
|
|
Rect thumbRect = thumbOrigin & thumbSize; |
|
|
|
if (radius == null) { |
|
|
|
canvas.drawRect(thumbRect, _paint); |
|
|
|
} |
|
|
|
else { |
|
|
|
canvas.drawRRect(RRect.fromRectAndRadius(thumbRect, radius), _paint); |
|
|
|
} |
|
|
|
} |
|
|
|
float thumbExtent = Mathf.Max( |
|
|
|
Mathf.Min(_trackExtent, minOverscrollLength), |
|
|
|
_trackExtent * fractionVisible |
|
|
|
); |
|
|
|
public delegate void painterDelegate(Canvas canvas, Size size, float thumbOffset, float thumbExtent); |
|
|
|
|
|
|
|
void _paintThumb( |
|
|
|
float before, |
|
|
|
float inside, |
|
|
|
float after, |
|
|
|
float viewport, |
|
|
|
Canvas canvas, |
|
|
|
Size size, |
|
|
|
painterDelegate painter |
|
|
|
) { |
|
|
|
float thumbExtent = Mathf.Min(viewport, minOverscrollLength); |
|
|
|
|
|
|
|
if (before + inside + after > 0.0) { |
|
|
|
float fractionVisible = inside / (before + inside + after); |
|
|
|
thumbExtent = Mathf.Max( |
|
|
|
thumbExtent, |
|
|
|
viewport * fractionVisible - 2 * mainAxisMargin |
|
|
|
); |
|
|
|
|
|
|
|
if (before != 0.0 && after != 0.0) { |
|
|
|
thumbExtent = Mathf.Max( |
|
|
|
minLength, |
|
|
|
thumbExtent |
|
|
|
); |
|
|
|
} |
|
|
|
else { |
|
|
|
thumbExtent = Mathf.Max( |
|
|
|
thumbExtent, |
|
|
|
minLength * (((inside / viewport) - 0.8f) / 0.2f) |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
float fractionPast = before / (before + after); |
|
|
|
float thumbOffset = (before + after > 0.0) |
|
|
|
? fractionPast * (viewport - thumbExtent - 2 * mainAxisMargin) + mainAxisMargin |
|
|
|
: mainAxisMargin; |
|
|
|
|
|
|
|
painter(canvas, size, thumbOffset, thumbExtent); |
|
|
|
} |
|
|
|
float fractionOverscrolled = 1.0f - _lastMetrics.extentInside() / _lastMetrics.viewportDimension; |
|
|
|
float safeMinLength = Mathf.Min(minLength, _trackExtent); |
|
|
|
float newMinLength = (_beforeExtent > 0 && _afterExtent > 0) |
|
|
|
? safeMinLength |
|
|
|
: safeMinLength * (1.0f - fractionOverscrolled.clamp(0.0f, 0.2f) / 0.2f); |
|
|
|
|
|
|
|
return thumbExtent.clamp(newMinLength, _trackExtent); |
|
|
|
} |
|
|
|
|
|
|
|
public override void dispose() { |
|
|
|
|
|
|
|
|
|
|
bool _isVertical => _lastAxisDirection == AxisDirection.down || _lastAxisDirection == AxisDirection.up; |
|
|
|
bool _isReversed => _lastAxisDirection == AxisDirection.up || _lastAxisDirection == AxisDirection.left; |
|
|
|
float _beforeExtent => _isReversed ? _lastMetrics.extentAfter() : _lastMetrics.extentBefore(); |
|
|
|
float _afterExtent => _isReversed ? _lastMetrics.extentBefore() : _lastMetrics.extentAfter(); |
|
|
|
float _mainAxisPadding => _isVertical ? padding.vertical : padding.horizontal; |
|
|
|
float _trackExtent => _lastMetrics.viewportDimension - 2 * mainAxisMargin - _mainAxisPadding; |
|
|
|
|
|
|
|
float _totalContentExtent { |
|
|
|
get { |
|
|
|
return _lastMetrics.maxScrollExtent |
|
|
|
- _lastMetrics.minScrollExtent |
|
|
|
+ _lastMetrics.viewportDimension; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
float getTrackToScroll(float thumbOffsetLocal) { |
|
|
|
float scrollableExtent = _lastMetrics.maxScrollExtent - _lastMetrics.minScrollExtent; |
|
|
|
float thumbMovableExtent = _trackExtent - _thumbExtent(); |
|
|
|
return scrollableExtent * thumbOffsetLocal / thumbMovableExtent; |
|
|
|
} |
|
|
|
|
|
|
|
float _getScrollToTrack(ScrollMetrics metrics, float thumbExtent) { |
|
|
|
float scrollableExtent = metrics.maxScrollExtent - metrics.minScrollExtent; |
|
|
|
|
|
|
|
float fractionPast = (scrollableExtent > 0) |
|
|
|
? ((metrics.pixels - metrics.minScrollExtent) / scrollableExtent).clamp(0.0f, 1.0f) |
|
|
|
: 0; |
|
|
|
|
|
|
|
return (_isReversed ? 1 - fractionPast : fractionPast) * (_trackExtent - thumbExtent); |
|
|
|
} |
|
|
|
|| fadeoutOpacityAnimation.value == 0.0) { |
|
|
|
|| fadeoutOpacityAnimation.value == 0.0f) { |
|
|
|
switch (_lastAxisDirection) { |
|
|
|
case AxisDirection.down: |
|
|
|
_paintThumb(_lastMetrics.extentBefore(), _lastMetrics.extentInside(), |
|
|
|
_lastMetrics.extentAfter(), size.height, canvas, size, _paintVerticalThumb); |
|
|
|
break; |
|
|
|
case AxisDirection.up: |
|
|
|
_paintThumb(_lastMetrics.extentAfter(), _lastMetrics.extentInside(), |
|
|
|
_lastMetrics.extentBefore(), size.height, canvas, size, _paintVerticalThumb); |
|
|
|
break; |
|
|
|
case AxisDirection.right: |
|
|
|
_paintThumb(_lastMetrics.extentBefore(), _lastMetrics.extentInside(), |
|
|
|
_lastMetrics.extentAfter(), size.width, canvas, size, _paintHorizontalThumb); |
|
|
|
break; |
|
|
|
case AxisDirection.left: |
|
|
|
_paintThumb(_lastMetrics.extentAfter(), _lastMetrics.extentInside(), |
|
|
|
_lastMetrics.extentBefore(), size.width, canvas, size, _paintHorizontalThumb); |
|
|
|
break; |
|
|
|
if (_lastMetrics.viewportDimension <= _mainAxisPadding || _trackExtent <= 0) { |
|
|
|
return; |
|
|
|
|
|
|
|
float beforePadding = _isVertical ? padding.top : padding.left; |
|
|
|
float thumbExtent = _thumbExtent(); |
|
|
|
float thumbOffsetLocal = _getScrollToTrack(_lastMetrics, thumbExtent); |
|
|
|
float thumbOffset = thumbOffsetLocal + mainAxisMargin + beforePadding; |
|
|
|
|
|
|
|
_paintThumbCrossAxis(canvas, size, thumbOffset, thumbExtent, _lastAxisDirection.Value); |
|
|
|
} |
|
|
|
|
|
|
|
bool hitTestInteractive(Offset position) { |
|
|
|
if (_thumbRect == null) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (fadeoutOpacityAnimation.value == 0.0f) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
Rect interactiveThumbRect = _thumbRect.expandToInclude( |
|
|
|
Rect.fromCircle(center: _thumbRect.center, radius: ScrollbarPainterUtils._kMinInteractiveSize / 2) |
|
|
|
); |
|
|
|
return interactiveThumbRect.contains(position); |
|
|
|
return false; |
|
|
|
if (_thumbRect == null) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
if (fadeoutOpacityAnimation.value == 0.0f) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
return _thumbRect.contains(position); |
|
|
|
} |
|
|
|
|
|
|
|
public bool shouldRepaint(CustomPainter oldRaw) { |
|
|
|
|
|
|
|| mainAxisMargin != old.mainAxisMargin |
|
|
|
|| crossAxisMargin != old.crossAxisMargin |
|
|
|
|| radius != old.radius |
|
|
|
|| minLength != old.minLength; |
|
|
|
|| minLength != old.minLength |
|
|
|
|| padding != old.padding; |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|