Skip to content

Commit bb278d1

Browse files
author
Mohammad Ghalayini
authored
[new feature] Add support for a RawScrollbar.shape (flutter#85652)
1 parent f5dd3d9 commit bb278d1

File tree

2 files changed

+209
-3
lines changed

2 files changed

+209
-3
lines changed

packages/flutter/lib/src/widgets/scrollbar.dart

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,12 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
8585
double mainAxisMargin = 0.0,
8686
double crossAxisMargin = 0.0,
8787
Radius? radius,
88+
OutlinedBorder? shape,
8889
double minLength = _kMinThumbExtent,
8990
double? minOverscrollLength,
9091
ScrollbarOrientation? scrollbarOrientation,
9192
}) : assert(color != null),
93+
assert(radius == null || shape == null),
9294
assert(thickness != null),
9395
assert(fadeoutOpacityAnimation != null),
9496
assert(mainAxisMargin != null),
@@ -103,6 +105,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
103105
_textDirection = textDirection,
104106
_thickness = thickness,
105107
_radius = radius,
108+
_shape = shape,
106109
_padding = padding,
107110
_mainAxisMargin = mainAxisMargin,
108111
_crossAxisMargin = crossAxisMargin,
@@ -217,13 +220,34 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
217220
Radius? get radius => _radius;
218221
Radius? _radius;
219222
set radius(Radius? value) {
223+
assert(shape == null || value == null);
220224
if (radius == value)
221225
return;
222226

223227
_radius = value;
224228
notifyListeners();
225229
}
226230

231+
/// The [OutlinedBorder] of the scrollbar's thumb.
232+
///
233+
/// Only one of [radius] and [shape] may be specified. For a rounded rectangle,
234+
/// it's simplest to just specify [radius]. By default, the scrollbar thumb's
235+
/// shape is a simple rectangle.
236+
///
237+
/// If [shape] is specified, the thumb will take the shape of the passed
238+
/// [OutlinedBorder] and fill itself with [color] (or grey if it
239+
/// is unspecified).
240+
///
241+
OutlinedBorder? get shape => _shape;
242+
OutlinedBorder? _shape;
243+
set shape(OutlinedBorder? value){
244+
assert(radius == null || value == null);
245+
if(shape == value)
246+
return;
247+
248+
_shape = value;
249+
notifyListeners();
250+
}
227251
/// The amount of space by which to inset the scrollbar's start and end, as
228252
/// well as its side to the nearest edge, in logical pixels.
229253
///
@@ -447,10 +471,20 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
447471
);
448472

449473
_thumbRect = Offset(x, y) & thumbSize;
450-
if (radius == null)
451-
canvas.drawRect(_thumbRect!, _paintThumb);
452-
else
474+
475+
if (radius != null) {
453476
canvas.drawRRect(RRect.fromRectAndRadius(_thumbRect!, radius!), _paintThumb);
477+
return;
478+
}
479+
480+
if (shape == null) {
481+
canvas.drawRect(_thumbRect!, _paintThumb);
482+
return;
483+
}
484+
485+
final Path outerPath = shape!.getOuterPath(_thumbRect!);
486+
canvas.drawPath(outerPath, _paintThumb);
487+
shape!.paint(canvas, _thumbRect!);
454488
}
455489

