Unverified Commit 7d74794a authored by xubaolin's avatar xubaolin Committed by GitHub

improve the scrollbar behavior when viewport size changed (#76102)

parent 82551118
...@@ -276,14 +276,21 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -276,14 +276,21 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
Rect? _trackRect; Rect? _trackRect;
late double _thumbOffset; late double _thumbOffset;
/// Update with new [ScrollMetrics]. The scrollbar will show and redraw itself /// Update with new [ScrollMetrics]. If the metrics change, the scrollbar will
/// based on these new metrics. /// show and redraw itself based on these new metrics.
/// ///
/// The scrollbar will remain on screen. /// The scrollbar will remain on screen.
void update( void update(
ScrollMetrics metrics, ScrollMetrics metrics,
AxisDirection axisDirection, AxisDirection axisDirection,
) { ) {
if (_lastMetrics != null &&
_lastMetrics!.extentBefore == metrics.extentBefore &&
_lastMetrics!.extentInside == metrics.extentInside &&
_lastMetrics!.extentAfter == metrics.extentAfter &&
_lastAxisDirection == axisDirection)
return;
_lastMetrics = metrics; _lastMetrics = metrics;
_lastAxisDirection = axisDirection; _lastAxisDirection = axisDirection;
notifyListeners(); notifyListeners();
...@@ -930,90 +937,90 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -930,90 +937,90 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_maybeTriggerScrollbar(); _maybeRequestEmptyScrollEvent();
} }
// Waits one frame and cause an empty scroll event (zero delta pixels). // Waits one frame and cause an empty scroll event (zero delta pixels).
// //
// This allows the thumb to show immediately when isAlwaysShown is true. // This allows the thumb to show immediately when isAlwaysShown is true.
// A scroll event is required in order to paint the thumb. // A scroll event is required in order to paint the thumb.
void _maybeTriggerScrollbar() { void _maybeRequestEmptyScrollEvent() {
if (!showScrollbar)
return;
WidgetsBinding.instance!.addPostFrameCallback((Duration duration) { WidgetsBinding.instance!.addPostFrameCallback((Duration duration) {
if (showScrollbar) { _fadeoutTimer?.cancel();
_fadeoutTimer?.cancel(); // Wait one frame and cause an empty scroll event. This allows the
// Wait one frame and cause an empty scroll event. This allows the // thumb to show immediately when isAlwaysShown is true. A scroll
// thumb to show immediately when isAlwaysShown is true. A scroll // event is required in order to paint the thumb.
// event is required in order to paint the thumb. final ScrollController? scrollController = widget.controller ?? PrimaryScrollController.of(context);
final ScrollController? scrollController = widget.controller ?? PrimaryScrollController.of(context); final bool tryPrimary = widget.controller == null;
final bool tryPrimary = widget.controller == null; final String controllerForError = tryPrimary
final String controllerForError = tryPrimary ? 'provided ScrollController'
? 'provided ScrollController' : 'PrimaryScrollController';
: 'PrimaryScrollController'; assert(
assert( scrollController != null,
scrollController != null, 'A ScrollController is required when Scrollbar.isAlwaysShown is true. '
'A ScrollController is required when Scrollbar.isAlwaysShown is true. ' '${tryPrimary ? 'The Scrollbar was not provided a ScrollController, '
'${tryPrimary ? 'The Scrollbar was not provided a ScrollController, ' 'and attempted to use the PrimaryScrollController, but none was found.' :''}',
'and attempted to use the PrimaryScrollController, but none was found.' :''}', );
); assert (() {
assert (() { if (!scrollController!.hasClients) {
if (!scrollController!.hasClients) { throw FlutterError.fromParts(<DiagnosticsNode>[
throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary(
ErrorSummary( 'The Scrollbar\'s ScrollController has no ScrollPosition attached.',
'The Scrollbar\'s ScrollController has no ScrollPosition attached.', ),
), ErrorDescription(
ErrorDescription( 'A Scrollbar cannot be painted without a ScrollPosition. ',
'A Scrollbar cannot be painted without a ScrollPosition. ', ),
), ErrorHint(
ErrorHint( 'The Scrollbar attempted to use the $controllerForError. This '
'The Scrollbar attempted to use the $controllerForError. This ' 'ScrollController should be associated with the ScrollView that '
'ScrollController should be associated with the ScrollView that ' 'the Scrollbar is being applied to. '
'the Scrollbar is being applied to. ' '${tryPrimary
'${tryPrimary ? 'A ScrollView with an Axis.vertical '
? 'A ScrollView with an Axis.vertical ' 'ScrollDirection will automatically use the '
'ScrollDirection will automatically use the ' 'PrimaryScrollController if the user has not provided a '
'PrimaryScrollController if the user has not provided a ' 'ScrollController, but a ScrollDirection of Axis.horizontal will '
'ScrollController, but a ScrollDirection of Axis.horizontal will ' 'not. To use the PrimaryScrollController explicitly, set ScrollView.primary '
'not. To use the PrimaryScrollController explicitly, set ScrollView.primary ' 'to true for the Scrollable widget.'
'to true for the Scrollable widget.' : 'When providing your own ScrollController, ensure both the '
: 'When providing your own ScrollController, ensure both the ' 'Scrollbar and the Scrollable widget use the same one.'
'Scrollbar and the Scrollable widget use the same one.' }',
}', ),
), ]);
]); }
} return true;
return true; }());
}()); assert (() {
assert (() { try {
try { scrollController!.position;
scrollController!.position; } catch (_) {
} catch (_) { throw FlutterError.fromParts(<DiagnosticsNode>[
throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary(
ErrorSummary( 'The $controllerForError is currently attached to more than one '
'The $controllerForError is currently attached to more than one ' 'ScrollPosition.',
'ScrollPosition.', ),
), ErrorDescription(
ErrorDescription( 'The Scrollbar requires a single ScrollPosition in order to be painted.',
'The Scrollbar requires a single ScrollPosition in order to be painted.', ),
), ErrorHint(
ErrorHint( 'When Scrollbar.isAlwaysShown is true, the associated Scrollable '
'When Scrollbar.isAlwaysShown is true, the associated Scrollable ' 'widgets must have unique ScrollControllers. '
'widgets must have unique ScrollControllers. ' '${tryPrimary
'${tryPrimary ? 'The PrimaryScrollController is used by default for '
? 'The PrimaryScrollController is used by default for ' 'ScrollViews with an Axis.vertical ScrollDirection, '
'ScrollViews with an Axis.vertical ScrollDirection, ' 'unless the ScrollView has been provided its own '
'unless the ScrollView has been provided its own ' 'ScrollController. More than one Scrollable may have tried '
'ScrollController. More than one Scrollable may have tried ' 'to use the PrimaryScrollController of the current context.'
'to use the PrimaryScrollController of the current context.' : 'The provided ScrollController must be unique to a '
: 'The provided ScrollController must be unique to a ' 'Scrollable widget.'
'Scrollable widget.' }',
}', ),
), ]);
]); }
} return true;
return true; }());
}()); scrollController!.position.didUpdateScrollPositionBy(0);
scrollController!.position.didUpdateScrollPositionBy(0);
}
}); });
} }
...@@ -1035,13 +1042,14 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1035,13 +1042,14 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
@override @override
void didUpdateWidget(T oldWidget) { void didUpdateWidget(T oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.isAlwaysShown != oldWidget.isAlwaysShown) { // If `isAlwaysShown` is true and does not change,
if (widget.isAlwaysShown == true) { // it may be necessary to trigger a scroll event to show or hide the bar when the
_maybeTriggerScrollbar(); // scrollable widget viewport size changed.
_fadeoutAnimationController.animateTo(1.0); if (widget.isAlwaysShown == true) {
} else { _maybeRequestEmptyScrollEvent();
_fadeoutAnimationController.reverse(); _fadeoutAnimationController.animateTo(1.0);
} } else if (widget.isAlwaysShown != oldWidget.isAlwaysShown) {
_fadeoutAnimationController.reverse();
} }
} }
...@@ -1203,13 +1211,19 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1203,13 +1211,19 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
return false; return false;
final ScrollMetrics metrics = notification.metrics; 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();
return false; return false;
}
if (notification is ScrollUpdateNotification || if (notification is ScrollUpdateNotification ||
notification is OverscrollNotification) { notification is OverscrollNotification) {
// Any movements always makes the scrollbar start showing up. // 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(); _fadeoutAnimationController.forward();
_fadeoutTimer?.cancel(); _fadeoutTimer?.cancel();
......
...@@ -1152,4 +1152,35 @@ void main() { ...@@ -1152,4 +1152,35 @@ void main() {
), ),
); );
}); });
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.
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment