update the scrollbar (#82687)
diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart
index 7e327de..b23d687 100644
--- a/packages/flutter/lib/src/widgets/scrollbar.dart
+++ b/packages/flutter/lib/src/widgets/scrollbar.dart
@@ -334,16 +334,29 @@
Rect? _trackRect;
late double _thumbOffset;
- /// Update with new [ScrollMetrics]. The scrollbar will show and redraw itself
- /// based on these new metrics.
+ /// Update with new [ScrollMetrics]. If the metrics change, the scrollbar will
+ /// show and redraw itself based on these new metrics.
///
/// The scrollbar will remain on screen.
void update(
ScrollMetrics metrics,
AxisDirection axisDirection,
) {
+ if (_lastMetrics != null &&
+ _lastMetrics!.extentBefore == metrics.extentBefore &&
+ _lastMetrics!.extentInside == metrics.extentInside &&
+ _lastMetrics!.extentAfter == metrics.extentAfter &&
+ _lastAxisDirection == axisDirection)
+ return;
+
+ final ScrollMetrics? oldMetrics = _lastMetrics;
_lastMetrics = metrics;
_lastAxisDirection = axisDirection;
+
+ bool _needPaint(ScrollMetrics? metrics) => metrics != null && metrics.maxScrollExtent > metrics.minScrollExtent;
+ if (!_needPaint(oldMetrics) && !_needPaint(metrics))
+ return;
+
notifyListeners();
}
@@ -526,7 +539,8 @@
void paint(Canvas canvas, Size size) {
if (_lastAxisDirection == null
|| _lastMetrics == null
- || fadeoutOpacityAnimation.value == 0.0)
+ || fadeoutOpacityAnimation.value == 0.0
+ || _lastMetrics!.maxScrollExtent <= _lastMetrics!.minScrollExtent)
return;
// Skip painting if there's not enough space.
@@ -1519,18 +1533,37 @@
);
}
+ bool _handleScrollMetricsNotification(ScrollMetricsNotification notification) {
+ if (!widget.notificationPredicate(ScrollUpdateNotification(metrics: notification.metrics, context: notification.context)))
+ return false;
+ if (showScrollbar) {
+ if (_fadeoutAnimationController.status != AnimationStatus.forward
+ && _fadeoutAnimationController.status != AnimationStatus.completed)
+ _fadeoutAnimationController.forward();
+ }
+ scrollbarPainter.update(notification.metrics, notification.metrics.axisDirection);
+ return false;
+ }
+
bool _handleScrollNotification(ScrollNotification notification) {
if (!widget.notificationPredicate(notification))
return false;
final ScrollMetrics metrics = notification.metrics;
- if (metrics.maxScrollExtent <= metrics.minScrollExtent)
+ if (metrics.maxScrollExtent <= metrics.minScrollExtent) {
+ // Hide the bar when the Scrollable widget has no space to scroll.
+ if (_fadeoutAnimationController.status != AnimationStatus.dismissed
+ && _fadeoutAnimationController.status != AnimationStatus.reverse)
+ _fadeoutAnimationController.reverse();
+ scrollbarPainter.update(metrics, metrics.axisDirection);
return false;
+ }
if (notification is ScrollUpdateNotification ||
notification is OverscrollNotification) {
// Any movements always makes the scrollbar start showing up.
- if (_fadeoutAnimationController.status != AnimationStatus.forward)
+ if (_fadeoutAnimationController.status != AnimationStatus.forward
+ && _fadeoutAnimationController.status != AnimationStatus.completed)
_fadeoutAnimationController.forward();
_fadeoutTimer?.cancel();
@@ -1658,47 +1691,49 @@
super.dispose();
}
-
@override
Widget build(BuildContext context) {
updateScrollbarPainter();
- return NotificationListener<ScrollNotification>(
- onNotification: _handleScrollNotification,
- child: RepaintBoundary(
- child: RawGestureDetector(
- gestures: _gestures,
- child: MouseRegion(
- onExit: (PointerExitEvent event) {
- switch(event.kind) {
- case PointerDeviceKind.mouse:
- if (enableGestures)
- handleHoverExit(event);
- break;
- case PointerDeviceKind.stylus:
- case PointerDeviceKind.invertedStylus:
- case PointerDeviceKind.unknown:
- case PointerDeviceKind.touch:
- break;
- }
- },
- onHover: (PointerHoverEvent event) {
- switch(event.kind) {
- case PointerDeviceKind.mouse:
- if (enableGestures)
- handleHover(event);
- break;
- case PointerDeviceKind.stylus:
- case PointerDeviceKind.invertedStylus:
- case PointerDeviceKind.unknown:
- case PointerDeviceKind.touch:
- break;
- }
- },
- child: CustomPaint(
- key: _scrollbarPainterKey,
- foregroundPainter: scrollbarPainter,
- child: RepaintBoundary(child: widget.child),
+ return NotificationListener<ScrollMetricsNotification>(
+ onNotification: _handleScrollMetricsNotification,
+ child: NotificationListener<ScrollNotification>(
+ onNotification: _handleScrollNotification,
+ child: RepaintBoundary(
+ child: RawGestureDetector(
+ gestures: _gestures,
+ child: MouseRegion(
+ onExit: (PointerExitEvent event) {
+ switch(event.kind) {
+ case PointerDeviceKind.mouse:
+ if (enableGestures)
+ handleHoverExit(event);
+ break;
+ case PointerDeviceKind.stylus:
+ case PointerDeviceKind.invertedStylus:
+ case PointerDeviceKind.unknown:
+ case PointerDeviceKind.touch:
+ break;
+ }
+ },
+ onHover: (PointerHoverEvent event) {
+ switch(event.kind) {
+ case PointerDeviceKind.mouse:
+ if (enableGestures)
+ handleHover(event);
+ break;
+ case PointerDeviceKind.stylus:
+ case PointerDeviceKind.invertedStylus:
+ case PointerDeviceKind.unknown:
+ case PointerDeviceKind.touch:
+ break;
+ }
+ },
+ child: CustomPaint(
+ key: _scrollbarPainterKey,
+ foregroundPainter: scrollbarPainter,
+ child: RepaintBoundary(child: widget.child),
+ ),
),
),
),
diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart
index 73aa3c1..4008057 100644
--- a/packages/flutter/test/widgets/scrollbar_test.dart
+++ b/packages/flutter/test/widgets/scrollbar_test.dart
@@ -1565,4 +1565,76 @@
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 8.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.
+ });
}