mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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.
This commit is contained in:
parent
c926aae4c7
commit
22ea031e28
@ -8,15 +8,22 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
// All values eyeballed.
|
||||
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 _kScrollbarMinOverscrollLength = 8.0;
|
||||
const Radius _kScrollbarRadius = Radius.circular(1.25);
|
||||
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 50);
|
||||
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.
|
||||
///
|
||||
/// A scrollbar indicates which portion of a [Scrollable] widget is actually
|
||||
@ -89,6 +96,7 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
|
||||
mainAxisMargin: _kScrollbarMainAxisMargin,
|
||||
crossAxisMargin: _kScrollbarCrossAxisMargin,
|
||||
radius: _kScrollbarRadius,
|
||||
padding: MediaQuery.of(context).padding,
|
||||
minLength: _kScrollbarMinLength,
|
||||
minOverscrollLength: _kScrollbarMinOverscrollLength,
|
||||
);
|
||||
|
||||
@ -104,6 +104,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
|
||||
textDirection: _textDirection,
|
||||
thickness: _kScrollbarThickness,
|
||||
fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
|
||||
padding: MediaQuery.of(context).padding,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -123,8 +123,9 @@ abstract class ViewportOffset extends ChangeNotifier {
|
||||
/// Called when the viewport's content extents are established.
|
||||
///
|
||||
/// 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
|
||||
/// or greater than zero.
|
||||
/// minimum will be equal to or less than the maximum. In the case of slivers,
|
||||
/// 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.
|
||||
/// For instance, if there is 100.0 pixels of scrollable content, and the
|
||||
|
||||
@ -60,14 +60,16 @@ abstract class ScrollMetrics {
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// The maximum in-range value for [pixels].
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// The current scroll position, in logical pixels along the [axisDirection].
|
||||
@ -90,25 +92,27 @@ abstract class ScrollMetrics {
|
||||
/// [maxScrollExtent].
|
||||
bool get atEdge => pixels == minScrollExtent || pixels == maxScrollExtent;
|
||||
|
||||
/// The quantity of content conceptually "above" the currently visible content
|
||||
/// of the viewport in the scrollable. This is the content above the content
|
||||
/// described by [extentInside].
|
||||
/// The quantity of content conceptually "above" the viewport in the scrollable.
|
||||
/// This is the content above the content described by [extentInside].
|
||||
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 height of the viewport. It could be less if there is less content
|
||||
/// visible than the size of the viewport.
|
||||
/// The value is typically the height of the viewport when [outOfRange] is false.
|
||||
/// It could be less if there is less content visible than the size of the
|
||||
/// viewport, such as when overscrolling.
|
||||
///
|
||||
/// The value is always non-negative, and less than or equal to [viewportDimension].
|
||||
double get extentInside {
|
||||
return math.min(pixels, maxScrollExtent) -
|
||||
math.max(pixels, minScrollExtent) +
|
||||
math.min(viewportDimension, maxScrollExtent - minScrollExtent);
|
||||
return viewportDimension
|
||||
// "above" overscroll value
|
||||
- (minScrollExtent - pixels).clamp(0, viewportDimension)
|
||||
// "below" overscroll value
|
||||
- (pixels - maxScrollExtent).clamp(0, viewportDimension);
|
||||
}
|
||||
|
||||
/// The quantity of content conceptually "below" the currently visible content
|
||||
/// of the viewport in the scrollable. This is the content below the content
|
||||
/// described by [extentInside].
|
||||
/// The quantity of content conceptually "below" the viewport in the scrollable.
|
||||
/// This is the content below the content described by [extentInside].
|
||||
double get extentAfter => math.max(maxScrollExtent - pixels, 0.0);
|
||||
}
|
||||
|
||||
|
||||
@ -450,6 +450,9 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
||||
if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
|
||||
!nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
|
||||
_didChangeViewportDimensionOrReceiveCorrection) {
|
||||
assert(minScrollExtent != null);
|
||||
assert(maxScrollExtent != null);
|
||||
assert(minScrollExtent <= maxScrollExtent);
|
||||
_minScrollExtent = minScrollExtent;
|
||||
_maxScrollExtent = maxScrollExtent;
|
||||
_haveDimensions = true;
|
||||
|
||||
@ -14,6 +14,10 @@ const double _kMinThumbExtent = 18.0;
|
||||
|
||||
/// 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
|
||||
/// when [shouldRepaint] returns true (which requires this [CustomPainter] to
|
||||
/// be rebuilt), this painter has the added optimization of repainting and not
|
||||
@ -43,18 +47,25 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
||||
@required this.textDirection,
|
||||
@required this.thickness,
|
||||
@required this.fadeoutOpacityAnimation,
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.mainAxisMargin = 0.0,
|
||||
this.crossAxisMargin = 0.0,
|
||||
this.radius,
|
||||
this.minLength = _kMinThumbExtent,
|
||||
this.minOverscrollLength = _kMinThumbExtent,
|
||||
double minOverscrollLength,
|
||||
}) : assert(color != null),
|
||||
assert(textDirection != null),
|
||||
assert(thickness != null),
|
||||
assert(fadeoutOpacityAnimation != null),
|
||||
assert(mainAxisMargin != 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);
|
||||
}
|
||||
|
||||
@ -65,7 +76,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
||||
/// screen the scrollbar appears in (the trailing side). Mustn't be null.
|
||||
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;
|
||||
|
||||
/// An opacity [Animation] that dictates the opacity of the thumb.
|
||||
@ -73,12 +84,15 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
||||
/// Mustn't be null.
|
||||
final Animation<double> fadeoutOpacityAnimation;
|
||||
|
||||
/// Distance from the scrollbar's start and end to the edge of the viewport in
|
||||
/// pixels. Mustn't be null.
|
||||
/// Distance from the scrollbar's start and end to the edge of the viewport
|
||||
/// in logical pixels. It affects the amount of available paint area.
|
||||
///
|
||||
/// Mustn't be null and defaults to 0.
|
||||
final double mainAxisMargin;
|
||||
|
||||
/// Distance from the scrollbar's side to the nearest edge in pixels. Must not
|
||||
/// be null.
|
||||
/// Distance from the scrollbar's side to the nearest edge in logical pixels.
|
||||
///
|
||||
/// Must not be null and defaults to 0.
|
||||
final double crossAxisMargin;
|
||||
|
||||
/// [Radius] of corners if the scrollbar should have rounded corners.
|
||||
@ -86,13 +100,40 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
||||
/// Scrollbar will be rectangular if [radius] is null.
|
||||
final Radius radius;
|
||||
|
||||
/// The smallest size the scrollbar can shrink to when the total scrollable
|
||||
/// extent is large and the current visible viewport is small, and the
|
||||
/// viewport is not overscrolled. Mustn't be null.
|
||||
/// The amount of space by which to inset the scrollbar's start and end, as
|
||||
/// well as its side to the nearest edge, in logical pixels.
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// The smallest size the scrollbar can shrink to when viewport is
|
||||
/// overscrolled. Mustn't be null.
|
||||
/// The preferred smallest size the scrollbar can shrink to when viewport is
|
||||
/// 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;
|
||||
|
||||
ScrollMetrics _lastMetrics;
|
||||
@ -116,64 +157,67 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
||||
color.withOpacity(color.opacity * fadeoutOpacityAnimation.value);
|
||||
}
|
||||
|
||||
double _getThumbX(Size size) {
|
||||
assert(textDirection != null);
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
return crossAxisMargin;
|
||||
case TextDirection.ltr:
|
||||
return size.width - thickness - crossAxisMargin;
|
||||
void _paintThumbCrossAxis(Canvas canvas, Size size, double thumbOffset, double thumbExtent, AxisDirection direction) {
|
||||
double x, y;
|
||||
Size thumbSize;
|
||||
|
||||
switch (direction) {
|
||||
case AxisDirection.down:
|
||||
thumbSize = Size(thickness, thumbExtent);
|
||||
x = textDirection == TextDirection.rtl
|
||||
? crossAxisMargin + padding.left
|
||||
: size.width - thickness - crossAxisMargin - padding.right;
|
||||
y = thumbOffset;
|
||||
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;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _paintVerticalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) {
|
||||
final Offset thumbOrigin = Offset(_getThumbX(size), thumbOffset);
|
||||
final Size thumbSize = Size(thickness, thumbExtent);
|
||||
final Rect thumbRect = thumbOrigin & thumbSize;
|
||||
final Rect thumbRect = Offset(x, y) & thumbSize;
|
||||
if (radius == null)
|
||||
canvas.drawRect(thumbRect, _paint);
|
||||
else
|
||||
canvas.drawRRect(RRect.fromRectAndRadius(thumbRect, radius), _paint);
|
||||
}
|
||||
|
||||
void _paintHorizontalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) {
|
||||
final Offset thumbOrigin = Offset(thumbOffset, size.height - thickness);
|
||||
final Size thumbSize = Size(thumbExtent, thickness);
|
||||
final Rect thumbRect = thumbOrigin & thumbSize;
|
||||
if (radius == null)
|
||||
canvas.drawRect(thumbRect, _paint);
|
||||
else
|
||||
canvas.drawRRect(RRect.fromRectAndRadius(thumbRect, radius), _paint);
|
||||
}
|
||||
|
||||
void _paintThumb(
|
||||
double before,
|
||||
double inside,
|
||||
double after,
|
||||
double viewport,
|
||||
Canvas canvas,
|
||||
Size size,
|
||||
void painter(Canvas canvas, Size size, double thumbOffset, double thumbExtent),
|
||||
double _thumbExtent(
|
||||
double mainAxisPadding,
|
||||
double extentInside,
|
||||
double contentExtent,
|
||||
double beforeExtent,
|
||||
double afterExtent,
|
||||
double trackExtent
|
||||
) {
|
||||
// Establish the minimum size possible.
|
||||
double thumbExtent = math.min(viewport, minOverscrollLength);
|
||||
// Thumb extent reflects fraction of content visible, as long as this
|
||||
// isn't less than the absolute minimum size.
|
||||
// contentExtent >= viewportDimension, so (contentExtent - mainAxisPadding) > 0
|
||||
final double fractionVisible = ((extentInside - mainAxisPadding) / (contentExtent - mainAxisPadding))
|
||||
.clamp(0.0, 1.0);
|
||||
|
||||
if (before + inside + after > 0.0) {
|
||||
// Thumb extent reflects fraction of content visible, as long as this
|
||||
// isn't less than the absolute minimum size.
|
||||
final double fractionVisible = inside / (before + inside + after);
|
||||
thumbExtent = math.max(
|
||||
thumbExtent,
|
||||
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.
|
||||
if (before != 0.0 && after != 0.0) {
|
||||
thumbExtent = math.max(
|
||||
minLength,
|
||||
thumbExtent,
|
||||
);
|
||||
}
|
||||
? safeMinLength
|
||||
// User is overscrolling. Thumb extent can be less than minLength
|
||||
// but no smaller than minOverscrollLength. We can't use the
|
||||
// fractionVisible to produce intermediate values between minLength and
|
||||
@ -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
|
||||
// values for the thumb that range between minLength and the smallest
|
||||
// possible value, minOverscrollLength.
|
||||
else {
|
||||
thumbExtent = math.max(
|
||||
thumbExtent,
|
||||
minLength * (((inside / viewport) - 0.8) / 0.2),
|
||||
);
|
||||
}
|
||||
}
|
||||
: safeMinLength * ((fractionVisible - 0.8).clamp(0.0, 0.2) / 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
|
||||
@ -213,20 +248,41 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
||||
|| _lastMetrics == null
|
||||
|| fadeoutOpacityAnimation.value == 0.0)
|
||||
return;
|
||||
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;
|
||||
|
||||
final bool isVertical = _lastAxisDirection == AxisDirection.down || _lastAxisDirection == AxisDirection.up;
|
||||
final bool isReversed = _lastAxisDirection == AxisDirection.up || _lastAxisDirection == AxisDirection.left;
|
||||
|
||||
final double mainAxisPadding = isVertical ? padding.vertical : padding.horizontal;
|
||||
// The size of the scrollable area.
|
||||
final double trackExtent = _lastMetrics.viewportDimension - 2 * mainAxisMargin - mainAxisPadding;
|
||||
|
||||
// Skip painting if there's not enough space.
|
||||
if (_lastMetrics.viewportDimension <= mainAxisPadding || trackExtent <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.
|
||||
@ -243,7 +299,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
||||
|| mainAxisMargin != old.mainAxisMargin
|
||||
|| crossAxisMargin != old.crossAxisMargin
|
||||
|| radius != old.radius
|
||||
|| minLength != old.minLength;
|
||||
|| minLength != old.minLength
|
||||
|| padding != old.padding;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -7,32 +7,90 @@ import 'package:flutter_test/flutter_test.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() {
|
||||
testWidgets('Paints iOS spec', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoScrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(width: 4000.0, height: 4000.0),
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: MediaQueryData(),
|
||||
child: CupertinoScrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(width: 4000.0, height: 4000.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
|
||||
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
|
||||
|
||||
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.
|
||||
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(const Duration(milliseconds: 500));
|
||||
expect(find.byType(CupertinoScrollbar), paints..rrect(
|
||||
color: const Color(0x99777777),
|
||||
color: _kScrollbarColor,
|
||||
rrect: RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(
|
||||
800.0 - 2.5 - 2.5, // Screen width - margin - thickness.
|
||||
4.0, // Initial position is the top margin.
|
||||
800.0 - 3 - 2.5, // Screen width - margin - thickness.
|
||||
3.0, // Initial position is the top margin.
|
||||
2.5, // Thickness.
|
||||
// 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),
|
||||
),
|
||||
|
||||
@ -9,14 +9,17 @@ import '../rendering/mock_canvas.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoScrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(width: 4000.0, height: 4000.0),
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: MediaQuery(
|
||||
data: MediaQueryData(),
|
||||
child: CupertinoScrollbar(
|
||||
child: SingleChildScrollView(child: SizedBox(width: 4000.0, height: 4000.0)),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
|
||||
await gesture.moveBy(const Offset(0.0, -10.0));
|
||||
await tester.pump();
|
||||
@ -42,25 +45,4 @@ void main() {
|
||||
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';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
|
||||
Widget _buildSingleChildScrollViewWithScrollbar({
|
||||
TextDirection textDirection = TextDirection.ltr,
|
||||
EdgeInsets padding = EdgeInsets.zero,
|
||||
Widget child}
|
||||
) {
|
||||
return Directionality(
|
||||
textDirection: textDirection,
|
||||
child: MediaQuery(
|
||||
data: MediaQueryData(padding: padding),
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('Viewport basic test (LTR)', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(width: 4000.0, height: 4000.0),
|
||||
),
|
||||
),
|
||||
await tester.pumpWidget(_buildSingleChildScrollViewWithScrollbar(
|
||||
child: const SizedBox(width: 4000.0, height: 4000.0),
|
||||
));
|
||||
expect(find.byType(Scrollbar), isNot(paints..rect()));
|
||||
await tester.fling(find.byType(SingleChildScrollView), const Offset(0.0, -10.0), 10.0);
|
||||
@ -23,16 +34,47 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Viewport basic test (RTL)', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const Directionality(
|
||||
await tester.pumpWidget(_buildSingleChildScrollViewWithScrollbar(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(width: 4000.0, height: 4000.0),
|
||||
),
|
||||
),
|
||||
child: const SizedBox(width: 4000.0, height: 4000.0),
|
||||
));
|
||||
expect(find.byType(Scrollbar), isNot(paints..rect()));
|
||||
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)));
|
||||
});
|
||||
|
||||
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,34 +19,49 @@ 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() {
|
||||
testWidgets('Scrollbar doesn\'t show when tapping list', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFFFFF00))
|
||||
),
|
||||
height: 200.0,
|
||||
width: 300.0,
|
||||
child: Scrollbar(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 40.0, child: const Text('0')),
|
||||
Container(height: 40.0, child: const Text('1')),
|
||||
Container(height: 40.0, child: const Text('2')),
|
||||
Container(height: 40.0, child: const Text('3')),
|
||||
Container(height: 40.0, child: const Text('4')),
|
||||
Container(height: 40.0, child: const Text('5')),
|
||||
Container(height: 40.0, child: const Text('6')),
|
||||
Container(height: 40.0, child: const Text('7')),
|
||||
],
|
||||
await tester.pumpWidget(
|
||||
_buildBoilerplate(
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFFFFF00))
|
||||
),
|
||||
height: 200.0,
|
||||
width: 300.0,
|
||||
child: Scrollbar(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 40.0, child: const Text('0')),
|
||||
Container(height: 40.0, child: const Text('1')),
|
||||
Container(height: 40.0, child: const Text('2')),
|
||||
Container(height: 40.0, child: const Text('3')),
|
||||
Container(height: 40.0, child: const Text('4')),
|
||||
Container(height: 40.0, child: const Text('5')),
|
||||
Container(height: 40.0, child: const Text('6')),
|
||||
Container(height: 40.0, child: const Text('7')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
SchedulerBinding.instance.debugAssertNoTransientCallbacks('Building a list with a scrollbar triggered an animation.');
|
||||
await tester.tap(find.byType(ListView));
|
||||
@ -64,9 +79,8 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('ScrollbarPainter does not divide by zero', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
await tester.pumpWidget(
|
||||
_buildBoilerplate(child: Container(
|
||||
height: 200.0,
|
||||
width: 300.0,
|
||||
child: Scrollbar(
|
||||
@ -76,8 +90,8 @@ void main() {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
))
|
||||
);
|
||||
|
||||
final CustomPaint custom = tester.widget(find.descendant(
|
||||
of: find.byType(Scrollbar),
|
||||
@ -107,8 +121,7 @@ void main() {
|
||||
|
||||
testWidgets('Adaptive scrollbar', (WidgetTester tester) async {
|
||||
Widget viewWithScroll(TargetPlatform platform) {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
return _buildBoilerplate(
|
||||
child: Theme(
|
||||
data: ThemeData(
|
||||
platform: platform
|
||||
|
||||
424
packages/flutter/test/widgets/scrollbar_test.dart
Normal file
424
packages/flutter/test/widgets/scrollbar_test.dart
Normal file
@ -0,0 +1,424 @@
|
||||
// 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,94 +73,99 @@ class TestViewportScrollPosition extends ScrollPositionWithSingleContext {
|
||||
void main() {
|
||||
testWidgets('Evil test of sliver features - 1', (WidgetTester tester) async {
|
||||
final GlobalKey centerKey = GlobalKey();
|
||||
await tester.pumpWidget(Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ScrollConfiguration(
|
||||
behavior: TestBehavior(),
|
||||
child: Scrollbar(
|
||||
child: Scrollable(
|
||||
axisDirection: AxisDirection.down,
|
||||
physics: const TestScrollPhysics(),
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
return Viewport(
|
||||
await tester.pumpWidget(
|
||||
MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ScrollConfiguration(
|
||||
behavior: TestBehavior(),
|
||||
child: Scrollbar(
|
||||
child: Scrollable(
|
||||
axisDirection: AxisDirection.down,
|
||||
anchor: 0.25,
|
||||
offset: offset,
|
||||
center: centerKey,
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(50.0),
|
||||
sliver: SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), floating: true),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(key: centerKey, child: Container(height: 520.0)), // ------------------------ CENTER ------------------------
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(50.0),
|
||||
sliver: SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), floating: true),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), floating: true),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
]),
|
||||
),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 50.0),
|
||||
sliver: SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
],
|
||||
);
|
||||
},
|
||||
physics: const TestScrollPhysics(),
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
return Viewport(
|
||||
axisDirection: AxisDirection.down,
|
||||
anchor: 0.25,
|
||||
offset: offset,
|
||||
center: centerKey,
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(50.0),
|
||||
sliver: SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), floating: true),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(key: centerKey, child: Container(height: 520.0)), // ------------------------ CENTER ------------------------
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(50.0),
|
||||
sliver: SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), floating: true),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), floating: true),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
Container(height: 50.0),
|
||||
]),
|
||||
),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
|
||||
SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 50.0),
|
||||
sliver: SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 520.0)),
|
||||
SliverToBoxAdapter(child: Container(height: 5.0)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
|
||||
|
||||
position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user