Added AxisOrientation property to Scrollbar (#75497)
diff --git a/packages/flutter/lib/src/cupertino/scrollbar.dart b/packages/flutter/lib/src/cupertino/scrollbar.dart
index f01eb7f..f49551d 100644
--- a/packages/flutter/lib/src/cupertino/scrollbar.dart
+++ b/packages/flutter/lib/src/cupertino/scrollbar.dart
@@ -62,6 +62,7 @@
Radius radius = defaultRadius,
this.radiusWhileDragging = defaultRadiusWhileDragging,
ScrollNotificationPredicate? notificationPredicate,
+ ScrollbarOrientation? scrollbarOrientation,
}) : assert(thickness != null),
assert(thickness < double.infinity),
assert(thicknessWhileDragging != null),
@@ -79,6 +80,7 @@
timeToFade: _kScrollbarTimeToFade,
pressDuration: const Duration(milliseconds: 100),
notificationPredicate: notificationPredicate ?? defaultScrollNotificationPredicate,
+ scrollbarOrientation: scrollbarOrientation,
);
/// Default value for [thickness] if it's not specified in [CupertinoScrollbar].
@@ -148,7 +150,8 @@
..radius = _radius
..padding = MediaQuery.of(context).padding
..minLength = _kScrollbarMinLength
- ..minOverscrollLength = _kScrollbarMinOverscrollLength;
+ ..minOverscrollLength = _kScrollbarMinOverscrollLength
+ ..scrollbarOrientation = widget.scrollbarOrientation;
}
double _pressStartAxisPosition = 0.0;
diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart
index cd445c4..3f53f6b 100644
--- a/packages/flutter/lib/src/material/scrollbar.dart
+++ b/packages/flutter/lib/src/material/scrollbar.dart
@@ -117,6 +117,7 @@
this.radius,
this.notificationPredicate,
this.interactive,
+ this.scrollbarOrientation,
}) : super(key: key);
/// {@macro flutter.widgets.Scrollbar.child}
@@ -165,6 +166,9 @@
/// {@macro flutter.widgets.Scrollbar.notificationPredicate}
final ScrollNotificationPredicate? notificationPredicate;
+ /// {@macro flutter.widgets.Scrollbar.scrollbarOrientation}
+ final ScrollbarOrientation? scrollbarOrientation;
+
@override
State<Scrollbar> createState() => _ScrollbarState();
}
@@ -183,6 +187,7 @@
radiusWhileDragging: widget.radius ?? CupertinoScrollbar.defaultRadiusWhileDragging,
controller: widget.controller,
notificationPredicate: widget.notificationPredicate,
+ scrollbarOrientation: widget.scrollbarOrientation,
child: widget.child,
);
}
@@ -195,6 +200,7 @@
radius: widget.radius,
notificationPredicate: widget.notificationPredicate,
interactive: widget.interactive,
+ scrollbarOrientation: widget.scrollbarOrientation,
child: widget.child,
);
}
@@ -212,6 +218,7 @@
Radius? radius,
ScrollNotificationPredicate? notificationPredicate,
bool? interactive,
+ ScrollbarOrientation? scrollbarOrientation,
}) : super(
key: key,
child: child,
@@ -224,6 +231,7 @@
pressDuration: Duration.zero,
notificationPredicate: notificationPredicate ?? defaultScrollNotificationPredicate,
interactive: interactive,
+ scrollbarOrientation: scrollbarOrientation,
);
final bool? showTrackOnHover;
@@ -380,7 +388,8 @@
..crossAxisMargin = _scrollbarTheme.crossAxisMargin ?? (_useAndroidScrollbar ? 0.0 : _kScrollbarMargin)
..mainAxisMargin = _scrollbarTheme.mainAxisMargin ?? 0.0
..minLength = _scrollbarTheme.minThumbLength ?? _kScrollbarMinLength
- ..padding = MediaQuery.of(context).padding;
+ ..padding = MediaQuery.of(context).padding
+ ..scrollbarOrientation = widget.scrollbarOrientation;
}
@override
diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart
index 248a1dc..7c451cc 100644
--- a/packages/flutter/lib/src/widgets/scrollbar.dart
+++ b/packages/flutter/lib/src/widgets/scrollbar.dart
@@ -29,6 +29,21 @@
const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300);
const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600);
+/// An orientation along either the horizontal or vertical [Axis].
+enum ScrollbarOrientation {
+ /// Place towards the left of the screen.
+ left,
+
+ /// Place towards the right of the screen.
+ right,
+
+ /// Place on top of the screen.
+ top,
+
+ /// Place on the bottom of the screen.
+ bottom,
+}
+
/// Paints a scrollbar's track and thumb.
///
/// The size of the scrollbar along its scroll direction is typically
@@ -72,6 +87,7 @@
Radius? radius,
double minLength = _kMinThumbExtent,
double? minOverscrollLength,
+ ScrollbarOrientation? scrollbarOrientation,
}) : assert(color != null),
assert(thickness != null),
assert(fadeoutOpacityAnimation != null),
@@ -93,6 +109,7 @@
_minLength = minLength,
_trackColor = trackColor,
_trackBorderColor = trackBorderColor,
+ _scrollbarOrientation = scrollbarOrientation,
_minOverscrollLength = minOverscrollLength ?? minLength {
fadeoutOpacityAnimation.addListener(notifyListeners);
}
@@ -270,6 +287,46 @@
notifyListeners();
}
+ /// {@template flutter.widgets.Scrollbar.scrollbarOrientation}
+ /// Dictates the orientation of the scrollbar.
+ ///
+ /// [ScrollbarOrientation.top] places the scrollbar on top of the screen.
+ /// [ScrollbarOrientation.bottom] places the scrollbar on the bottom of the screen.
+ /// [ScrollbarOrientation.left] places the scrollbar on the left of the screen.
+ /// [ScrollbarOrientation.right] places the scrollbar on the right of the screen.
+ ///
+ /// [ScrollbarOrientation.top] and [ScrollbarOrientation.bottom] can only be
+ /// used with a vertical scroll.
+ /// [ScrollbarOrientation.left] and [ScrollbarOrientation.right] can only be
+ /// used with a horizontal scroll.
+ ///
+ /// For a vertical scroll the orientation defaults to
+ /// [ScrollbarOrientation.right] for [TextDirection.ltr] and
+ /// [ScrollbarOrientation.left] for [TextDirection.rtl].
+ /// For a horizontal scroll the orientation defaults to [ScrollbarOrientation.bottom].
+ /// {@endtemplate}
+ ScrollbarOrientation? get scrollbarOrientation => _scrollbarOrientation;
+ ScrollbarOrientation? _scrollbarOrientation;
+ set scrollbarOrientation(ScrollbarOrientation? value) {
+ if (scrollbarOrientation == value)
+ return;
+
+ _scrollbarOrientation = value;
+ notifyListeners();
+ }
+
+ void _debugAssertIsValidOrientation(ScrollbarOrientation orientation) {
+ assert(
+ (_isVertical && _isVerticalOrientation(orientation)) || (!_isVertical && !_isVerticalOrientation(orientation)),
+ 'The given ScrollbarOrientation: $orientation is incompatible with the current AxisDirection: $_lastAxisDirection.'
+ );
+ }
+
+ /// Check whether given scrollbar orientation is vertical
+ bool _isVerticalOrientation(ScrollbarOrientation orientation) =>
+ orientation == ScrollbarOrientation.left
+ || orientation == ScrollbarOrientation.right;
+
ScrollMetrics? _lastMetrics;
AxisDirection? _lastAxisDirection;
Rect? _thumbRect;
@@ -317,37 +374,48 @@
'A TextDirection must be provided before a Scrollbar can be painted.',
);
+ final ScrollbarOrientation resolvedOrientation;
+
+ if (scrollbarOrientation == null) {
+ if (_isVertical)
+ resolvedOrientation = textDirection == TextDirection.ltr
+ ? ScrollbarOrientation.right
+ : ScrollbarOrientation.left;
+ else
+ resolvedOrientation = ScrollbarOrientation.bottom;
+ }
+ else {
+ resolvedOrientation = scrollbarOrientation!;
+ }
+
final double x, y;
final Size thumbSize, trackSize;
final Offset trackOffset;
- switch (direction) {
- case AxisDirection.down:
+ _debugAssertIsValidOrientation(resolvedOrientation);
+ switch(resolvedOrientation) {
+ case ScrollbarOrientation.left:
thumbSize = Size(thickness, thumbExtent);
trackSize = Size(thickness + 2 * crossAxisMargin, _trackExtent);
- x = textDirection == TextDirection.rtl
- ? crossAxisMargin + padding.left
- : size.width - thickness - crossAxisMargin - padding.right;
+ x = crossAxisMargin + padding.left;
y = _thumbOffset;
trackOffset = Offset(x - crossAxisMargin, 0.0);
break;
- case AxisDirection.up:
+ case ScrollbarOrientation.right:
thumbSize = Size(thickness, thumbExtent);
trackSize = Size(thickness + 2 * crossAxisMargin, _trackExtent);
- x = textDirection == TextDirection.rtl
- ? crossAxisMargin + padding.left
- : size.width - thickness - crossAxisMargin - padding.right;
+ x = size.width - thickness - crossAxisMargin - padding.right;
y = _thumbOffset;
trackOffset = Offset(x - crossAxisMargin, 0.0);
break;
- case AxisDirection.left:
+ case ScrollbarOrientation.top:
thumbSize = Size(thumbExtent, thickness);
- x = _thumbOffset;
- y = size.height - thickness - crossAxisMargin - padding.bottom;
trackSize = Size(_trackExtent, thickness + 2 * crossAxisMargin);
+ x = _thumbOffset;
+ y = crossAxisMargin + padding.top;
trackOffset = Offset(0.0, y - crossAxisMargin);
break;
- case AxisDirection.right:
+ case ScrollbarOrientation.bottom:
thumbSize = Size(thumbExtent, thickness);
trackSize = Size(_trackExtent, thickness + 2 * crossAxisMargin);
x = _thumbOffset;
@@ -570,7 +638,8 @@
|| radius != old.radius
|| minLength != old.minLength
|| padding != old.padding
- || minOverscrollLength != old.minOverscrollLength;
+ || minOverscrollLength != old.minOverscrollLength
+ || scrollbarOrientation != old.scrollbarOrientation;
}
@override
@@ -655,6 +724,7 @@
this.pressDuration = Duration.zero,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.interactive,
+ this.scrollbarOrientation,
}) : assert(child != null),
assert(fadeDuration != null),
assert(timeToFade != null),
@@ -866,6 +936,9 @@
/// {@endtemplate}
final bool? interactive;
+ /// {@macro flutter.widgets.Scrollbar.scrollbarOrientation}
+ final ScrollbarOrientation? scrollbarOrientation;
+
@override
RawScrollbarState<RawScrollbar> createState() => RawScrollbarState<RawScrollbar>();
}
@@ -937,6 +1010,7 @@
color: widget.thumbColor ?? const Color(0x66BCBCBC),
thickness: widget.thickness ?? _kScrollbarThickness,
fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
+ scrollbarOrientation: widget.scrollbarOrientation,
);
}
@@ -1042,7 +1116,8 @@
..textDirection = Directionality.of(context)
..thickness = widget.thickness ?? _kScrollbarThickness
..radius = widget.radius
- ..padding = MediaQuery.of(context).padding;
+ ..padding = MediaQuery.of(context).padding
+ ..scrollbarOrientation = widget.scrollbarOrientation;
}
@override
diff --git a/packages/flutter/test/cupertino/scrollbar_test.dart b/packages/flutter/test/cupertino/scrollbar_test.dart
index 8977e2a..1240e48 100644
--- a/packages/flutter/test/cupertino/scrollbar_test.dart
+++ b/packages/flutter/test/cupertino/scrollbar_test.dart
@@ -889,4 +889,43 @@
),
);
});
+
+ testWidgets('CupertinoScrollbar scrollOrientation works correctly', (WidgetTester tester) async {
+ final ScrollController scrollController = ScrollController();
+
+ await tester.pumpWidget(
+ CupertinoApp(
+ home: PrimaryScrollController(
+ controller: scrollController,
+ child: CupertinoScrollbar(
+ isAlwaysShown: true,
+ controller: scrollController,
+ scrollbarOrientation: ScrollbarOrientation.left,
+ child: const SingleChildScrollView(
+ child: SizedBox(width: 4000.0, height: 4000.0),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ await tester.pumpAndSettle();
+
+ expect(
+ find.byType(CupertinoScrollbar),
+ paints
+ ..rect(
+ rect: const Rect.fromLTRB(0.0, 0.0, 9.0, 594.0),
+ )
+ ..line(
+ p1: const Offset(0.0, 0.0),
+ p2: const Offset(0.0, 594.0),
+ strokeWidth: 1.0,
+ )
+ ..rrect(
+ rrect: RRect.fromRectAndRadius(const Rect.fromLTRB(3.0, 3.0, 6.0, 92.1), const Radius.circular(1.5)),
+ color: _kScrollbarColor.color,
+ ),
+ );
+ });
}
diff --git a/packages/flutter/test/material/scrollbar_test.dart b/packages/flutter/test/material/scrollbar_test.dart
index f27d4f1..2a24bc7 100644
--- a/packages/flutter/test/material/scrollbar_test.dart
+++ b/packages/flutter/test/material/scrollbar_test.dart
@@ -1492,4 +1492,52 @@
);
}
});
+
+ testWidgets('Scrollbar scrollOrientation works correctly', (WidgetTester tester) async {
+ final ScrollController scrollController = ScrollController();
+
+ Widget _buildScrollWithOrientation(ScrollbarOrientation orientation) {
+ return _buildBoilerplate(
+ child: Theme(
+ data: ThemeData(
+ platform: TargetPlatform.android,
+ ),
+ child: PrimaryScrollController(
+ controller: scrollController,
+ child: Scrollbar(
+ interactive: true,
+ isAlwaysShown: true,
+ scrollbarOrientation: orientation,
+ controller: scrollController,
+ child: const SingleChildScrollView(
+ child: SizedBox(width: 4000.0, height: 4000.0)
+ ),
+ ),
+ ),
+ )
+ );
+ }
+
+ await tester.pumpWidget(_buildScrollWithOrientation(ScrollbarOrientation.left));
+ await tester.pumpAndSettle();
+
+ expect(
+ find.byType(Scrollbar),
+ paints
+ ..rect(
+ rect: const Rect.fromLTRB(0.0, 0.0, 4.0, 600.0),
+ color: Colors.transparent,
+ )
+ ..line(
+ p1: const Offset(0.0, 0.0),
+ p2: const Offset(0.0, 600.0),
+ strokeWidth: 1.0,
+ color: Colors.transparent,
+ )
+ ..rect(
+ rect: const Rect.fromLTRB(0.0, 0.0, 4.0, 90.0),
+ color: _kAndroidThumbIdleColor,
+ ),
+ );
+ });
}
diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart
index 64845a6..e1ba844 100644
--- a/packages/flutter/test/widgets/scrollbar_test.dart
+++ b/packages/flutter/test/widgets/scrollbar_test.dart
@@ -26,6 +26,7 @@
Radius? radius,
double minLength = _kMinThumbExtent,
double? minOverscrollLength,
+ ScrollbarOrientation? scrollbarOrientation,
required ScrollMetrics scrollMetrics,
}) {
return ScrollbarPainter(
@@ -39,6 +40,7 @@
minLength: minLength,
minOverscrollLength: minOverscrollLength ?? minLength,
fadeoutOpacityAnimation: kAlwaysCompleteAnimation,
+ scrollbarOrientation: scrollbarOrientation,
)..update(scrollMetrics, scrollMetrics.axisDirection);
}
@@ -248,6 +250,128 @@
},
);
+ 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);
+ const double margin = 0;
+
+ 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(
+ crossAxisMargin: margin,
+ 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);
+ const double margin = 0;
+ Rect rect;
+
+ // Vertical scroll with TextDirection.ltr
+ painter = _buildPainter(
+ crossAxisMargin: margin,
+ scrollMetrics: startingMetrics,
+ textDirection: TextDirection.ltr,
+ );
+ 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(
+ crossAxisMargin: margin,
+ 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(
+ crossAxisMargin: margin,
+ 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);
@@ -1214,4 +1338,22 @@
),
);
});
+
+ 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>()));
+ });
}