Unverified Commit bb278d1d authored by Mohammad Ghalayini's avatar Mohammad Ghalayini Committed by GitHub

[new feature] Add support for a RawScrollbar.shape (#85652)

parent f5dd3d9d
......@@ -85,10 +85,12 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
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 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
_textDirection = textDirection,
_thickness = thickness,
_radius = radius,
_shape = shape,
_padding = padding,
_mainAxisMargin = mainAxisMargin,
_crossAxisMargin = crossAxisMargin,
......@@ -217,6 +220,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
Radius? get radius => _radius;
Radius? _radius;
set radius(Radius? value) {
assert(shape == null || value == null);
if (radius == value)
return;
......@@ -224,6 +228,26 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
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 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
);
_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 @@ class RawScrollbar extends StatefulWidget {
required this.child,
this.controller,
this.isAlwaysShown,
this.shape,
this.radius,
this.thickness,
this.thumbColor,
......@@ -795,6 +830,7 @@ class RawScrollbar extends StatefulWidget {
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 @@ class RawScrollbar extends StatefulWidget {
/// {@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 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
scrollbarOrientation: widget.scrollbarOrientation,
mainAxisMargin: widget.mainAxisMargin,
shape: widget.shape,
crossAxisMargin: widget.crossAxisMargin
);
}
......@@ -1253,6 +1323,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
..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;
......
......@@ -1445,6 +1445,7 @@ void main() {
),
);
});
testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async {
final ScrollbarPainter painter = ScrollbarPainter(
color: _kScrollbarColor,
......@@ -1492,6 +1493,44 @@ void main() {
..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 @@ void main() {
..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 @@ void main() {
..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 @@ void main() {
..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) {
......
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