| // 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/foundation.dart'; |
| 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, |
| ScrollbarOrientation? scrollbarOrientation, |
| 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, |
| scrollbarOrientation: scrollbarOrientation, |
| )..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; |
| } |
| } |
| } |
| }, |
| ); |
| |
| test('scrollbarOrientation 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); |
| |
| for (final ScrollbarOrientation scrollbarOrientation in ScrollbarOrientation.values) { |
| final AxisDirection axisDirection; |
| if (scrollbarOrientation == ScrollbarOrientation.left || scrollbarOrientation == ScrollbarOrientation.right) |
| axisDirection = AxisDirection.down; |
| else |
| axisDirection = AxisDirection.right; |
| |
| painter = _buildPainter( |
| scrollMetrics: startingMetrics, |
| scrollbarOrientation: scrollbarOrientation, |
| ); |
| |
| painter.update( |
| startingMetrics.copyWith(axisDirection: axisDirection), |
| axisDirection |
| ); |
| |
| painter.paint(testCanvas, size); |
| final Rect rect = captureRect(); |
| |
| switch (scrollbarOrientation) { |
| case ScrollbarOrientation.left: |
| expect(rect.left, 0); |
| expect(rect.top, 0); |
| expect(rect.right, _kThickness); |
| expect(rect.bottom, _kMinThumbExtent); |
| break; |
| case ScrollbarOrientation.right: |
| expect(rect.left, 600 - _kThickness); |
| expect(rect.top, 0); |
| expect(rect.right, 600); |
| expect(rect.bottom, _kMinThumbExtent); |
| break; |
| case ScrollbarOrientation.top: |
| expect(rect.left, 0); |
| expect(rect.top, 0); |
| expect(rect.right, _kMinThumbExtent); |
| expect(rect.bottom, _kThickness); |
| break; |
| case ScrollbarOrientation.bottom: |
| expect(rect.left, 0); |
| expect(rect.top, 23 - _kThickness); |
| expect(rect.right, _kMinThumbExtent); |
| expect(rect.bottom, 23); |
| break; |
| } |
| } |
| }); |
| |
| test('scrollbarOrientation default values are correct', () { |
| const double viewportDimension = 23; |
| const double maxExtent = 100; |
| final ScrollMetrics startingMetrics = defaultMetrics.copyWith( |
| maxScrollExtent: maxExtent, |
| viewportDimension: viewportDimension, |
| ); |
| const Size size = Size(600, viewportDimension); |
| Rect rect; |
| |
| // Vertical scroll with TextDirection.ltr |
| painter = _buildPainter( |
| scrollMetrics: startingMetrics, |
| ); |
| painter.update( |
| startingMetrics.copyWith(axisDirection: AxisDirection.down), |
| AxisDirection.down |
| ); |
| painter.paint(testCanvas, size); |
| rect = captureRect(); |
| expect(rect.left, 600 - _kThickness); |
| expect(rect.top, 0); |
| expect(rect.right, 600); |
| expect(rect.bottom, _kMinThumbExtent); |
| |
| // Vertical scroll with TextDirection.rtl |
| painter = _buildPainter( |
| scrollMetrics: startingMetrics, |
| textDirection: TextDirection.rtl, |
| ); |
| painter.update( |
| startingMetrics.copyWith(axisDirection: AxisDirection.down), |
| AxisDirection.down |
| ); |
| painter.paint(testCanvas, size); |
| rect = captureRect(); |
| expect(rect.left, 0); |
| expect(rect.top, 0); |
| expect(rect.right, _kThickness); |
| expect(rect.bottom, _kMinThumbExtent); |
| |
| // Horizontal scroll |
| painter = _buildPainter( |
| scrollMetrics: startingMetrics, |
| ); |
| painter.update( |
| startingMetrics.copyWith(axisDirection: AxisDirection.right), |
| AxisDirection.right, |
| ); |
| painter.paint(testCanvas, size); |
| rect = captureRect(); |
| expect(rect.left, 0); |
| expect(rect.top, 23 - _kThickness); |
| expect(rect.right, _kMinThumbExtent); |
| expect(rect.bottom, 23); |
| }); |
| |
| 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 painter = _buildPainter( |
| padding: padding, |
| scrollMetrics: metrics, |
| ); |
| |
| testWidgets('down', (WidgetTester tester) async { |
| painter.update( |
| metrics.copyWith( |
| viewportDimension: size.height, |
| pixels: double.negativeInfinity, |
| ), |
| AxisDirection.down, |
| ); |
| |
| // Top overscroll. |
| painter.paint(testCanvas, size); |
| final Rect rect0 = captureRect(); |
| expect(rect0.top, padding.top); |
| expect(size.width - rect0.right, padding.right); |
| |
| // Bottom overscroll. |
| painter.update( |
| metrics.copyWith( |
| viewportDimension: size.height, |
| pixels: double.infinity, |
| ), |
| AxisDirection.down, |
| ); |
| |
| painter.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 { |
| painter.update( |
| metrics.copyWith( |
| viewportDimension: size.height, |
| pixels: double.infinity, |
| axisDirection: AxisDirection.up, |
| ), |
| AxisDirection.up, |
| ); |
| |
| // Top overscroll. |
| painter.paint(testCanvas, size); |
| final Rect rect0 = captureRect(); |
| expect(rect0.top, padding.top); |
| expect(size.width - rect0.right, padding.right); |
| |
| // Bottom overscroll. |
| painter.update( |
| metrics.copyWith( |
| viewportDimension: size.height, |
| pixels: double.negativeInfinity, |
| axisDirection: AxisDirection.up, |
| ), |
| AxisDirection.up, |
| ); |
| |
| painter.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 { |
| painter.update( |
| metrics.copyWith( |
| viewportDimension: size.width, |
| pixels: double.negativeInfinity, |
| axisDirection: AxisDirection.left, |
| ), |
| AxisDirection.left, |
| ); |
| |
| // Right overscroll. |
| painter.paint(testCanvas, size); |
| final Rect rect0 = captureRect(); |
| expect(size.height - rect0.bottom, padding.bottom); |
| expect(size.width - rect0.right, padding.right); |
| |
| // Left overscroll. |
| painter.update( |
| metrics.copyWith( |
| viewportDimension: size.width, |
| pixels: double.infinity, |
| axisDirection: AxisDirection.left, |
| ), |
| AxisDirection.left, |
| ); |
| |
| painter.paint(testCanvas, size); |
| final Rect rect1 = captureRect(); |
| expect(size.height - rect1.bottom, padding.bottom); |
| expect(rect1.left, padding.left); |
| }); |
| |
| testWidgets('right', (WidgetTester tester) async { |
| painter.update( |
| metrics.copyWith( |
| viewportDimension: size.width, |
| pixels: double.infinity, |
| axisDirection: AxisDirection.right, |
| ), |
| AxisDirection.right, |
| ); |
| |
| // Right overscroll. |
| painter.paint(testCanvas, size); |
| final Rect rect0 = captureRect(); |
| expect(size.height - rect0.bottom, padding.bottom); |
| expect(size.width - rect0.right, padding.right); |
| |
| // Left overscroll. |
| painter.update( |
| metrics.copyWith( |
| viewportDimension: size.width, |
| pixels: double.negativeInfinity, |
| axisDirection: AxisDirection.right, |
| ), |
| AxisDirection.right, |
| ); |
| |
| painter.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 painter = _buildPainter( |
| padding: padding, |
| scrollMetrics: metrics, |
| minLength: 36.0, |
| minOverscrollLength: 8.0, |
| ); |
| |
| // No overscroll gives a full sized thumb. |
| painter.update( |
| metrics.copyWith( |
| pixels: 0.0, |
| ), |
| AxisDirection.down, |
| ); |
| painter.paint(testCanvas, size); |
| final double fullThumbExtent = captureRect().height; |
| expect(fullThumbExtent, greaterThan(_kMinThumbExtent)); |
| |
| // Scrolling to the middle also gives a full sized thumb. |
| painter.update( |
| metrics.copyWith( |
| pixels: scrollExtent / 2, |
| ), |
| AxisDirection.down, |
| ); |
| painter.paint(testCanvas, size); |
| expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 1e-6)); |
| |
| // Scrolling just to the very end also gives a full sized thumb. |
| painter.update( |
| metrics.copyWith( |
| pixels: scrollExtent, |
| ), |
| AxisDirection.down, |
| ); |
| painter.paint(testCanvas, size); |
| expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 1e-6)); |
| |
| // Scrolling just past the end shrinks the thumb slightly. |
| painter.update( |
| metrics.copyWith( |
| pixels: scrollExtent * 1.001, |
| ), |
| AxisDirection.down, |
| ); |
| painter.paint(testCanvas, size); |
| expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 2.0)); |
| |
| // Scrolling way past the end shrinks the thumb to minimum. |
| painter.update( |
| metrics.copyWith( |
| pixels: double.infinity, |
| ), |
| AxisDirection.down, |
| ); |
| painter.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 will show on hover without needing to scroll first for metrics', (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), |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pump(); |
| |
| // Hover over scrollbar with mouse. Even though we have not scrolled, the |
| // ScrollMetricsNotification will have informed the Scrollbar's hit testing. |
| 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 have appeared in response to hover event. |
| 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('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), |
| ), |
| ); |
| }); |
| |
| testWidgets('Scrollbar thumb cannot be dragged into overscroll if the platform does not allow it', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: ScrollConfiguration( |
| // Don't apply a scrollbar automatically for this test. |
| behavior: const ScrollBehavior().copyWith( |
| scrollbars: false, |
| physics: const AlwaysScrollableScrollPhysics(), |
| ), |
| 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 platform drag handling 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), |
| ), |
| ); |
| |
| await dragScrollbarGesture.up(); |
| await tester.pumpAndSettle(); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.macOS, |
| TargetPlatform.linux, |
| TargetPlatform.windows, |
| TargetPlatform.fuchsia, |
| })); |
| |
| testWidgets('Scrollbar thumb can be dragged into overscroll if the platform allows it', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: ScrollConfiguration( |
| // Don't apply a scrollbar automatically for this test. |
| behavior: const ScrollBehavior().copyWith( |
| scrollbars: false, |
| physics: const AlwaysScrollableScrollPhysics(), |
| ), |
| 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 platform drag handling should have allowed us to enter overscroll. |
| expect(scrollController.offset, lessThan(-66.0)); |
| expect( |
| find.byType(RawScrollbar), |
| paints |
| ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0)) |
| ..rect( |
| // The size of the scrollbar thumb shrinks when overscrolling |
| rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 80.0), |
| color: const Color(0x66BCBCBC), |
| ), |
| ); |
| |
| await dragScrollbarGesture.up(); |
| await tester.pumpAndSettle(); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.android, |
| TargetPlatform.iOS, |
| })); |
| |
| // 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 { |
| final FlutterExceptionHandler? handler = FlutterError.onError; |
| FlutterErrorDetails? error; |
| FlutterError.onError = (FlutterErrorDetails details) { |
| error = details; |
| }; |
| |
| 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(); |
| |
| expect(error, isNotNull); |
| final AssertionError exception = error!.exception as AssertionError; |
| expect( |
| exception.message, |
| contains("The Scrollbar's ScrollController has no ScrollPosition attached."), |
| ); |
| |
| FlutterError.onError = handler; |
| }); |
| |
| testWidgets('Interactive scrollbars should have a valid scroll controller', (WidgetTester tester) async { |
| final ScrollController primaryScrollController = ScrollController(); |
| final ScrollController scrollController = ScrollController(); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: PrimaryScrollController( |
| controller: primaryScrollController, |
| child: RawScrollbar( |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: const SizedBox( |
| height: 1000.0, |
| width: 1000.0, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pumpAndSettle(); |
| |
| AssertionError? exception = tester.takeException() as AssertionError?; |
| // The scrollbar is not visible and cannot be interacted with, so no assertion. |
| expect(exception, isNull); |
| // Scroll to trigger the scrollbar to come into view. |
| final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView))); |
| await gesture.moveBy(const Offset(0.0, -20.0)); |
| exception = tester.takeException() as AssertionError; |
| expect(exception, isAssertionError); |
| expect( |
| exception.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), |
| ), |
| ); |
| }); |
| |
| testWidgets('Scrollbar thumb can be dragged in reverse', (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( |
| reverse: true, |
| 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, 510.0, 800.0, 600.0), |
| color: const Color(0x66BCBCBC), |
| ), |
| ); |
| |
| // Drag the thumb up to scroll up. |
| const double scrollAmount = 10.0; |
| final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 550.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, 500.0, 800.0, 590.0), |
| color: const Color(0x66BCBCBC), |
| ), |
| ); |
| }); |
| |
| testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async { |
| final ScrollbarPainter painter = ScrollbarPainter( |
| color: _kScrollbarColor, |
| fadeoutOpacityAnimation: kAlwaysCompleteAnimation, |
| textDirection: TextDirection.ltr, |
| scrollbarOrientation: ScrollbarOrientation.left, |
| ); |
| const Size size = Size(60, 80); |
| final ScrollMetrics scrollMetrics = defaultMetrics.copyWith( |
| maxScrollExtent: 100, |
| viewportDimension: size.height, |
| axisDirection: AxisDirection.right, |
| ); |
| painter.update(scrollMetrics, scrollMetrics.axisDirection); |
| |
| expect(() => painter.paint(testCanvas, size), throwsA(isA<AssertionError>())); |
| }); |
| |
| testWidgets('RawScrollbar mainAxisMargin property works properly', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| mainAxisMargin: 10, |
| 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, 10.0, 800.0, 590.0)) |
| ..rect(rect: const Rect.fromLTRB(794.0, 10.0, 800.0, 358.0)) |
| ); |
| }); |
| |
| testWidgets('shape property of RawScrollbar can draw a BeveledRectangleBorder', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| shape: const BeveledRectangleBorder( |
| borderRadius: BorderRadius.all(Radius.circular(8.0)) |
| ), |
| controller: scrollController, |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: const SizedBox(height: 1000.0), |
| ), |
| ), |
| ))); |
| await tester.pumpAndSettle(); |
| expect( |
| find.byType(RawScrollbar), |
| paints |
| ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0)) |
| ..path( |
| includes: const <Offset>[ |
| Offset(797.0, 0.0), |
| Offset(797.0, 18.0), |
| ], |
| excludes: const <Offset>[ |
| Offset(796.0, 0.0), |
| Offset(798.0, 0.0), |
| ], |
| ), |
| ); |
| }); |
| |
| testWidgets('minThumbLength property of RawScrollbar is respected', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| controller: scrollController, |
| minThumbLength: 21, |
| minOverscrollLength: 8, |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: const SizedBox(width: 1000.0, height: 50000.0), |
| ), |
| ), |
| ))); |
| await tester.pumpAndSettle(); |
| expect( |
| find.byType(RawScrollbar), |
| paints |
| ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0)) // track |
| ..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 21.0))); // thumb |
| }); |
| |
| testWidgets('shape property of RawScrollbar can draw a CircleBorder', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| shape: const CircleBorder(side: BorderSide(width: 2.0)), |
| thickness: 36.0, |
| controller: scrollController, |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: const SizedBox(height: 1000.0, width: 1000), |
| ), |
| ), |
| ))); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| find.byType(RawScrollbar), |
| paints |
| ..path( |
| includes: const <Offset>[ |
| Offset(782.0, 180.0), |
| Offset(782.0, 180.0 - 18.0), |
| Offset(782.0 + 18.0, 180), |
| Offset(782.0, 180.0 + 18.0), |
| Offset(782.0 - 18.0, 180), |
| ], |
| ) |
| ..circle(x: 782.0, y: 180.0, radius: 17.0, strokeWidth: 2.0) |
| ); |
| }); |
| |
| testWidgets('crossAxisMargin property of RawScrollbar is respected', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| controller: scrollController, |
| crossAxisMargin: 30, |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: const SizedBox(width: 1000.0, height: 1000.0), |
| ), |
| ), |
| ))); |
| await tester.pumpAndSettle(); |
| expect( |
| find.byType(RawScrollbar), |
| paints |
| ..rect(rect: const Rect.fromLTRB(734.0, 0.0, 800.0, 600.0)) |
| ..rect(rect: const Rect.fromLTRB(764.0, 0.0, 770.0, 360.0))); |
| }); |
| |
| testWidgets('shape property of RawScrollbar can draw a RoundedRectangleBorder', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| thickness: 20, |
| shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(8))), |
| controller: scrollController, |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: const SizedBox(height: 1000.0, width: 1000.0), |
| ), |
| ), |
| ))); |
| await tester.pumpAndSettle(); |
| expect( |
| find.byType(RawScrollbar), |
| paints |
| ..rect(rect: const Rect.fromLTRB(780.0, 0.0, 800.0, 600.0)) |
| ..path( |
| includes: const <Offset>[ |
| Offset(800.0, 0.0), |
| ], |
| excludes: const <Offset>[ |
| Offset(780.0, 0.0), |
| ], |
| ), |
| ); |
| }); |
| |
| testWidgets('minOverscrollLength property of RawScrollbar is respected', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| controller: scrollController, |
| isAlwaysShown: true, |
| minOverscrollLength: 8.0, |
| minThumbLength: 36.0, |
| child: SingleChildScrollView( |
| physics: const BouncingScrollPhysics(), |
| controller: scrollController, |
| child: const SizedBox(height: 10000), |
| ) |
| ), |
| ) |
| ) |
| ); |
| await tester.pumpAndSettle(); |
| final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(RawScrollbar))); |
| await gesture.moveBy(const Offset(0, 1000)); |
| await tester.pump(); |
| 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, 8.0))); |
| }); |
| |
| testWidgets('not passing any shape or radius to RawScrollbar will draw the usual rectangular thumb', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| controller: scrollController, |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: const SizedBox(height: 1000.0), |
| ), |
| ), |
| ))); |
| await tester.pumpAndSettle(); |
| |
| 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)) |
| ); |
| }); |
| |
| testWidgets('The bar can show or hide when the viewport size change', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| Widget buildFrame(double height) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| controller: scrollController, |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: SizedBox(width: double.infinity, height: height) |
| ), |
| ), |
| ), |
| ); |
| } |
| await tester.pumpWidget(buildFrame(600.0)); |
| await tester.pumpAndSettle(); |
| expect(find.byType(RawScrollbar), isNot(paints..rect())); // Not shown. |
| |
| await tester.pumpWidget(buildFrame(600.1)); |
| await tester.pumpAndSettle(); |
| expect(find.byType(RawScrollbar), paints..rect()..rect()); // Show the bar. |
| |
| await tester.pumpWidget(buildFrame(600.0)); |
| await tester.pumpAndSettle(); |
| expect(find.byType(RawScrollbar), isNot(paints..rect())); // Hide the bar. |
| }); |
| |
| testWidgets('The bar can show or hide when the window size change', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| Widget buildFrame() { |
| return 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: double.infinity, |
| height: 600.0, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| tester.binding.window.physicalSizeTestValue = const Size(800.0, 600.0); |
| tester.binding.window.devicePixelRatioTestValue = 1; |
| addTearDown(tester.binding.window.clearPhysicalSizeTestValue); |
| addTearDown(tester.binding.window.clearDevicePixelRatioTestValue); |
| |
| await tester.pumpWidget(buildFrame()); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| expect(find.byType(RawScrollbar), isNot(paints..rect())); // Not shown. |
| |
| tester.binding.window.physicalSizeTestValue = const Size(800.0, 599.0); |
| await tester.pumpAndSettle(); |
| expect(find.byType(RawScrollbar), paints..rect()..rect()); // Show the bar. |
| |
| tester.binding.window.physicalSizeTestValue = const Size(800.0, 600.0); |
| await tester.pumpAndSettle(); |
| expect(find.byType(RawScrollbar), isNot(paints..rect())); // Not shown. |
| }); |
| |
| testWidgets('Scrollbar will not flip axes based on notification is there is a scroll controller', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/87697 |
| final ScrollController verticalScrollController = ScrollController(); |
| final ScrollController horizontalScrollController = ScrollController(); |
| Widget buildFrame() { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| isAlwaysShown: true, |
| controller: verticalScrollController, |
| // This scrollbar will receive scroll notifications from both nested |
| // scroll views of opposite axes, but should stay on the vertical |
| // axis that its scroll controller is associated with. |
| notificationPredicate: (ScrollNotification notification) => notification.depth <= 1, |
| child: SingleChildScrollView( |
| controller: verticalScrollController, |
| child: SingleChildScrollView( |
| scrollDirection: Axis.horizontal, |
| controller: horizontalScrollController, |
| child: const SizedBox( |
| width: 1000.0, |
| height: 1000.0, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame()); |
| await tester.pumpAndSettle(); |
| expect(verticalScrollController.offset, 0.0); |
| expect(horizontalScrollController.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), |
| ), |
| ); |
| |
| // Move the horizontal scroll view. The vertical scrollbar should not flip. |
| horizontalScrollController.jumpTo(10.0); |
| expect(verticalScrollController.offset, 0.0); |
| expect(horizontalScrollController.offset, 10.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('notificationPredicate depth test.', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| final List<int> depths = <int>[]; |
| Widget buildFrame() { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| notificationPredicate: (ScrollNotification notification) { |
| depths.add(notification.depth); |
| return notification.depth == 0; |
| }, |
| controller: scrollController, |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: const SingleChildScrollView(), |
| ), |
| ), |
| ), |
| ); |
| } |
| await tester.pumpWidget(buildFrame()); |
| await tester.pumpAndSettle(); |
| |
| // `notificationPredicate` should be called twice with different `depth` |
| // because there are two scrollable widgets. |
| expect(depths.length, 2); |
| expect(depths[0], 1); |
| expect(depths[1], 0); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/92262 |
| testWidgets('Do not crash when resize from scrollable to non-scrollable.', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| Widget buildFrame(double height) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: RawScrollbar( |
| controller: scrollController, |
| interactive: true, |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| controller: scrollController, |
| child: Container( |
| width: 100.0, |
| height: height, |
| color: const Color(0xFF000000), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| await tester.pumpWidget(buildFrame(700.0)); |
| await tester.pumpAndSettle(); |
| |
| await tester.pumpWidget(buildFrame(600.0)); |
| await tester.pumpAndSettle(); |
| |
| // Try to drag the thumb. |
| final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(798.0, 5.0)); |
| await tester.pumpAndSettle(); |
| await dragScrollbarGesture.moveBy(const Offset(0.0, 5.0)); |
| await tester.pumpAndSettle(); |
| }); |
| |
| test('ScrollbarPainter.shouldRepaint returns true when any of the properties changes', () { |
| ScrollbarPainter createPainter({ |
| Color color = const Color(0xFF000000), |
| Animation<double> fadeoutOpacityAnimation = kAlwaysCompleteAnimation, |
| Color trackColor = const Color(0x00000000), |
| Color trackBorderColor = const Color(0x00000000), |
| TextDirection textDirection = TextDirection.ltr, |
| double thickness = _kThickness, |
| EdgeInsets padding = EdgeInsets.zero, |
| double mainAxisMargin = 0.0, |
| double crossAxisMargin = 0.0, |
| Radius? radius, |
| OutlinedBorder? shape, |
| double minLength = _kMinThumbExtent, |
| double? minOverscrollLength, |
| ScrollbarOrientation scrollbarOrientation = ScrollbarOrientation.top, |
| }) { |
| return ScrollbarPainter( |
| color: color, |
| fadeoutOpacityAnimation: fadeoutOpacityAnimation, |
| trackColor: trackColor, |
| trackBorderColor: trackBorderColor, |
| textDirection: textDirection, |
| thickness: thickness, |
| padding: padding, |
| mainAxisMargin: mainAxisMargin, |
| crossAxisMargin: crossAxisMargin, |
| radius: radius, |
| shape: shape, |
| minLength: minLength, |
| minOverscrollLength: minOverscrollLength, |
| scrollbarOrientation: scrollbarOrientation, |
| ); |
| } |
| final ScrollbarPainter painter = createPainter(); |
| expect(painter.shouldRepaint(createPainter()), false); |
| expect(painter.shouldRepaint(createPainter(color: const Color(0xFFFFFFFF))), true); |
| expect(painter.shouldRepaint(createPainter(fadeoutOpacityAnimation: kAlwaysDismissedAnimation)), true); |
| expect(painter.shouldRepaint(createPainter(trackColor: const Color(0xFFFFFFFF))), true); |
| expect(painter.shouldRepaint(createPainter(trackBorderColor: const Color(0xFFFFFFFF))), true); |
| expect(painter.shouldRepaint(createPainter(textDirection: TextDirection.rtl)), true); |
| expect(painter.shouldRepaint(createPainter(thickness: _kThickness + 1.0)), true); |
| expect(painter.shouldRepaint(createPainter(padding: const EdgeInsets.all(1.0))), true); |
| expect(painter.shouldRepaint(createPainter(mainAxisMargin: 1.0)), true); |
| expect(painter.shouldRepaint(createPainter(crossAxisMargin: 1.0)), true); |
| expect(painter.shouldRepaint(createPainter(radius: const Radius.circular(1.0))), true); |
| expect(painter.shouldRepaint(createPainter(shape: const CircleBorder(side: BorderSide(width: 2.0)))), true); |
| expect(painter.shouldRepaint(createPainter(minLength: _kMinThumbExtent + 1.0)), true); |
| expect(painter.shouldRepaint(createPainter(minOverscrollLength: 1.0)), true); |
| expect(painter.shouldRepaint(createPainter(scrollbarOrientation: ScrollbarOrientation.bottom)), true); |
| }); |
| } |