456490
double _thumbExtent() {
@@ -776,6 +810,7 @@ class RawScrollbar extends StatefulWidget {
776810
required this.child,
777811
this.controller,
778812
this.isAlwaysShown,
813+
this.shape,
779814
this.radius,
780815
this.thickness,
781816
this.thumbColor,
@@ -795,6 +830,7 @@ class RawScrollbar extends StatefulWidget {
795830
assert(minOverscrollLength == null || minOverscrollLength <= minThumbLength),
796831
assert(minOverscrollLength == null || minOverscrollLength >= 0),
797832
assert(fadeDuration != null),
833+
assert(radius == null || shape == null),
798834
assert(timeToFade != null),
799835
assert(pressDuration != null),
800836
assert(mainAxisMargin != null),
@@ -944,6 +980,39 @@ class RawScrollbar extends StatefulWidget {
944980
/// {@endtemplate}
945981
final bool? isAlwaysShown;
946982

983+
/// The [OutlinedBorder] of the scrollbar's thumb.
984+
///
985+
/// Only one of [radius] and [shape] may be specified. For a rounded rectangle,
986+
/// it's simplest to just specify [radius]. By default, the scrollbar thumb's
987+
/// shape is a simple rectangle.
988+
///
989+
/// If [shape] is specified, the thumb will take the shape of the passed
990+
/// [OutlinedBorder] and fill itself with [thumbColor] (or grey if it
991+
/// is unspecified).
992+
///
993+
/// Here is an example of using a [StadiumBorder] for drawing the [shape] of the
994+
/// thumb in a [RawScrollbar]:
995+
///
996+
/// {@tool dartpad --template=stateless_widget_material}
997+
/// ```dart
998+
/// Widget build(BuildContext context) {
999+
/// return Scaffold(
1000+
/// body: RawScrollbar(
1001+
/// child: ListView(
1002+
/// children: List<Text>.generate(100, (int index) => Text((index * index).toString())),
1003+
/// physics: const BouncingScrollPhysics(),
1004+
/// ),
1005+
/// shape: const StadiumBorder(side: BorderSide(color: Colors.brown, width: 3.0)),
1006+
/// thickness: 15.0,
1007+
/// thumbColor: Colors.blue,
1008+
/// isAlwaysShown: true,
1009+
/// ),
1010+
/// );
1011+
/// }
1012+
/// ```
1013+
/// {@end-tool}
1014+
final OutlinedBorder? shape;
1015+
9471016
/// The [Radius] of the scrollbar thumb's rounded rectangle corners.
9481017
///
9491018
/// 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
11241193
fadeoutOpacityAnimation: _fadeoutOpacityAnimation,
11251194
scrollbarOrientation: widget.scrollbarOrientation,
11261195
mainAxisMargin: widget.mainAxisMargin,
1196+
shape: widget.shape,
11271197
crossAxisMargin: widget.crossAxisMargin
11281198
);
11291199
}
@@ -1253,6 +1323,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
12531323
..padding = MediaQuery.of(context).padding
12541324
..scrollbarOrientation = widget.scrollbarOrientation
12551325
..mainAxisMargin = widget.mainAxisMargin
1326+
..shape = widget.shape
12561327
..crossAxisMargin = widget.crossAxisMargin
12571328
..minLength = widget.minThumbLength
12581329
..minOverscrollLength = widget.minOverscrollLength ?? widget.minThumbLength;

packages/flutter/test/widgets/scrollbar_test.dart

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,7 @@ void main() {
14451445
),
14461446
);
14471447
});
1448+
14481449
testWidgets('ScrollbarPainter asserts if scrollbarOrientation is used with wrong axisDirection', (WidgetTester tester) async {
14491450
final ScrollbarPainter painter = ScrollbarPainter(
14501451
color: _kScrollbarColor,
@@ -1492,6 +1493,44 @@ void main() {
14921493
..rect(rect: const Rect.fromLTRB(794.0, 10.0, 800.0, 358.0))
14931494
);
14941495
});
1496+
1497+
testWidgets('shape property of RawScrollbar can draw a BeveledRectangleBorder', (WidgetTester tester) async {
1498+
final ScrollController scrollController = ScrollController();
1499+
await tester.pumpWidget(
1500+
Directionality(
1501+
textDirection: TextDirection.ltr,
1502+
child: MediaQuery(
1503+
data: const MediaQueryData(),
1504+
child: RawScrollbar(
1505+
shape: const BeveledRectangleBorder(
1506+
borderRadius: BorderRadius.all(Radius.circular(8.0))
1507+
),
1508+
controller: scrollController,
1509+
isAlwaysShown: true,
1510+
child: SingleChildScrollView(
1511+
controller: scrollController,
1512+
child: const SizedBox(height: 1000.0),
1513+
),
1514+
),
1515+
)));
1516+
await tester.pumpAndSettle();
1517+
expect(
1518+
find.byType(RawScrollbar),
1519+
paints
1520+
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
1521+
..path(
1522+
includes: const <Offset>[
1523+
Offset(797.0, 0.0),
1524+
Offset(797.0, 18.0),
1525+
],
1526+
excludes: const <Offset>[
1527+
Offset(796.0, 0.0),
1528+
Offset(798.0, 0.0),
1529+
],
1530+
),
1531+
);
1532+
});
1533+
14951534
testWidgets('minThumbLength property of RawScrollbar is respected', (WidgetTester tester) async {
14961535
final ScrollController scrollController = ScrollController();
14971536
await tester.pumpWidget(
@@ -1518,6 +1557,42 @@ void main() {
15181557
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 21.0))); // thumb
15191558
});
15201559

