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 { ...@@ -85,10 +85,12 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
double mainAxisMargin = 0.0, double mainAxisMargin = 0.0,
double crossAxisMargin = 0.0, double crossAxisMargin = 0.0,
Radius? radius, Radius? radius,
OutlinedBorder? shape,
double minLength = _kMinThumbExtent, double minLength = _kMinThumbExtent,
double? minOverscrollLength, double? minOverscrollLength,
ScrollbarOrientation? scrollbarOrientation, ScrollbarOrientation? scrollbarOrientation,
}) : assert(color != null), }) : assert(color != null),
assert(radius == null || shape == null),
assert(thickness != null), assert(thickness != null),
assert(fadeoutOpacityAnimation != null), assert(fadeoutOpacityAnimation != null),
assert(mainAxisMargin != null), assert(mainAxisMargin != null),
...@@ -103,6 +105,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -103,6 +105,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
_textDirection = textDirection, _textDirection = textDirection,
_thickness = thickness, _thickness = thickness,
_radius = radius, _radius = radius,
_shape = shape,
_padding = padding, _padding = padding,
_mainAxisMargin = mainAxisMargin, _mainAxisMargin = mainAxisMargin,
_crossAxisMargin = crossAxisMargin, _crossAxisMargin = crossAxisMargin,
...@@ -217,6 +220,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -217,6 +220,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
Radius? get radius => _radius; Radius? get radius => _radius;
Radius? _radius; Radius? _radius;
set radius(Radius? value) { set radius(Radius? value) {
assert(shape == null || value == null);
if (radius == value) if (radius == value)
return; return;
...@@ -224,6 +228,26 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -224,6 +228,26 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
notifyListeners(); 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 /// 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. /// well as its side to the nearest edge, in logical pixels.
/// ///
...@@ -447,10 +471,20 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -447,10 +471,20 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
); );
_thumbRect = Offset(x, y) & thumbSize; _thumbRect = Offset(x, y) & thumbSize;
if (radius == null)
canvas.drawRect(_thumbRect!, _paintThumb); if (radius != null) {
else
canvas.drawRRect(RRect.fromRectAndRadius(_thumbRect!, radius!), _paintThumb); 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() { double _thumbExtent() {
...@@ -776,6 +810,7 @@ class RawScrollbar extends StatefulWidget { ...@@ -776,6 +810,7 @@ class RawScrollbar extends StatefulWidget {
required this.child, required this.child,
this.controller, this.controller,
this.isAlwaysShown, this.isAlwaysShown,
this.shape,
this.radius, this.radius,
this.thickness, this.thickness,
this.thumbColor, this.thumbColor,
...@@ -795,6 +830,7 @@ class RawScrollbar extends StatefulWidget { ...@@ -795,6 +830,7 @@ class RawScrollbar extends StatefulWidget {
assert(minOverscrollLength == null || minOverscrollLength <= minThumbLength), assert(minOverscrollLength == null || minOverscrollLength <= minThumbLength),
assert(minOverscrollLength == null || minOverscrollLength >= 0), assert(minOverscrollLength == null || minOverscrollLength >= 0),
assert(fadeDuration != null), assert(fadeDuration != null),
assert(radius == null || shape == null),
assert(timeToFade != null), assert(timeToFade != null),
assert(pressDuration != null), assert(pressDuration != null),
assert(mainAxisMargin != null), assert(mainAxisMargin != null),
...@@ -944,6 +980,39 @@ class RawScrollbar extends StatefulWidget { ...@@ -944,6 +980,39 @@ class RawScrollbar extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final bool? isAlwaysShown; 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. /// The [Radius] of the scrollbar thumb's rounded rectangle corners.
/// ///
/// Scrollbar will be rectangular if [radius] is null, which is the default /// 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 ...@@ -1124,6 +1193,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
fadeoutOpacityAnimation: _fadeoutOpacityAnimation, fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
scrollbarOrientation: widget.scrollbarOrientation, scrollbarOrientation: widget.scrollbarOrientation,
mainAxisMargin: widget.mainAxisMargin, mainAxisMargin: widget.mainAxisMargin,
shape: widget.shape,
crossAxisMargin: widget.crossAxisMargin crossAxisMargin: widget.crossAxisMargin
); );
} }
...@@ -1253,6 +1323,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1253,6 +1323,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
..padding = MediaQuery.of(context).padding ..padding = MediaQuery.of(context).padding
..scrollbarOrientation = widget.scrollbarOrientation ..scrollbarOrientation = widget.scrollbarOrientation
..mainAxisMargin = widget.mainAxisMargin ..mainAxisMargin = widget.mainAxisMargin
..shape = widget.shape
..crossAxisMargin = widget.crossAxisMargin ..crossAxisMargin = widget.crossAxisMargin
..minLength = widget.minThumbLength ..minLength = widget.minThumbLength
..minOverscrollLength = widget.minOverscrollLength ?? widget.minThumbLength; ..minOverscrollLength = widget.minOverscrollLength ?? widget.minThumbLength;
......
...@@ -1445,6 +1445,7 @@ void main() { ...@@ -1445,6 +1445,7 @@ void main() {
), ),
); );
}); });
testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async { testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async {
final ScrollbarPainter painter = ScrollbarPainter( final ScrollbarPainter painter = ScrollbarPainter(
color: _kScrollbarColor, color: _kScrollbarColor,
...@@ -1492,6 +1493,44 @@ void main() { ...@@ -1492,6 +1493,44 @@ void main() {
..rect(rect: const Rect.fromLTRB(794.0, 10.0, 800.0, 358.0)) ..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 { testWidgets('minThumbLength property of RawScrollbar is respected', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1518,6 +1557,42 @@ void main() { ...@@ -1518,6 +1557,42 @@ void main() {
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 21.0))); // thumb ..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 { testWidgets('crossAxisMargin property of RawScrollbar is respected', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1543,6 +1618,40 @@ void main() { ...@@ -1543,6 +1618,40 @@ void main() {
..rect(rect: const Rect.fromLTRB(764.0, 0.0, 770.0, 360.0))); ..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 { testWidgets('minOverscrollLength property of RawScrollbar is respected', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1575,6 +1684,32 @@ void main() { ...@@ -1575,6 +1684,32 @@ void main() {
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 8.0))); ..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 { testWidgets('The bar can show or hide when the viewport size change', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Widget buildFrame(double height) { 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