1218 lines
38 KiB
Dart
1218 lines
38 KiB
Dart
// Copyright 2014 The Flutter 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 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/src/physics/utils.dart' show nearEqual;
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../rendering/mock_canvas.dart';
|
|
|
|
const Color _kScrollbarColor = Color(0xFF123456);
|
|
const double _kThickness = 2.5;
|
|
const double _kMinThumbExtent = 18.0;
|
|
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300);
|
|
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600);
|
|
|
|
ScrollbarPainter _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,
|
|
required 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 Fake implements Canvas {
|
|
List<Rect> rects = <Rect>[];
|
|
|
|
@override
|
|
void drawRect(Rect rect, Paint paint) {
|
|
rects.add(rect);
|
|
}
|
|
|
|
@override
|
|
void drawLine(Offset p1, Offset p2, Paint paint) {}
|
|
}
|
|
|
|
void main() {
|
|
final _DrawRectOnceCanvas testCanvas = _DrawRectOnceCanvas();
|
|
ScrollbarPainter painter;
|
|
|
|
Rect captureRect() => testCanvas.rects.removeLast();
|
|
|
|
tearDown(() {
|
|
testCanvas.rects.clear();
|
|
});
|
|
|
|
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),
|
|
...List<ScrollMetrics>.generate(
|
|
(maxExtent / viewportDimension).round(),
|
|
(int index) => startingMetrics.copyWith(pixels: (index + 1) * viewportDimension),
|
|
).where((ScrollMetrics metrics) => !metrics.outOfRange),
|
|
startingMetrics.copyWith(pixels: maxExtent - 0.01),
|
|
];
|
|
|
|
late double lastCoefficient;
|
|
for (final 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 (final 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 (final TextDirection textDirection in TextDirection.values) {
|
|
painter = _buildPainter(
|
|
crossAxisMargin: margin,
|
|
scrollMetrics: startingMetrics,
|
|
textDirection: textDirection,
|
|
);
|
|
|
|
for (final 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);
|
|
});
|
|
});
|
|
|
|
testWidgets('thumb resizes gradually on overscroll', (WidgetTester tester) async {
|
|
const EdgeInsets padding = EdgeInsets.fromLTRB(1, 2, 3, 4);
|
|
const Size size = Size(60, 300);
|
|
final double scrollExtent = size.height * 10;
|
|
final ScrollMetrics metrics = defaultMetrics.copyWith(
|
|
minScrollExtent: 0,
|
|
maxScrollExtent: scrollExtent,
|
|
axisDirection: AxisDirection.down,
|
|
viewportDimension: size.height,
|
|
);
|
|
|
|
const double minOverscrollLength = 8.0;
|
|
final ScrollbarPainter p = _buildPainter(
|
|
padding: padding,
|
|
scrollMetrics: metrics,
|
|
minLength: 36.0,
|
|
minOverscrollLength: 8.0,
|
|
);
|
|
|
|
// No overscroll gives a full sized thumb.
|
|
p.update(
|
|
metrics.copyWith(
|
|
pixels: 0.0,
|
|
),
|
|
AxisDirection.down,
|
|
);
|
|
p.paint(testCanvas, size);
|
|
final double fullThumbExtent = captureRect().height;
|
|
expect(fullThumbExtent, greaterThan(_kMinThumbExtent));
|
|
|
|
// Scrolling to the middle also gives a full sized thumb.
|
|
p.update(
|
|
metrics.copyWith(
|
|
pixels: scrollExtent / 2,
|
|
),
|
|
AxisDirection.down,
|
|
);
|
|
p.paint(testCanvas, size);
|
|
expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 1e-6));
|
|
|
|
// Scrolling just to the very end also gives a full sized thumb.
|
|
p.update(
|
|
metrics.copyWith(
|
|
pixels: scrollExtent,
|
|
),
|
|
AxisDirection.down,
|
|
);
|
|
p.paint(testCanvas, size);
|
|
expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 1e-6));
|
|
|
|
// Scrolling just past the end shrinks the thumb slightly.
|
|
p.update(
|
|
metrics.copyWith(
|
|
pixels: scrollExtent * 1.001,
|
|
),
|
|
AxisDirection.down,
|
|
);
|
|
p.paint(testCanvas, size);
|
|
expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 2.0));
|
|
|
|
// Scrolling way past the end shrinks the thumb to minimum.
|
|
p.update(
|
|
metrics.copyWith(
|
|
pixels: double.infinity,
|
|
),
|
|
AxisDirection.down,
|
|
);
|
|
p.paint(testCanvas, size);
|
|
expect(captureRect().height, minOverscrollLength);
|
|
});
|
|
|
|
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 (final 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 (final 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;
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
testWidgets('ScrollbarPainter asserts if no TextDirection has been provided', (WidgetTester tester) async {
|
|
final ScrollbarPainter painter = ScrollbarPainter(
|
|
color: _kScrollbarColor,
|
|
fadeoutOpacityAnimation: kAlwaysCompleteAnimation,
|
|
);
|
|
const Size size = Size(60, 80);
|
|
final ScrollMetrics scrollMetrics = defaultMetrics.copyWith(
|
|
maxScrollExtent: 100000,
|
|
viewportDimension: size.height,
|
|
);
|
|
painter.update(scrollMetrics, scrollMetrics.axisDirection);
|
|
// Try to paint the scrollbar
|
|
try {
|
|
painter.paint(testCanvas, size);
|
|
} on AssertionError catch (error) {
|
|
expect(error.message, 'A TextDirection must be provided before a Scrollbar can be painted.');
|
|
}
|
|
});
|
|
|
|
testWidgets('Tapping the track area pages the Scroll View', (WidgetTester tester) async {
|
|
final ScrollController scrollController = ScrollController();
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: RawScrollbar(
|
|
isAlwaysShown: true,
|
|
controller: scrollController,
|
|
child: SingleChildScrollView(
|
|
controller: scrollController,
|
|
child: const SizedBox(width: 1000.0, height: 1000.0),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 360.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
// Tap on the track area below the thumb.
|
|
await tester.tapAt(const Offset(796.0, 550.0));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(scrollController.offset, 400.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 240.0, 800.0, 600.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
// Tap on the track area above the thumb.
|
|
await tester.tapAt(const Offset(796.0, 50.0));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 360.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData(),
|
|
child: RawScrollbar(
|
|
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, -20.0));
|
|
await tester.pump();
|
|
// Scrollbar fully showing
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
await tester.pump(const Duration(seconds: 3));
|
|
await tester.pump(const Duration(seconds: 3));
|
|
// Still there.
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
await gesture.up();
|
|
await tester.pump(_kScrollbarTimeToFade);
|
|
await tester.pump(_kScrollbarFadeDuration * 0.5);
|
|
|
|
// Opacity going down now.
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0),
|
|
color: const Color(0x4fbcbcbc),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Scrollbar does not fade away while hovering', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData(),
|
|
child: RawScrollbar(
|
|
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, -20.0));
|
|
await tester.pump();
|
|
// Scrollbar fully showing
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
final TestPointer testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Hover over the thumb to prevent the scrollbar from fading out.
|
|
testPointer.hover(const Offset(790.0, 5.0));
|
|
await gesture.up();
|
|
await tester.pump(const Duration(seconds: 3));
|
|
|
|
// Still there.
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Scrollbar will fade back in when hovering over known track area', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: MediaQueryData(),
|
|
child: RawScrollbar(
|
|
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, -20.0));
|
|
await tester.pump();
|
|
// Scrollbar fully showing
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
await gesture.up();
|
|
await tester.pump(_kScrollbarTimeToFade);
|
|
await tester.pump(_kScrollbarFadeDuration * 0.5);
|
|
|
|
// Scrollbar is fading out
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0),
|
|
color: const Color(0x4fbcbcbc),
|
|
),
|
|
);
|
|
|
|
// Hover over scrollbar with mouse to bring opacity back up
|
|
final TestGesture mouseGesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse);
|
|
await mouseGesture.addPointer();
|
|
addTearDown(mouseGesture.removePointer);
|
|
await mouseGesture.moveTo(const Offset(794.0, 5.0));
|
|
await tester.pumpAndSettle();
|
|
// Scrollbar should be visible
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 3.0, 800.0, 93.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Scrollbar thumb can be dragged', (WidgetTester tester) async {
|
|
final ScrollController scrollController = ScrollController();
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: PrimaryScrollController(
|
|
controller: scrollController,
|
|
child: RawScrollbar(
|
|
isAlwaysShown: true,
|
|
controller: scrollController,
|
|
child: const SingleChildScrollView(
|
|
child: SizedBox(width: 4000.0, height: 4000.0),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
// Drag the thumb down to scroll down.
|
|
const double scrollAmount = 10.0;
|
|
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
|
|
await tester.pumpAndSettle();
|
|
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
|
|
await tester.pumpAndSettle();
|
|
await dragScrollbarGesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// The view has scrolled more than it would have by a swipe gesture of the
|
|
// same distance.
|
|
expect(scrollController.offset, greaterThan(scrollAmount * 2));
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 10.0, 800.0, 100.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Scrollbar thumb cannot be dragged into overscroll if the physics do not allow', (WidgetTester tester) async {
|
|
final ScrollController scrollController = ScrollController();
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: PrimaryScrollController(
|
|
controller: scrollController,
|
|
child: RawScrollbar(
|
|
isAlwaysShown: true,
|
|
controller: scrollController,
|
|
child: const SingleChildScrollView(
|
|
child: SizedBox(width: 4000.0, height: 4000.0),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
// Try to drag the thumb into overscroll.
|
|
const double scrollAmount = -10.0;
|
|
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
|
|
await tester.pumpAndSettle();
|
|
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
|
|
await tester.pumpAndSettle();
|
|
|
|
// The physics should not have allowed us to enter overscroll.
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/66444
|
|
testWidgets("RawScrollbar doesn't show when scroll the inner scrollable widget", (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey();
|
|
final GlobalKey key2 = GlobalKey();
|
|
final GlobalKey outerKey = GlobalKey();
|
|
final GlobalKey innerKey = GlobalKey();
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: RawScrollbar(
|
|
key: key2,
|
|
thumbColor: const Color(0x11111111),
|
|
child: SingleChildScrollView(
|
|
key: outerKey,
|
|
child: SizedBox(
|
|
height: 1000.0,
|
|
width: double.infinity,
|
|
child: Column(
|
|
children: <Widget>[
|
|
RawScrollbar(
|
|
key: key1,
|
|
thumbColor: const Color(0x22222222),
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: double.infinity,
|
|
child: SingleChildScrollView(
|
|
key: innerKey,
|
|
child: const SizedBox(
|
|
key: Key('Inner scrollable'),
|
|
height: 1000.0,
|
|
width: double.infinity,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Drag the inner scrollable widget.
|
|
await tester.drag(find.byKey(innerKey), const Offset(0.0, -25.0));
|
|
await tester.pump();
|
|
// Scrollbar fully showing.
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
|
|
expect(
|
|
tester.renderObject(find.byKey(key2)),
|
|
paintsExactlyCountTimes(#drawRect, 2), // Each bar will call [drawRect] twice.
|
|
);
|
|
|
|
expect(
|
|
tester.renderObject(find.byKey(key1)),
|
|
paintsExactlyCountTimes(#drawRect, 2),
|
|
);
|
|
});
|
|
|
|
testWidgets('Scrollbar hit test area adjusts for PointerDeviceKind', (WidgetTester tester) async {
|
|
final ScrollController scrollController = ScrollController();
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: PrimaryScrollController(
|
|
controller: scrollController,
|
|
child: RawScrollbar(
|
|
isAlwaysShown: true,
|
|
controller: scrollController,
|
|
child: const SingleChildScrollView(
|
|
child: SizedBox(width: 4000.0, height: 4000.0),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
// Drag the scrollbar just outside of the painted thumb with touch input.
|
|
// The hit test area is padded to meet the minimum interactive size.
|
|
const double scrollAmount = 10.0;
|
|
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(790.0, 45.0));
|
|
await tester.pumpAndSettle();
|
|
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
|
|
await tester.pumpAndSettle();
|
|
|
|
// The scrollbar moved by scrollAmount, and the scrollOffset moved forward.
|
|
expect(scrollController.offset, greaterThan(0.0));
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 10.0, 800.0, 100.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
// Move back to reset.
|
|
await dragScrollbarGesture.moveBy(const Offset(0.0, -scrollAmount));
|
|
await tester.pumpAndSettle();
|
|
await dragScrollbarGesture.up();
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
|
|
// The same should not be possible with a mouse since it is more precise,
|
|
// the padding it not necessary.
|
|
final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse);
|
|
await gesture.addPointer();
|
|
addTearDown(gesture.removePointer);
|
|
await gesture.down(const Offset(790.0, 45.0));
|
|
await tester.pump();
|
|
await gesture.moveTo(const Offset(790.0, 55.0));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
// The scrollbar/scrollable should not have moved.
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
color: const Color(0x66BCBCBC),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('RawScrollbar.isAlwaysShown asserts that a ScrollPosition is attached', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: RawScrollbar(
|
|
isAlwaysShown: true,
|
|
controller: ScrollController(),
|
|
thumbColor: const Color(0x11111111),
|
|
child: const SingleChildScrollView(
|
|
child: SizedBox(
|
|
height: 1000.0,
|
|
width: 50.0,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final dynamic exception = tester.takeException();
|
|
expect(exception, isAssertionError);
|
|
expect(
|
|
(exception as AssertionError).message,
|
|
contains("The Scrollbar's ScrollController has no ScrollPosition attached."),
|
|
);
|
|
});
|
|
|
|
testWidgets('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/70105
|
|
final ScrollController scrollController = ScrollController();
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(),
|
|
child: PrimaryScrollController(
|
|
controller: scrollController,
|
|
child: RawScrollbar(
|
|
isAlwaysShown: true,
|
|
controller: scrollController,
|
|
child: const SingleChildScrollView(
|
|
child: SizedBox(width: 4000.0, height: 4000.0),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0),
|
|
color: const Color(0x00000000),
|
|
)
|
|
..line(
|
|
p1: const Offset(794.0, 0.0),
|
|
p2: const Offset(794.0, 600.0),
|
|
strokeWidth: 1.0,
|
|
color: const Color(0x00000000),
|
|
)
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
color: const Color(0x66bcbcbc),
|
|
),
|
|
);
|
|
|
|
// Drag the thumb down to scroll down.
|
|
const double scrollAmount = 10.0;
|
|
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0),
|
|
color: const Color(0x00000000),
|
|
)
|
|
..line(
|
|
p1: const Offset(794.0, 0.0),
|
|
p2: const Offset(794.0, 600.0),
|
|
strokeWidth: 1.0,
|
|
color: const Color(0x00000000),
|
|
)
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
// Drag color
|
|
color: const Color(0x66bcbcbc),
|
|
),
|
|
);
|
|
|
|
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
|
|
await tester.pumpAndSettle();
|
|
expect(scrollController.offset, greaterThan(10.0));
|
|
final double previousOffset = scrollController.offset;
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0),
|
|
color: const Color(0x00000000),
|
|
)
|
|
..line(
|
|
p1: const Offset(794.0, 0.0),
|
|
p2: const Offset(794.0, 600.0),
|
|
strokeWidth: 1.0,
|
|
color: const Color(0x00000000),
|
|
)
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 10.0, 800.0, 100.0),
|
|
color: const Color(0x66bcbcbc),
|
|
),
|
|
);
|
|
|
|
// Execute a pointer scroll while dragging (drag gesture has not come up yet)
|
|
final TestPointer pointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
pointer.hover(const Offset(798.0, 15.0));
|
|
await tester.sendEventToBinding(pointer.scroll(const Offset(0.0, 20.0)));
|
|
await tester.pumpAndSettle();
|
|
// Scrolling while holding the drag on the scrollbar and still hovered over
|
|
// the scrollbar should not have changed the scroll offset.
|
|
expect(pointer.location, const Offset(798.0, 15.0));
|
|
expect(scrollController.offset, previousOffset);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0),
|
|
color: const Color(0x00000000),
|
|
)
|
|
..line(
|
|
p1: const Offset(794.0, 0.0),
|
|
p2: const Offset(794.0, 600.0),
|
|
strokeWidth: 1.0,
|
|
color: const Color(0x00000000),
|
|
)
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 10.0, 800.0, 100.0),
|
|
color: const Color(0x66bcbcbc),
|
|
),
|
|
);
|
|
|
|
// Drag is still being held, move pointer to be hovering over another area
|
|
// of the scrollable (not over the scrollbar) and execute another pointer scroll
|
|
pointer.hover(tester.getCenter(find.byType(SingleChildScrollView)));
|
|
await tester.sendEventToBinding(pointer.scroll(const Offset(0.0, -70.0)));
|
|
await tester.pumpAndSettle();
|
|
// Scrolling while holding the drag on the scrollbar changed the offset
|
|
expect(pointer.location, const Offset(400.0, 300.0));
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0),
|
|
color: const Color(0x00000000),
|
|
)
|
|
..line(
|
|
p1: const Offset(794.0, 0.0),
|
|
p2: const Offset(794.0, 600.0),
|
|
strokeWidth: 1.0,
|
|
color: const Color(0x00000000),
|
|
)
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
color: const Color(0x66bcbcbc),
|
|
),
|
|
);
|
|
|
|
await dragScrollbarGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(RawScrollbar),
|
|
paints
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0),
|
|
color: const Color(0x00000000),
|
|
)
|
|
..line(
|
|
p1: const Offset(794.0, 0.0),
|
|
p2: const Offset(794.0, 600.0),
|
|
strokeWidth: 1.0,
|
|
color: const Color(0x00000000),
|
|
)
|
|
..rect(
|
|
rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 90.0),
|
|
color: const Color(0x66bcbcbc),
|
|
),
|
|
);
|
|
});
|
|
}
|