[new feature] Add support for a RawScrollbar.shape (#85652)
diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart
index 261bebd..0944c07 100644
--- a/packages/flutter/lib/src/widgets/scrollbar.dart
+++ b/packages/flutter/lib/src/widgets/scrollbar.dart
@@ -85,10 +85,12 @@
double mainAxisMargin = 0.0,
double crossAxisMargin = 0.0,
Radius? radius,
+ OutlinedBorder? shape,
double minLength = _kMinThumbExtent,
double? minOverscrollLength,
ScrollbarOrientation? scrollbarOrientation,
}) : assert(color != null),
+ assert(radius == null || shape == null),
assert(thickness != null),
assert(fadeoutOpacityAnimation != null),
assert(mainAxisMargin != null),
@@ -103,6 +105,7 @@
_textDirection = textDirection,
_thickness = thickness,
_radius = radius,
+ _shape = shape,
_padding = padding,
_mainAxisMargin = mainAxisMargin,
_crossAxisMargin = crossAxisMargin,
@@ -217,6 +220,7 @@
Radius? get radius => _radius;
Radius? _radius;
set radius(Radius? value) {
+ assert(shape == null || value == null);
if (radius == value)
return;
@@ -224,6 +228,26 @@
notifyListeners();
}
+ /// The [OutlinedBorder] of the scrollbar's thumb.
+ ///
+ /// Only one of [radius] and [shape] may be specified. For a rounded rectangle,
+ /// it's simplest to just specify [radius]. By default, the scrollbar thumb's
+ /// shape is a simple rectangle.
+ ///
+ /// If [shape] is specified, the thumb will take the shape of the passed
+ /// [OutlinedBorder] and fill itself with [color] (or grey if it
+ /// is unspecified).
+ ///
+ OutlinedBorder? get shape => _shape;
+ OutlinedBorder? _shape;
+ set shape(OutlinedBorder? value){
+ assert(radius == null || value == null);
+ if(shape == value)
+ return;
+
+ _shape = value;
+ notifyListeners();
+ }
/// The amount of space by which to inset the scrollbar's start and end, as
/// well as its side to the nearest edge, in logical pixels.
///
@@ -447,10 +471,20 @@
);
_thumbRect = Offset(x, y) & thumbSize;
- if (radius == null)
- canvas.drawRect(_thumbRect!, _paintThumb);
- else
+
+ if (radius != null) {
canvas.drawRRect(RRect.fromRectAndRadius(_thumbRect!, radius!), _paintThumb);
+ return;
+ }
+
+ if (shape == null) {
+ canvas.drawRect(_thumbRect!, _paintThumb);
+ return;
+ }
+
+ final Path outerPath = shape!.getOuterPath(_thumbRect!);
+ canvas.drawPath(outerPath, _paintThumb);
+ shape!.paint(canvas, _thumbRect!);
}
double _thumbExtent() {
@@ -776,6 +810,7 @@
required this.child,
this.controller,
this.isAlwaysShown,
+ this.shape,
this.radius,
this.thickness,
this.thumbColor,
@@ -795,6 +830,7 @@
assert(minOverscrollLength == null || minOverscrollLength <= minThumbLength),
assert(minOverscrollLength == null || minOverscrollLength >= 0),
assert(fadeDuration != null),
+ assert(radius == null || shape == null),
assert(timeToFade != null),
assert(pressDuration != null),
assert(mainAxisMargin != null),
@@ -944,6 +980,39 @@
/// {@endtemplate}
final bool? isAlwaysShown;
+ /// The [OutlinedBorder] of the scrollbar's thumb.
+ ///
+ /// Only one of [radius] and [shape] may be specified. For a rounded rectangle,
+ /// it's simplest to just specify [radius]. By default, the scrollbar thumb's
+ /// shape is a simple rectangle.
+ ///
+ /// If [shape] is specified, the thumb will take the shape of the passed
+ /// [OutlinedBorder] and fill itself with [thumbColor] (or grey if it
+ /// is unspecified).
+ ///
+ /// Here is an example of using a [StadiumBorder] for drawing the [shape] of the
+ /// thumb in a [RawScrollbar]:
+ ///
+ /// {@tool dartpad --template=stateless_widget_material}
+ /// ```dart
+ /// Widget build(BuildContext context) {
+ /// return Scaffold(
+ /// body: RawScrollbar(
+ /// child: ListView(
+ /// children: List<Text>.generate(100, (int index) => Text((index * index).toString())),
+ /// physics: const BouncingScrollPhysics(),
+ /// ),
+ /// shape: const StadiumBorder(side: BorderSide(color: Colors.brown, width: 3.0)),
+ /// thickness: 15.0,
+ /// thumbColor: Colors.blue,
+ /// isAlwaysShown: true,
+ /// ),
+ /// );
+ /// }
+ /// ```
+ /// {@end-tool}
+ final OutlinedBorder? shape;
+
/// The [Radius] of the scrollbar thumb's rounded rectangle corners.
///
/// Scrollbar will be rectangular if [radius] is null, which is the default
@@ -1124,6 +1193,7 @@
fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
scrollbarOrientation: widget.scrollbarOrientation,
mainAxisMargin: widget.mainAxisMargin,
+ shape: widget.shape,
crossAxisMargin: widget.crossAxisMargin
);
}
@@ -1253,6 +1323,7 @@
..padding = MediaQuery.of(context).padding
..scrollbarOrientation = widget.scrollbarOrientation
..mainAxisMargin = widget.mainAxisMargin
+ ..shape = widget.shape
..crossAxisMargin = widget.crossAxisMargin
..minLength = widget.minThumbLength
..minOverscrollLength = widget.minOverscrollLength ?? widget.minThumbLength;
diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart
index e9f40b8..31d2024 100644
--- a/packages/flutter/test/widgets/scrollbar_test.dart
+++ b/packages/flutter/test/widgets/scrollbar_test.dart
@@ -1445,6 +1445,7 @@
),
);
});
+
testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async {
final ScrollbarPainter painter = ScrollbarPainter(
color: _kScrollbarColor,
@@ -1492,6 +1493,44 @@
..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(
@@ -1518,6 +1557,42 @@
..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(
@@ -1543,6 +1618,40 @@
..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(
@@ -1575,6 +1684,32 @@
..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) {