1560+
testWidgets('shape property of RawScrollbar can draw a CircleBorder', (WidgetTester tester) async {
1561+
final ScrollController scrollController = ScrollController();
1562+
await tester.pumpWidget(
1563+
Directionality(
1564+
textDirection: TextDirection.ltr,
1565+
child: MediaQuery(
1566+
data: const MediaQueryData(),
1567+
child: RawScrollbar(
1568+
shape: const CircleBorder(side: BorderSide(width: 2.0)),
1569+
thickness: 36.0,
1570+
controller: scrollController,
1571+
isAlwaysShown: true,
1572+
child: SingleChildScrollView(
1573+
controller: scrollController,
1574+
child: const SizedBox(height: 1000.0, width: 1000),
1575+
),
1576+
),
1577+
)));
1578+
await tester.pumpAndSettle();
1579+
1580+
expect(
1581+
find.byType(RawScrollbar),
1582+
paints
1583+
..path(
1584+
includes: const <Offset>[
1585+
Offset(782.0, 180.0),
1586+
Offset(782.0, 180.0 - 18.0),
1587+
Offset(782.0 + 18.0, 180),
1588+
Offset(782.0, 180.0 + 18.0),
1589+
Offset(782.0 - 18.0, 180),
1590+
],
1591+
)
1592+
..circle(x: 782.0, y: 180.0, radius: 17.0, strokeWidth: 2.0)
1593+
);
1594+
});
1595+
15211596
testWidgets('crossAxisMargin property of RawScrollbar is respected', (WidgetTester tester) async {
15221597
final ScrollController scrollController = ScrollController();
15231598
await tester.pumpWidget(
@@ -1543,6 +1618,40 @@ void main() {
15431618
..rect(rect: const Rect.fromLTRB(764.0, 0.0, 770.0, 360.0)));
15441619
});
15451620

1621+
testWidgets('shape property of RawScrollbar can draw a RoundedRectangleBorder', (WidgetTester tester) async {
1622+
final ScrollController scrollController = ScrollController();
1623+
await tester.pumpWidget(
1624+
Directionality(
1625+
textDirection: TextDirection.ltr,
1626+
child: MediaQuery(
1627+
data: const MediaQueryData(),
1628+
child: RawScrollbar(
1629+
thickness: 20,
1630+
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(8))),
1631+
controller: scrollController,
1632+
isAlwaysShown: true,
1633+
child: SingleChildScrollView(
1634+
controller: scrollController,
1635+
child: const SizedBox(height: 1000.0, width: 1000.0),
1636+
),
1637+
),
1638+
)));
1639+
await tester.pumpAndSettle();
1640+
expect(
1641+
find.byType(RawScrollbar),
1642+
paints
1643+
..rect(rect: const Rect.fromLTRB(780.0, 0.0, 800.0, 600.0))
1644+
..path(
1645+
includes: const <Offset>[
1646+
Offset(800.0, 0.0),
1647+
],
1648+
excludes: const <Offset>[
1649+
Offset(780.0, 0.0),
1650+
],
1651+
),
1652+
);
1653+
});
1654+
15461655
testWidgets('minOverscrollLength property of RawScrollbar is respected', (WidgetTester tester) async {
15471656
final ScrollController scrollController = ScrollController();
15481657
await tester.pumpWidget(
@@ -1575,6 +1684,32 @@ void main() {
15751684
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 8.0)));
15761685
});
15771686

1687+
testWidgets('not passing any shape or radius to RawScrollbar will draw the usual rectangular thumb', (WidgetTester tester) async {
1688+
final ScrollController scrollController = ScrollController();
1689+
await tester.pumpWidget(
1690+
Directionality(
1691+
textDirection: TextDirection.ltr,
1692+
child: MediaQuery(
1693+
data: const MediaQueryData(),
1694+
child: RawScrollbar(
1695+
controller: scrollController,
1696+
isAlwaysShown: true,
1697+
child: SingleChildScrollView(
1698+
controller: scrollController,
1699+
child: const SizedBox(height: 1000.0),
1700+
),
1701+
),
1702+
)));
1703+
await tester.pumpAndSettle();
1704+
1705+
expect(
1706+
find.byType(RawScrollbar),
1707+
paints
1708+
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
1709+
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 360.0))
1710+
);
1711+
});
1712+
15781713
testWidgets('The bar can show or hide when the viewport size change', (WidgetTester tester) async {
15791714
final ScrollController scrollController = ScrollController();
15801715
Widget buildFrame(double height) {

0 commit comments

Comments
 (0)