Unverified Commit 22ea031e authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Fix ScrollbarPainter thumbExtent calculation and add padding (#31763)

- Fixed extentInside calculation in ScrollMetrics
- Added asserts to extentInside getter, as well as ScrollPosition.applyContentDimensions to enforce minScrollExtent <= maxScrollExtent
- Added padding to ScrollbarPainter, updated implementation. Took care of some edge cases.
- Changed some scroll bar constants on Cupertino side.
parent c926aae4
...@@ -8,15 +8,22 @@ import 'package:flutter/widgets.dart'; ...@@ -8,15 +8,22 @@ import 'package:flutter/widgets.dart';
// All values eyeballed. // All values eyeballed.
const Color _kScrollbarColor = Color(0x99777777); const Color _kScrollbarColor = Color(0x99777777);
const double _kScrollbarThickness = 2.5;
const double _kScrollbarMainAxisMargin = 4.0;
const double _kScrollbarCrossAxisMargin = 2.5;
const double _kScrollbarMinLength = 36.0; const double _kScrollbarMinLength = 36.0;
const double _kScrollbarMinOverscrollLength = 8.0; const double _kScrollbarMinOverscrollLength = 8.0;
const Radius _kScrollbarRadius = Radius.circular(1.25); const Radius _kScrollbarRadius = Radius.circular(1.25);
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 50); const Duration _kScrollbarTimeToFade = Duration(milliseconds: 50);
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250); const Duration _kScrollbarFadeDuration = Duration(milliseconds: 250);
// These values are measured using screenshots from an iPhone XR 12.1 simulator.
const double _kScrollbarThickness = 2.5;
// This is the amount of space from the top of a vertical scrollbar to the
// top edge of the scrollable, measured when the vertical scrollbar overscrolls
// to the top.
// TODO(LongCatIsLooong): fix https://github.com/flutter/flutter/issues/32175
const double _kScrollbarMainAxisMargin = 3.0;
const double _kScrollbarCrossAxisMargin = 3.0;
/// An iOS style scrollbar. /// An iOS style scrollbar.
/// ///
/// A scrollbar indicates which portion of a [Scrollable] widget is actually /// A scrollbar indicates which portion of a [Scrollable] widget is actually
...@@ -89,6 +96,7 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv ...@@ -89,6 +96,7 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
mainAxisMargin: _kScrollbarMainAxisMargin, mainAxisMargin: _kScrollbarMainAxisMargin,
crossAxisMargin: _kScrollbarCrossAxisMargin, crossAxisMargin: _kScrollbarCrossAxisMargin,
radius: _kScrollbarRadius, radius: _kScrollbarRadius,
padding: MediaQuery.of(context).padding,
minLength: _kScrollbarMinLength, minLength: _kScrollbarMinLength,
minOverscrollLength: _kScrollbarMinOverscrollLength, minOverscrollLength: _kScrollbarMinOverscrollLength,
); );
......
...@@ -104,6 +104,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin { ...@@ -104,6 +104,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
textDirection: _textDirection, textDirection: _textDirection,
thickness: _kScrollbarThickness, thickness: _kScrollbarThickness,
fadeoutOpacityAnimation: _fadeoutOpacityAnimation, fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
padding: MediaQuery.of(context).padding,
); );
} }
......
...@@ -123,8 +123,9 @@ abstract class ViewportOffset extends ChangeNotifier { ...@@ -123,8 +123,9 @@ abstract class ViewportOffset extends ChangeNotifier {
/// Called when the viewport's content extents are established. /// Called when the viewport's content extents are established.
/// ///
/// The arguments are the minimum and maximum scroll extents respectively. The /// The arguments are the minimum and maximum scroll extents respectively. The
/// minimum will be equal to or less than zero, the maximum will be equal to /// minimum will be equal to or less than the maximum. In the case of slivers,
/// or greater than zero. /// the minimum will be equal to or less than zero, the maximum will be equal
/// to or greater than zero.
/// ///
/// The maximum scroll extent has the viewport dimension subtracted from it. /// The maximum scroll extent has the viewport dimension subtracted from it.
/// For instance, if there is 100.0 pixels of scrollable content, and the /// For instance, if there is 100.0 pixels of scrollable content, and the
......
...@@ -60,14 +60,16 @@ abstract class ScrollMetrics { ...@@ -60,14 +60,16 @@ abstract class ScrollMetrics {
/// ///
/// The actual [pixels] value might be [outOfRange]. /// The actual [pixels] value might be [outOfRange].
/// ///
/// This value can be negative infinity, if the scroll is unbounded. /// This value should typically be non-null and less than or equal to
/// [maxScrollExtent]. It can be negative infinity, if the scroll is unbounded.
double get minScrollExtent; double get minScrollExtent;
/// The maximum in-range value for [pixels]. /// The maximum in-range value for [pixels].
/// ///
/// The actual [pixels] value might be [outOfRange]. /// The actual [pixels] value might be [outOfRange].
/// ///
/// This value can be infinity, if the scroll is unbounded. /// This value should typically be non-null and greater than or equal to
/// [minScrollExtent]. It can be infinity, if the scroll is unbounded.
double get maxScrollExtent; double get maxScrollExtent;
/// The current scroll position, in logical pixels along the [axisDirection]. /// The current scroll position, in logical pixels along the [axisDirection].
...@@ -90,25 +92,27 @@ abstract class ScrollMetrics { ...@@ -90,25 +92,27 @@ abstract class ScrollMetrics {
/// [maxScrollExtent]. /// [maxScrollExtent].
bool get atEdge => pixels == minScrollExtent || pixels == maxScrollExtent; bool get atEdge => pixels == minScrollExtent || pixels == maxScrollExtent;
/// The quantity of content conceptually "above" the currently visible content /// The quantity of content conceptually "above" the viewport in the scrollable.
/// of the viewport in the scrollable. This is the content above the content /// This is the content above the content described by [extentInside].
/// described by [extentInside].
double get extentBefore => math.max(pixels - minScrollExtent, 0.0); double get extentBefore => math.max(pixels - minScrollExtent, 0.0);
/// The quantity of visible content. /// The quantity of content conceptually "inside" the viewport in the scrollable.
/// ///
/// If [extentBefore] and [extentAfter] are non-zero, then this is typically /// The value is typically the height of the viewport when [outOfRange] is false.
/// the height of the viewport. It could be less if there is less content /// It could be less if there is less content visible than the size of the
/// visible than the size of the viewport. /// viewport, such as when overscrolling.
///
/// The value is always non-negative, and less than or equal to [viewportDimension].
double get extentInside { double get extentInside {
return math.min(pixels, maxScrollExtent) - return viewportDimension
math.max(pixels, minScrollExtent) + // "above" overscroll value
math.min(viewportDimension, maxScrollExtent - minScrollExtent); - (minScrollExtent - pixels).clamp(0, viewportDimension)
// "below" overscroll value
- (pixels - maxScrollExtent).clamp(0, viewportDimension);
} }
/// The quantity of content conceptually "below" the currently visible content /// The quantity of content conceptually "below" the viewport in the scrollable.
/// of the viewport in the scrollable. This is the content below the content /// This is the content below the content described by [extentInside].
/// described by [extentInside].
double get extentAfter => math.max(maxScrollExtent - pixels, 0.0); double get extentAfter => math.max(maxScrollExtent - pixels, 0.0);
} }
......
...@@ -450,6 +450,9 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -450,6 +450,9 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) || if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
!nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) || !nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
_didChangeViewportDimensionOrReceiveCorrection) { _didChangeViewportDimensionOrReceiveCorrection) {
assert(minScrollExtent != null);
assert(maxScrollExtent != null);
assert(minScrollExtent <= maxScrollExtent);
_minScrollExtent = minScrollExtent; _minScrollExtent = minScrollExtent;
_maxScrollExtent = maxScrollExtent; _maxScrollExtent = maxScrollExtent;
_haveDimensions = true; _haveDimensions = true;
......
...@@ -14,6 +14,10 @@ const double _kMinThumbExtent = 18.0; ...@@ -14,6 +14,10 @@ const double _kMinThumbExtent = 18.0;
/// A [CustomPainter] for painting scrollbars. /// A [CustomPainter] for painting scrollbars.
/// ///
/// The size of the scrollbar along its scroll direction is typically
/// proportional to the percentage of content completely visible on screen,
/// as long as its size isn't less than [minLength] and it isn't overscrolling.
///
/// Unlike [CustomPainter]s that subclasses [CustomPainter] and only repaint /// Unlike [CustomPainter]s that subclasses [CustomPainter] and only repaint
/// when [shouldRepaint] returns true (which requires this [CustomPainter] to /// when [shouldRepaint] returns true (which requires this [CustomPainter] to
/// be rebuilt), this painter has the added optimization of repainting and not /// be rebuilt), this painter has the added optimization of repainting and not
...@@ -43,18 +47,25 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -43,18 +47,25 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
@required this.textDirection, @required this.textDirection,
@required this.thickness, @required this.thickness,
@required this.fadeoutOpacityAnimation, @required this.fadeoutOpacityAnimation,
this.padding = EdgeInsets.zero,
this.mainAxisMargin = 0.0, this.mainAxisMargin = 0.0,
this.crossAxisMargin = 0.0, this.crossAxisMargin = 0.0,
this.radius, this.radius,
this.minLength = _kMinThumbExtent, this.minLength = _kMinThumbExtent,
this.minOverscrollLength = _kMinThumbExtent, double minOverscrollLength,
}) : assert(color != null), }) : assert(color != null),
assert(textDirection != null), assert(textDirection != null),
assert(thickness != null), assert(thickness != null),
assert(fadeoutOpacityAnimation != null), assert(fadeoutOpacityAnimation != null),
assert(mainAxisMargin != null), assert(mainAxisMargin != null),
assert(crossAxisMargin != null), assert(crossAxisMargin != null),
assert(minLength != null) { assert(minLength != null),
assert(minLength >= 0),
assert(minOverscrollLength == null || minOverscrollLength <= minLength),
assert(minOverscrollLength == null || minOverscrollLength >= 0),
assert(padding != null),
assert(padding.isNonNegative),
minOverscrollLength = minOverscrollLength ?? minLength {
fadeoutOpacityAnimation.addListener(notifyListeners); fadeoutOpacityAnimation.addListener(notifyListeners);
} }
...@@ -65,7 +76,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -65,7 +76,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// screen the scrollbar appears in (the trailing side). Mustn't be null. /// screen the scrollbar appears in (the trailing side). Mustn't be null.
final TextDirection textDirection; final TextDirection textDirection;
/// Thickness of the scrollbar in its cross-axis in pixels. Mustn't be null. /// Thickness of the scrollbar in its cross-axis in logical pixels. Mustn't be null.
final double thickness; final double thickness;
/// An opacity [Animation] that dictates the opacity of the thumb. /// An opacity [Animation] that dictates the opacity of the thumb.
...@@ -73,12 +84,15 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -73,12 +84,15 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// Mustn't be null. /// Mustn't be null.
final Animation<double> fadeoutOpacityAnimation; final Animation<double> fadeoutOpacityAnimation;
/// Distance from the scrollbar's start and end to the edge of the viewport in /// Distance from the scrollbar's start and end to the edge of the viewport
/// pixels. Mustn't be null. /// in logical pixels. It affects the amount of available paint area.
///
/// Mustn't be null and defaults to 0.
final double mainAxisMargin; final double mainAxisMargin;
/// Distance from the scrollbar's side to the nearest edge in pixels. Must not /// Distance from the scrollbar's side to the nearest edge in logical pixels.
/// be null. ///
/// Must not be null and defaults to 0.
final double crossAxisMargin; final double crossAxisMargin;
/// [Radius] of corners if the scrollbar should have rounded corners. /// [Radius] of corners if the scrollbar should have rounded corners.
...@@ -86,13 +100,40 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -86,13 +100,40 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// Scrollbar will be rectangular if [radius] is null. /// Scrollbar will be rectangular if [radius] is null.
final Radius radius; final Radius radius;
/// The smallest size the scrollbar can shrink to when the total scrollable /// The amount of space by which to inset the scrollbar's start and end, as
/// extent is large and the current visible viewport is small, and the /// well as its side to the nearest edge, in logical pixels.
/// viewport is not overscrolled. Mustn't be null. ///
/// This is typically set to the current [MediaQueryData.padding] to avoid
/// partial obstructions such as display notches. If you only want additional
/// margins around the scrollbar, see [mainAxisMargin].
///
/// Defaults to [EdgeInsets.zero]. Must not be null and offsets from all four
/// directions must be greater than or equal to zero.
final EdgeInsets padding;
/// The preferred smallest size the scrollbar can shrink to when the total
/// scrollable extent is large, the current visible viewport is small, and the
/// viewport is not overscrolled.
///
/// The size of the scrollbar may shrink to a smaller size than [minLength]
/// to fit in the available paint area. E.g., when [minLength] is
/// `double.infinity`, it will not be respected if [viewportDimension] and
/// [mainAxisMargin] are finite.
///
/// Mustn't be null and the value has to be within the range of 0 to
/// [minOverscrollLength], inclusive. Defaults to 18.0.
final double minLength; final double minLength;
/// The smallest size the scrollbar can shrink to when viewport is /// The preferred smallest size the scrollbar can shrink to when viewport is
/// overscrolled. Mustn't be null. /// overscrolled.
///
/// When overscrolling, the size of the scrollbar may shrink to a smaller size
/// than [minOverscrollLength] to fit in the available paint area. E.g., when
/// [minOverscrollLength] is `double.infinity`, it will not be respected if
/// the [viewportDimension] and [mainAxisMargin] are finite.
///
/// The value is less than or equal to [minLength] and greater than or equal to 0.
/// If unspecified or set to null, it will defaults to the value of [minLength].
final double minOverscrollLength; final double minOverscrollLength;
ScrollMetrics _lastMetrics; ScrollMetrics _lastMetrics;
...@@ -116,64 +157,67 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -116,64 +157,67 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
color.withOpacity(color.opacity * fadeoutOpacityAnimation.value); color.withOpacity(color.opacity * fadeoutOpacityAnimation.value);
} }
double _getThumbX(Size size) { void _paintThumbCrossAxis(Canvas canvas, Size size, double thumbOffset, double thumbExtent, AxisDirection direction) {
assert(textDirection != null); double x, y;
switch (textDirection) { Size thumbSize;
case TextDirection.rtl:
return crossAxisMargin;
case TextDirection.ltr:
return size.width - thickness - crossAxisMargin;
}
return null;
}
void _paintVerticalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) { switch (direction) {
final Offset thumbOrigin = Offset(_getThumbX(size), thumbOffset); case AxisDirection.down:
final Size thumbSize = Size(thickness, thumbExtent); thumbSize = Size(thickness, thumbExtent);
final Rect thumbRect = thumbOrigin & thumbSize; x = textDirection == TextDirection.rtl
if (radius == null) ? crossAxisMargin + padding.left
canvas.drawRect(thumbRect, _paint); : size.width - thickness - crossAxisMargin - padding.right;
else y = thumbOffset;
canvas.drawRRect(RRect.fromRectAndRadius(thumbRect, radius), _paint); break;
case AxisDirection.up:
thumbSize = Size(thickness, thumbExtent);
x = textDirection == TextDirection.rtl
? crossAxisMargin + padding.left
: size.width - thickness - crossAxisMargin - padding.right;
y = thumbOffset;
break;
case AxisDirection.left:
thumbSize = Size(thumbExtent, thickness);
x = thumbOffset;
y = size.height - thickness - crossAxisMargin - padding.bottom;
break;
case AxisDirection.right:
thumbSize = Size(thumbExtent, thickness);
x = thumbOffset;
y = size.height - thickness - crossAxisMargin - padding.bottom;
break;
} }
void _paintHorizontalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) { final Rect thumbRect = Offset(x, y) & thumbSize;
final Offset thumbOrigin = Offset(thumbOffset, size.height - thickness);
final Size thumbSize = Size(thumbExtent, thickness);
final Rect thumbRect = thumbOrigin & thumbSize;
if (radius == null) if (radius == null)
canvas.drawRect(thumbRect, _paint); canvas.drawRect(thumbRect, _paint);
else else
canvas.drawRRect(RRect.fromRectAndRadius(thumbRect, radius), _paint); canvas.drawRRect(RRect.fromRectAndRadius(thumbRect, radius), _paint);
} }
void _paintThumb( double _thumbExtent(
double before, double mainAxisPadding,
double inside, double extentInside,
double after, double contentExtent,
double viewport, double beforeExtent,
Canvas canvas, double afterExtent,
Size size, double trackExtent
void painter(Canvas canvas, Size size, double thumbOffset, double thumbExtent),
) { ) {
// Establish the minimum size possible.
double thumbExtent = math.min(viewport, minOverscrollLength);
if (before + inside + after > 0.0) {
// Thumb extent reflects fraction of content visible, as long as this // Thumb extent reflects fraction of content visible, as long as this
// isn't less than the absolute minimum size. // isn't less than the absolute minimum size.
final double fractionVisible = inside / (before + inside + after); // contentExtent >= viewportDimension, so (contentExtent - mainAxisPadding) > 0
thumbExtent = math.max( final double fractionVisible = ((extentInside - mainAxisPadding) / (contentExtent - mainAxisPadding))
thumbExtent, .clamp(0.0, 1.0);
viewport * fractionVisible - 2 * mainAxisMargin,
final double thumbExtent = math.max(
math.min(trackExtent, minOverscrollLength),
trackExtent * fractionVisible
); );
final double safeMinLength = math.min(minLength, trackExtent);
final double newMinLength = (beforeExtent > 0 && afterExtent > 0)
// Thumb extent is no smaller than minLength if scrolling normally. // Thumb extent is no smaller than minLength if scrolling normally.
if (before != 0.0 && after != 0.0) { ? safeMinLength
thumbExtent = math.max(
minLength,
thumbExtent,
);
}
// User is overscrolling. Thumb extent can be less than minLength // User is overscrolling. Thumb extent can be less than minLength
// but no smaller than minOverscrollLength. We can't use the // but no smaller than minOverscrollLength. We can't use the
// fractionVisible to produce intermediate values between minLength and // fractionVisible to produce intermediate values between minLength and
...@@ -185,20 +229,11 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -185,20 +229,11 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
// [0.8, 1.0] to [0.0, 1.0], so 0% to 20% of overscroll will produce // [0.8, 1.0] to [0.0, 1.0], so 0% to 20% of overscroll will produce
// values for the thumb that range between minLength and the smallest // values for the thumb that range between minLength and the smallest
// possible value, minOverscrollLength. // possible value, minOverscrollLength.
else { : safeMinLength * ((fractionVisible - 0.8).clamp(0.0, 0.2) / 0.2);
thumbExtent = math.max(
thumbExtent,
minLength * (((inside / viewport) - 0.8) / 0.2),
);
}
}
final double fractionPast = before / (before + after);
final double thumbOffset = (before + after > 0.0)
? fractionPast * (viewport - thumbExtent - 2 * mainAxisMargin) + mainAxisMargin
: mainAxisMargin;
painter(canvas, size, thumbOffset, thumbExtent); // The `thumbExtent` should be no greater than `trackSize`, otherwise
// the scrollbar may scroll towards the wrong direction.
return thumbExtent.clamp(newMinLength, trackExtent);
} }
@override @override
...@@ -213,20 +248,41 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -213,20 +248,41 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|| _lastMetrics == null || _lastMetrics == null
|| fadeoutOpacityAnimation.value == 0.0) || fadeoutOpacityAnimation.value == 0.0)
return; return;
switch (_lastAxisDirection) {
case AxisDirection.down: final bool isVertical = _lastAxisDirection == AxisDirection.down || _lastAxisDirection == AxisDirection.up;
_paintThumb(_lastMetrics.extentBefore, _lastMetrics.extentInside, _lastMetrics.extentAfter, size.height, canvas, size, _paintVerticalThumb); final bool isReversed = _lastAxisDirection == AxisDirection.up || _lastAxisDirection == AxisDirection.left;
break;
case AxisDirection.up: final double mainAxisPadding = isVertical ? padding.vertical : padding.horizontal;
_paintThumb(_lastMetrics.extentAfter, _lastMetrics.extentInside, _lastMetrics.extentBefore, size.height, canvas, size, _paintVerticalThumb); // The size of the scrollable area.
break; final double trackExtent = _lastMetrics.viewportDimension - 2 * mainAxisMargin - mainAxisPadding;
case AxisDirection.right:
_paintThumb(_lastMetrics.extentBefore, _lastMetrics.extentInside, _lastMetrics.extentAfter, size.width, canvas, size, _paintHorizontalThumb); // Skip painting if there's not enough space.
break; if (_lastMetrics.viewportDimension <= mainAxisPadding || trackExtent <= 0) {
case AxisDirection.left: return;
_paintThumb(_lastMetrics.extentAfter, _lastMetrics.extentInside, _lastMetrics.extentBefore, size.width, canvas, size, _paintHorizontalThumb);
break;
} }
final double totalContentExtent =
_lastMetrics.maxScrollExtent
- _lastMetrics.minScrollExtent
+ _lastMetrics.viewportDimension;
final double beforeExtent = isReversed ? _lastMetrics.extentAfter : _lastMetrics.extentBefore;
final double afterExtent = isReversed ? _lastMetrics.extentBefore : _lastMetrics.extentAfter;
final double thumbExtent = _thumbExtent(mainAxisPadding, _lastMetrics.extentInside, totalContentExtent,
beforeExtent, afterExtent, trackExtent);
final double beforePadding = isVertical ? padding.top : padding.left;
final double scrollableExtent = _lastMetrics.maxScrollExtent - _lastMetrics.minScrollExtent;
final double fractionPast = (scrollableExtent > 0)
? ((_lastMetrics.pixels - _lastMetrics.minScrollExtent) / scrollableExtent).clamp(0.0, 1.0)
: 0;
final double thumbOffset = (isReversed ? 1 - fractionPast : fractionPast) * (trackExtent - thumbExtent)
+ mainAxisMargin + beforePadding;
return _paintThumbCrossAxis(canvas, size, thumbOffset, thumbExtent, _lastAxisDirection);
} }
// Scrollbars are (currently) not interactive. // Scrollbars are (currently) not interactive.
...@@ -243,7 +299,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -243,7 +299,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|| mainAxisMargin != old.mainAxisMargin || mainAxisMargin != old.mainAxisMargin
|| crossAxisMargin != old.crossAxisMargin || crossAxisMargin != old.crossAxisMargin
|| radius != old.radius || radius != old.radius
|| minLength != old.minLength; || minLength != old.minLength
|| padding != old.padding;
} }
@override @override
......
...@@ -7,32 +7,90 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -7,32 +7,90 @@ import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
const Color _kScrollbarColor = Color(0x99777777);
// The `y` offset has to be larger than `ScrollDragController._bigThresholdBreakDistance`
// to prevent [motionStartDistanceThreshold] from affecting the actual drag distance.
const Offset _kGestureOffset = Offset(0, -25);
void main() { void main() {
testWidgets('Paints iOS spec', (WidgetTester tester) async { testWidgets('Paints iOS spec', (WidgetTester tester) async {
await tester.pumpWidget(const Directionality( await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData(),
child: CupertinoScrollbar( child: CupertinoScrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0), child: SizedBox(width: 4000.0, height: 4000.0),
), ),
), ),
)); ),
),
);
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect())); expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView))); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
await gesture.moveBy(const Offset(0.0, -10.0)); await gesture.moveBy(_kGestureOffset);
// Move back to original position. // Move back to original position.
await gesture.moveBy(const Offset(0.0, 10.0)); await gesture.moveBy(Offset.zero.translate(-_kGestureOffset.dx, -_kGestureOffset.dy));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(CupertinoScrollbar), paints..rrect( expect(find.byType(CupertinoScrollbar), paints..rrect(
color: const Color(0x99777777), color: _kScrollbarColor,
rrect: RRect.fromRectAndRadius( rrect: RRect.fromRectAndRadius(
const Rect.fromLTWH( const Rect.fromLTWH(
800.0 - 2.5 - 2.5, // Screen width - margin - thickness. 800.0 - 3 - 2.5, // Screen width - margin - thickness.
4.0, // Initial position is the top margin. 3.0, // Initial position is the top margin.
2.5, // Thickness. 2.5, // Thickness.
// Fraction in viewport * scrollbar height - top, bottom margin. // Fraction in viewport * scrollbar height - top, bottom margin.
600.0 / 4000.0 * 600.0 - 4.0 - 4.0, 600.0 / 4000.0 * (600.0 - 2 * 3),
),
const Radius.circular(1.25),
),
));
});
testWidgets('Paints iOS spec with nav bar', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.fromLTRB(0, 20, 0, 34),
),
child: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Title'),
backgroundColor: Color(0x11111111),
),
child: CupertinoScrollbar(
child: ListView(
children: const <Widget> [SizedBox(width: 4000, height: 4000)]
),
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
await gesture.moveBy(_kGestureOffset);
// Move back to original position.
await gesture.moveBy(Offset.zero.translate(-_kGestureOffset.dx, -_kGestureOffset.dy));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor,
rrect: RRect.fromRectAndRadius(
const Rect.fromLTWH(
800.0 - 3 - 2.5, // Screen width - margin - thickness.
44 + 20 + 3.0, // nav bar height + top margin
2.5, // Thickness.
// Fraction visible * (viewport size - padding - margin)
// where Fraction visible = (viewport size - padding) / content size
(600.0 - 34 - 44 - 20) / 4000.0 * (600.0 - 2 * 3 - 34 - 44 - 20),
), ),
const Radius.circular(1.25), const Radius.circular(1.25),
), ),
......
...@@ -9,14 +9,17 @@ import '../rendering/mock_canvas.dart'; ...@@ -9,14 +9,17 @@ import '../rendering/mock_canvas.dart';
void main() { void main() {
testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async { testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async {
await tester.pumpWidget(const Directionality( await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData(),
child: CupertinoScrollbar( child: CupertinoScrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(child: SizedBox(width: 4000.0, height: 4000.0)),
child: SizedBox(width: 4000.0, height: 4000.0),
), ),
), ),
)); ),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView))); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
await gesture.moveBy(const Offset(0.0, -10.0)); await gesture.moveBy(const Offset(0.0, -10.0));
await tester.pump(); await tester.pump();
...@@ -42,25 +45,4 @@ void main() { ...@@ -42,25 +45,4 @@ void main() {
color: const Color(0x15777777), color: const Color(0x15777777),
)); ));
}); });
testWidgets('Scrollbar is not smaller than minLength with large scroll views', (WidgetTester tester) async {
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.ltr,
child: CupertinoScrollbar(
child: SingleChildScrollView(
child: SizedBox(width: 800.0, height: 20000.0),
),
),
));
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
await gesture.moveBy(const Offset(0.0, -10.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
// Height is 36.0.
const Rect scrollbarRect = Rect.fromLTWH(795.0, 4.28659793814433, 2.5, 36.0);
expect(find.byType(CupertinoScrollbar), paints..rrect(
rrect: RRect.fromRectAndRadius(scrollbarRect, const Radius.circular(1.25)),
));
});
} }
...@@ -7,15 +7,26 @@ import 'package:flutter/material.dart'; ...@@ -7,15 +7,26 @@ import 'package:flutter/material.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
void main() { Widget _buildSingleChildScrollViewWithScrollbar({
testWidgets('Viewport basic test (LTR)', (WidgetTester tester) async { TextDirection textDirection = TextDirection.ltr,
await tester.pumpWidget(const Directionality( EdgeInsets padding = EdgeInsets.zero,
textDirection: TextDirection.ltr, Widget child}
) {
return Directionality(
textDirection: textDirection,
child: MediaQuery(
data: MediaQueryData(padding: padding),
child: Scrollbar( child: Scrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(child: child),
child: SizedBox(width: 4000.0, height: 4000.0),
), ),
), ),
);
}
void main() {
testWidgets('Viewport basic test (LTR)', (WidgetTester tester) async {
await tester.pumpWidget(_buildSingleChildScrollViewWithScrollbar(
child: const SizedBox(width: 4000.0, height: 4000.0),
)); ));
expect(find.byType(Scrollbar), isNot(paints..rect())); expect(find.byType(Scrollbar), isNot(paints..rect()));
await tester.fling(find.byType(SingleChildScrollView), const Offset(0.0, -10.0), 10.0); await tester.fling(find.byType(SingleChildScrollView), const Offset(0.0, -10.0), 10.0);
...@@ -23,16 +34,47 @@ void main() { ...@@ -23,16 +34,47 @@ void main() {
}); });
testWidgets('Viewport basic test (RTL)', (WidgetTester tester) async { testWidgets('Viewport basic test (RTL)', (WidgetTester tester) async {
await tester.pumpWidget(const Directionality( await tester.pumpWidget(_buildSingleChildScrollViewWithScrollbar(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: Scrollbar( child: const SizedBox(width: 4000.0, height: 4000.0),
child: SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0),
),
),
)); ));
expect(find.byType(Scrollbar), isNot(paints..rect())); expect(find.byType(Scrollbar), isNot(paints..rect()));
await tester.fling(find.byType(SingleChildScrollView), const Offset(0.0, -10.0), 10.0); await tester.fling(find.byType(SingleChildScrollView), const Offset(0.0, -10.0), 10.0);
expect(find.byType(Scrollbar), paints..rect(rect: const Rect.fromLTRB(0.0, 1.5, 6.0, 91.5))); expect(find.byType(Scrollbar), paints..rect(rect: const Rect.fromLTRB(0.0, 1.5, 6.0, 91.5)));
}); });
testWidgets('workds with MaterialApp and Scaffold', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.fromLTRB(0, 20, 0, 34)
),
child: Scaffold(
appBar: AppBar(title: const Text('Title')),
body: Scrollbar(
child: ListView(
children: const <Widget>[SizedBox(width: 4000, height: 4000)]
),
),
),
),
));
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(ListView)));
// On Android it should not overscroll.
await gesture.moveBy(const Offset(0, 100));
// Trigger fade in animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(Scrollbar), paints..rect(
rect: const Rect.fromLTWH(
800.0 - 6, // screen width - thickness
0, // the paint area starts from the bottom of the app bar
6, // thickness
// 56 being the height of the app bar
(600.0 - 56 - 34 - 20) / 4000 * (600 - 56 - 34 - 20),
),
));
});
} }
...@@ -19,10 +19,24 @@ class TestCanvas implements Canvas { ...@@ -19,10 +19,24 @@ class TestCanvas implements Canvas {
} }
} }
Widget _buildBoilerplate({
TextDirection textDirection = TextDirection.ltr,
EdgeInsets padding = EdgeInsets.zero,
Widget child
}) {
return Directionality(
textDirection: textDirection,
child: MediaQuery(
data: MediaQueryData(padding: padding),
child: child,
),
);
}
void main() { void main() {
testWidgets('Scrollbar doesn\'t show when tapping list', (WidgetTester tester) async { testWidgets('Scrollbar doesn\'t show when tapping list', (WidgetTester tester) async {
await tester.pumpWidget(Directionality( await tester.pumpWidget(
textDirection: TextDirection.ltr, _buildBoilerplate(
child: Center( child: Center(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
...@@ -45,8 +59,9 @@ void main() { ...@@ -45,8 +59,9 @@ void main() {
), ),
), ),
), ),
), )
)); )
);
SchedulerBinding.instance.debugAssertNoTransientCallbacks('Building a list with a scrollbar triggered an animation.'); SchedulerBinding.instance.debugAssertNoTransientCallbacks('Building a list with a scrollbar triggered an animation.');
await tester.tap(find.byType(ListView)); await tester.tap(find.byType(ListView));
...@@ -64,9 +79,8 @@ void main() { ...@@ -64,9 +79,8 @@ void main() {
}); });
testWidgets('ScrollbarPainter does not divide by zero', (WidgetTester tester) async { testWidgets('ScrollbarPainter does not divide by zero', (WidgetTester tester) async {
await tester.pumpWidget(Directionality( await tester.pumpWidget(
textDirection: TextDirection.ltr, _buildBoilerplate(child: Container(
child: Container(
height: 200.0, height: 200.0,
width: 300.0, width: 300.0,
child: Scrollbar( child: Scrollbar(
...@@ -76,8 +90,8 @@ void main() { ...@@ -76,8 +90,8 @@ void main() {
], ],
), ),
), ),
), ))
)); );
final CustomPaint custom = tester.widget(find.descendant( final CustomPaint custom = tester.widget(find.descendant(
of: find.byType(Scrollbar), of: find.byType(Scrollbar),
...@@ -107,8 +121,7 @@ void main() { ...@@ -107,8 +121,7 @@ void main() {
testWidgets('Adaptive scrollbar', (WidgetTester tester) async { testWidgets('Adaptive scrollbar', (WidgetTester tester) async {
Widget viewWithScroll(TargetPlatform platform) { Widget viewWithScroll(TargetPlatform platform) {
return Directionality( return _buildBoilerplate(
textDirection: TextDirection.ltr,
child: Theme( child: Theme(
data: ThemeData( data: ThemeData(
platform: platform platform: platform
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/src/physics/utils.dart' show nearEqual;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
const Color _kScrollbarColor = Color(0xFF123456);
const double _kThickness = 2.5;
const double _kMinThumbExtent = 18.0;
CustomPainter _buildPainter({
TextDirection textDirection = TextDirection.ltr,
EdgeInsets padding = EdgeInsets.zero,
Color color = _kScrollbarColor,
double thickness = _kThickness,
double mainAxisMargin = 0.0,
double crossAxisMargin = 0.0,
Radius radius,
double minLength = _kMinThumbExtent,
double minOverscrollLength,
ScrollMetrics scrollMetrics,
}) {
return ScrollbarPainter(
color: color,
textDirection: textDirection,
thickness: thickness,
padding: padding,
mainAxisMargin: mainAxisMargin,
crossAxisMargin: crossAxisMargin,
radius: radius,
minLength: minLength,
minOverscrollLength: minOverscrollLength ?? minLength,
fadeoutOpacityAnimation: kAlwaysCompleteAnimation,
)..update(scrollMetrics, scrollMetrics.axisDirection);
}
class _DrawRectOnceCanvas extends Mock implements Canvas { }
void main() {
final _DrawRectOnceCanvas testCanvas = _DrawRectOnceCanvas();
ScrollbarPainter painter;
Rect captureRect() => verify(testCanvas.drawRect(captureAny, any)).captured.single;
tearDown(() => painter = null);
final ScrollMetrics defaultMetrics = FixedScrollMetrics(
minScrollExtent: 0,
maxScrollExtent: 0,
pixels: 0,
viewportDimension: 100,
axisDirection: AxisDirection.down
);
test(
'Scrollbar is not smaller than minLength with large scroll views, '
'if minLength is small ',
() {
const double minLen = 3.5;
const Size size = Size(600, 10);
final ScrollMetrics metrics = defaultMetrics.copyWith(
maxScrollExtent: 100000,
viewportDimension: size.height,
);
// When overscroll.
painter = _buildPainter(
minLength: minLen,
minOverscrollLength: minLen,
scrollMetrics: metrics,
);
painter.paint(testCanvas, size);
final Rect rect0 = captureRect();
expect(rect0.top, 0);
expect(rect0.left, size.width - _kThickness);
expect(rect0.width, _kThickness);
expect(rect0.height >= minLen, true);
// When scroll normally.
const double newPixels = 1.0;
painter.update(metrics.copyWith(pixels: newPixels), metrics.axisDirection);
painter.paint(testCanvas, size);
final Rect rect1 = captureRect();
expect(rect1.left, size.width - _kThickness);
expect(rect1.width, _kThickness);
expect(rect1.height >= minLen, true);
}
);
test(
'When scrolling normally (no overscrolling), the size of the scrollbar stays the same, '
'and it scrolls evenly',
() {
const double viewportDimension = 23;
const double maxExtent = 100;
final ScrollMetrics startingMetrics = defaultMetrics.copyWith(
maxScrollExtent: maxExtent,
viewportDimension: viewportDimension,
);
const Size size = Size(600, viewportDimension);
const double minLen = 0;
painter = _buildPainter(
minLength: minLen,
minOverscrollLength: minLen,
scrollMetrics: defaultMetrics,
);
final List<ScrollMetrics> metricsList =
<ScrollMetrics> [startingMetrics.copyWith(pixels: 0.01)]
..addAll(List<ScrollMetrics>.generate(
(maxExtent/viewportDimension).round(),
(int index) => startingMetrics.copyWith(pixels: (index + 1) * viewportDimension),
).where((ScrollMetrics metrics) => !metrics.outOfRange))
..add(startingMetrics.copyWith(pixels: maxExtent - 0.01));
double lastCoefficient;
for(ScrollMetrics metrics in metricsList) {
painter.update(metrics, metrics.axisDirection);
painter.paint(testCanvas, size);
final Rect rect = captureRect();
final double newCoefficient = metrics.pixels/rect.top;
lastCoefficient ??= newCoefficient;
expect(rect.top >= 0, true);
expect(rect.bottom <= maxExtent, true);
expect(rect.left, size.width - _kThickness);
expect(rect.width, _kThickness);
expect(nearEqual(rect.height, viewportDimension * viewportDimension / (viewportDimension + maxExtent), 0.001), true);
expect(nearEqual(lastCoefficient, newCoefficient, 0.001), true);
}
}
);
test(
'mainAxisMargin is respected',
() {
const double viewportDimension = 23;
const double maxExtent = 100;
final ScrollMetrics startingMetrics = defaultMetrics.copyWith(
maxScrollExtent: maxExtent,
viewportDimension: viewportDimension,
);
const Size size = Size(600, viewportDimension);
const double minLen = 0;
const List<double> margins = <double> [-10, 1, viewportDimension/2 - 0.01];
for(double margin in margins) {
painter = _buildPainter(
mainAxisMargin: margin,
minLength: minLen,
scrollMetrics: defaultMetrics,
);
// Overscroll to double.negativeInfinity (top).
painter.update(
startingMetrics.copyWith(pixels: double.negativeInfinity),
startingMetrics.axisDirection,
);
painter.paint(testCanvas, size);
expect(captureRect().top, margin);
// Overscroll to double.infinity (down).
painter.update(
startingMetrics.copyWith(pixels: double.infinity),
startingMetrics.axisDirection,
);
painter.paint(testCanvas, size);
expect(size.height - captureRect().bottom, margin);
}
}
);
test(
'crossAxisMargin & text direction are respected',
() {
const double viewportDimension = 23;
const double maxExtent = 100;
final ScrollMetrics startingMetrics = defaultMetrics.copyWith(
maxScrollExtent: maxExtent,
viewportDimension: viewportDimension,
);
const Size size = Size(600, viewportDimension);
const double margin = 4;
for(TextDirection textDirection in TextDirection.values) {
painter = _buildPainter(
crossAxisMargin: margin,
scrollMetrics: startingMetrics,
textDirection: textDirection,
);
for(AxisDirection direction in AxisDirection.values) {
painter.update(
startingMetrics.copyWith(axisDirection: direction),
direction,
);
painter.paint(testCanvas, size);
final Rect rect = captureRect();
switch (direction) {
case AxisDirection.up:
case AxisDirection.down:
expect(
margin,
textDirection == TextDirection.ltr
? size.width - rect.right
: rect.left
);
break;
case AxisDirection.left:
case AxisDirection.right:
expect(margin, size.height - rect.bottom);
break;
}
}
}
}
);
group('Padding works for all scroll directions', () {
const EdgeInsets padding = EdgeInsets.fromLTRB(1, 2, 3, 4);
const Size size = Size(60, 80);
final ScrollMetrics metrics = defaultMetrics.copyWith(
minScrollExtent: -100,
maxScrollExtent: 240,
axisDirection: AxisDirection.down,
);
final ScrollbarPainter p = _buildPainter(
padding: padding,
scrollMetrics: metrics,
);
testWidgets('down', (WidgetTester tester) async {
p.update(
metrics.copyWith(
viewportDimension: size.height,
pixels: double.negativeInfinity,
),
AxisDirection.down,
);
// Top overscroll.
p.paint(testCanvas, size);
final Rect rect0 = captureRect();
expect(rect0.top, padding.top);
expect(size.width - rect0.right, padding.right);
// Bottom overscroll.
p.update(
metrics.copyWith(
viewportDimension: size.height,
pixels: double.infinity,
),
AxisDirection.down,
);
p.paint(testCanvas, size);
final Rect rect1 = captureRect();
expect(size.height - rect1.bottom, padding.bottom);
expect(size.width - rect1.right, padding.right);
});
testWidgets('up', (WidgetTester tester) async {
p.update(
metrics.copyWith(
viewportDimension: size.height,
pixels: double.infinity,
axisDirection: AxisDirection.up,
),
AxisDirection.up,
);
// Top overscroll.
p.paint(testCanvas, size);
final Rect rect0 = captureRect();
expect(rect0.top, padding.top);
expect(size.width - rect0.right, padding.right);
// Bottom overscroll.
p.update(
metrics.copyWith(
viewportDimension: size.height,
pixels: double.negativeInfinity,
axisDirection: AxisDirection.up,
),
AxisDirection.up,
);
p.paint(testCanvas, size);
final Rect rect1 = captureRect();
expect(size.height - rect1.bottom, padding.bottom);
expect(size.width - rect1.right, padding.right);
});
testWidgets('left', (WidgetTester tester) async {
p.update(
metrics.copyWith(
viewportDimension: size.width,
pixels: double.negativeInfinity,
axisDirection: AxisDirection.left,
),
AxisDirection.left,
);
// Right overscroll.
p.paint(testCanvas, size);
final Rect rect0 = captureRect();
expect(size.height - rect0.bottom, padding.bottom);
expect(size.width - rect0.right, padding.right);
// Left overscroll.
p.update(
metrics.copyWith(
viewportDimension: size.width,
pixels: double.infinity,
axisDirection: AxisDirection.left,
),
AxisDirection.left,
);
p.paint(testCanvas, size);
final Rect rect1 = captureRect();
expect(size.height - rect1.bottom, padding.bottom);
expect(rect1.left, padding.left);
});
testWidgets('right', (WidgetTester tester) async {
p.update(
metrics.copyWith(
viewportDimension: size.width,
pixels: double.infinity,
axisDirection: AxisDirection.right,
),
AxisDirection.right,
);
// Right overscroll.
p.paint(testCanvas, size);
final Rect rect0 = captureRect();
expect(size.height - rect0.bottom, padding.bottom);
expect(size.width - rect0.right, padding.right);
// Left overscroll.
p.update(
metrics.copyWith(
viewportDimension: size.width,
pixels: double.negativeInfinity,
axisDirection: AxisDirection.right,
),
AxisDirection.right,
);
p.paint(testCanvas, size);
final Rect rect1 = captureRect();
expect(size.height - rect1.bottom, padding.bottom);
expect(rect1.left, padding.left);
});
});
test('should scroll towards the right direction',
() {
const Size size = Size(60, 80);
const double maxScrollExtent = 240;
const double minScrollExtent = -100;
final ScrollMetrics startingMetrics = defaultMetrics.copyWith(
minScrollExtent: minScrollExtent,
maxScrollExtent: maxScrollExtent,
axisDirection: AxisDirection.down,
viewportDimension: size.height,
);
for(double minLength in <double> [_kMinThumbExtent, double.infinity]) {
// Disregard `minLength` and `minOverscrollLength` to keep
// scroll direction correct, if needed
painter = _buildPainter(
minLength: minLength,
minOverscrollLength: minLength,
scrollMetrics: startingMetrics,
);
final Iterable<ScrollMetrics> metricsList = Iterable<ScrollMetrics>.generate(
9999,
(int index) => startingMetrics.copyWith(pixels: minScrollExtent + index * size.height / 3)
)
.takeWhile((ScrollMetrics metrics) => !metrics.outOfRange);
Rect previousRect;
for(ScrollMetrics metrics in metricsList) {
painter.update(metrics, metrics.axisDirection);
painter.paint(testCanvas, size);
final Rect rect = captureRect();
if (previousRect != null) {
if (rect.height == size.height) {
// Size of the scrollbar is too large for the view port
expect(previousRect.top <= rect.top, true);
expect(previousRect.bottom <= rect.bottom, true);
} else {
// The scrollbar can fit in the view port.
expect(previousRect.top < rect.top, true);
expect(previousRect.bottom < rect.bottom, true);
}
}
previousRect = rect;
}
}
}
);
}
...@@ -73,7 +73,10 @@ class TestViewportScrollPosition extends ScrollPositionWithSingleContext { ...@@ -73,7 +73,10 @@ class TestViewportScrollPosition extends ScrollPositionWithSingleContext {
void main() { void main() {
testWidgets('Evil test of sliver features - 1', (WidgetTester tester) async { testWidgets('Evil test of sliver features - 1', (WidgetTester tester) async {
final GlobalKey centerKey = GlobalKey(); final GlobalKey centerKey = GlobalKey();
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: TestBehavior(), behavior: TestBehavior(),
...@@ -160,7 +163,9 @@ void main() { ...@@ -160,7 +163,9 @@ void main() {
), ),
), ),
), ),
)); ),
),
);
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position; final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1)); position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment