diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart index 0b1588663b..28c951dc9a 100644 --- a/packages/flutter/lib/src/widgets/scrollbar.dart +++ b/packages/flutter/lib/src/widgets/scrollbar.dart @@ -607,6 +607,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { return _paintScrollbar(canvas, size, thumbExtent, _lastAxisDirection!); } + bool get _lastMetricsAreScrollable => _lastMetrics!.minScrollExtent != _lastMetrics!.maxScrollExtent; + /// Same as hitTest, but includes some padding when the [PointerEvent] is /// caused by [PointerDeviceKind.touch] to make sure that the region /// isn't too small to be interacted with by the user. @@ -617,12 +619,16 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { /// based on proximity. When `forHover` is true, the larger hit test area will /// be used. bool hitTestInteractive(Offset position, PointerDeviceKind kind, { bool forHover = false }) { - if (_thumbRect == null) { + if (_trackRect == null) { // We have not computed the scrollbar position yet. return false; } - final Rect interactiveRect = _trackRect ?? _thumbRect!; + if (!_lastMetricsAreScrollable) { + return false; + } + + final Rect interactiveRect = _trackRect!; final Rect paddedRect = interactiveRect.expandToInclude( Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2), ); @@ -658,6 +664,10 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { return false; } + if (!_lastMetricsAreScrollable) { + return false; + } + switch (kind) { case PointerDeviceKind.touch: final Rect touchThumbRect = _thumbRect!.expandToInclude( @@ -682,6 +692,11 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { if (fadeoutOpacityAnimation.value == 0.0) { return false; } + + if (!_lastMetricsAreScrollable) { + return false; + } + return _thumbRect!.contains(position!); } @@ -1347,6 +1362,7 @@ class RawScrollbarState extends State with TickerProv void _updateScrollPosition(Offset updatedOffset) { assert(_currentController != null); assert(_dragScrollbarAxisOffset != null); + final ScrollPosition position = _currentController!.position; late double primaryDelta; switch (position.axisDirection) { diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart index c7f3433b42..c890049930 100644 --- a/packages/flutter/test/widgets/scrollbar_test.dart +++ b/packages/flutter/test/widgets/scrollbar_test.dart @@ -2038,6 +2038,43 @@ void main() { 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),