Skip to content

Commit 4f0c82b

Browse files
authored
fixes isAlwaysShown material scrollbar.dart (flutter#54128)
1 parent 8cb6d5e commit 4f0c82b

File tree

4 files changed

+145
-21
lines changed

4 files changed

+145
-21
lines changed

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -238,23 +238,15 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
238238
..color = CupertinoDynamicColor.resolve(_kScrollbarColor, context)
239239
..padding = MediaQuery.of(context).padding;
240240
}
241-
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
242-
if (widget.isAlwaysShown) {
243-
assert(widget.controller != null);
244-
// Wait one frame and cause an empty scroll event. This allows the
245-
// thumb to show immediately when isAlwaysShown is true. A scroll
246-
// event is required in order to paint the thumb.
247-
widget.controller.position.didUpdateScrollPositionBy(0);
248-
}
249-
});
241+
_triggerScrollbar();
250242
}
251243

252244
@override
253245
void didUpdateWidget(CupertinoScrollbar oldWidget) {
254246
super.didUpdateWidget(oldWidget);
255247
if (widget.isAlwaysShown != oldWidget.isAlwaysShown) {
256248
if (widget.isAlwaysShown == true) {
257-
assert(widget.controller != null);
249+
_triggerScrollbar();
258250
_fadeoutAnimationController.animateTo(1.0);
259251
} else {
260252
_fadeoutAnimationController.reverse();
@@ -278,6 +270,19 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
278270
);
279271
}
280272

273+
// Wait one frame and cause an empty scroll event. This allows the thumb to
274+
// show immediately when isAlwaysShown is true. A scroll event is required in
275+
// order to paint the thumb.
276+
void _triggerScrollbar() {
277+
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
278+
if (widget.isAlwaysShown) {
279+
assert(widget.controller != null);
280+
_fadeoutTimer?.cancel();
281+
widget.controller.position.didUpdateScrollPositionBy(0);
282+
}
283+
});
284+
}
285+
281286
// Handle a gesture that drags the scrollbar by the given amount.
282287
void _dragScrollbar(double primaryDelta) {
283288
assert(_currentController != null);

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
106106
_textDirection = Directionality.of(context);
107107
_materialPainter = _buildMaterialScrollbarPainter();
108108
_useCupertinoScrollbar = false;
109-
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
110-
if (widget.isAlwaysShown) {
111-
assert(widget.controller != null);
112-
// Wait one frame and cause an empty scroll event. This allows the
113-
// thumb to show immediately when isAlwaysShown is true. A scroll
114-
// event is required in order to paint the thumb.
115-
widget.controller.position.didUpdateScrollPositionBy(0);
116-
}
117-
});
109+
_triggerScrollbar();
118110
break;
119111
}
120112
assert(_useCupertinoScrollbar != null);
@@ -124,15 +116,28 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
124116
void didUpdateWidget(Scrollbar oldWidget) {
125117
super.didUpdateWidget(oldWidget);
126118
if (widget.isAlwaysShown != oldWidget.isAlwaysShown) {
127-
assert(widget.controller != null);
128119
if (widget.isAlwaysShown == false) {
129120
_fadeoutAnimationController.reverse();
130121
} else {
122+
_triggerScrollbar();
131123
_fadeoutAnimationController.animateTo(1.0);
132124
}
133125
}
134126
}
135127

128+
// Wait one frame and cause an empty scroll event. This allows the thumb to
129+
// show immediately when isAlwaysShown is true. A scroll event is required in
130+
// order to paint the thumb.
131+
void _triggerScrollbar() {
132+
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
133+
if (widget.isAlwaysShown) {
134+
assert(widget.controller != null);
135+
_fadeoutTimer?.cancel();
136+
widget.controller.position.didUpdateScrollPositionBy(0);
137+
}
138+
});
139+
}
140+
136141
ScrollbarPainter _buildMaterialScrollbarPainter() {
137142
return ScrollbarPainter(
138143
color: _themeColor,

packages/flutter/test/cupertino/scrollbar_test.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,59 @@ void main() {
286286
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
287287
});
288288

289+
testWidgets(
290+
'With isAlwaysShown: false, set isAlwaysShown: true. The thumb should be always shown directly',
291+
(WidgetTester tester) async {
292+
final ScrollController controller = ScrollController();
293+
bool isAlwaysShown = false;
294+
Widget viewWithScroll() {
295+
return StatefulBuilder(
296+
builder: (BuildContext context, StateSetter setState) {
297+
return Directionality(
298+
textDirection: TextDirection.ltr,
299+
child: MediaQuery(
300+
data: const MediaQueryData(),
301+
child: Stack(
302+
children: <Widget>[
303+
CupertinoScrollbar(
304+
isAlwaysShown: isAlwaysShown,
305+
controller: controller,
306+
child: SingleChildScrollView(
307+
controller: controller,
308+
child: const SizedBox(
309+
width: 4000.0,
310+
height: 4000.0,
311+
),
312+
),
313+
),
314+
Positioned(
315+
bottom: 10,
316+
child: CupertinoButton(
317+
onPressed: () {
318+
setState(() {
319+
isAlwaysShown = !isAlwaysShown;
320+
});
321+
},
322+
child: const Text('change isAlwaysShown'),
323+
),
324+
)
325+
],
326+
),
327+
),
328+
);
329+
},
330+
);
331+
}
332+
333+
await tester.pumpWidget(viewWithScroll());
334+
await tester.pumpAndSettle();
335+
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
336+
337+
await tester.tap(find.byType(CupertinoButton));
338+
await tester.pumpAndSettle();
339+
expect(find.byType(CupertinoScrollbar), paints..rrect());
340+
});
341+
289342
testWidgets(
290343
'With isAlwaysShown: false, fling a scroll. While it is still scrolling, set isAlwaysShown: true. The thumb should not fade even after the scrolling stops',
291344
(WidgetTester tester) async {
@@ -332,6 +385,7 @@ void main() {
332385

333386
await tester.pumpWidget(viewWithScroll());
334387
await tester.pumpAndSettle();
388+
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
335389
await tester.fling(
336390
find.byType(SingleChildScrollView),
337391
const Offset(0.0, -10.0),
@@ -340,7 +394,13 @@ void main() {
340394
expect(find.byType(CupertinoScrollbar), paints..rrect());
341395

342396
await tester.tap(find.byType(CupertinoButton));
397+
await tester.pump();
398+
expect(find.byType(CupertinoScrollbar), paints..rrect());
399+
400+
// Wait for the timer delay to expire.
401+
await tester.pump(const Duration(milliseconds: 600)); // _kScrollbarTimeToFade
343402
await tester.pumpAndSettle();
403+
// Scrollbar thumb is showing after scroll finishes and timer ends.
344404
expect(find.byType(CupertinoScrollbar), paints..rrect());
345405
});
346406

packages/flutter/test/material/scrollbar_test.dart

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,54 @@ void main() {
308308
expect(find.byType(Scrollbar), isNot(paints..rect()));
309309
});
310310

311+
testWidgets(
312+
'With isAlwaysShown: false, set isAlwaysShown: true. The thumb should be always shown directly',
313+
(WidgetTester tester) async {
314+
final ScrollController controller = ScrollController();
315+
bool isAlwaysShown = false;
316+
Widget viewWithScroll() {
317+
return _buildBoilerplate(
318+
child: StatefulBuilder(
319+
builder: (BuildContext context, StateSetter setState) {
320+
return Theme(
321+
data: ThemeData(),
322+
child: Scaffold(
323+
floatingActionButton: FloatingActionButton(
324+
child: const Icon(Icons.threed_rotation),
325+
onPressed: () {
326+
setState(() {
327+
isAlwaysShown = !isAlwaysShown;
328+
});
329+
},
330+
),
331+
body: Scrollbar(
332+
isAlwaysShown: isAlwaysShown,
333+
controller: controller,
334+
child: SingleChildScrollView(
335+
controller: controller,
336+
child: const SizedBox(
337+
width: 4000.0,
338+
height: 4000.0,
339+
),
340+
),
341+
),
342+
),
343+
);
344+
},
345+
),
346+
);
347+
}
348+
349+
await tester.pumpWidget(viewWithScroll());
350+
await tester.pumpAndSettle();
351+
expect(find.byType(Scrollbar), isNot(paints..rect()));
352+
353+
await tester.tap(find.byType(FloatingActionButton));
354+
await tester.pumpAndSettle();
355+
// Scrollbar is not showing after scroll finishes
356+
expect(find.byType(Scrollbar), paints..rect());
357+
});
358+
311359
testWidgets(
312360
'With isAlwaysShown: false, fling a scroll. While it is still scrolling, set isAlwaysShown: true. The thumb should not fade even after the scrolling stops',
313361
(WidgetTester tester) async {
@@ -348,6 +396,7 @@ void main() {
348396

349397
await tester.pumpWidget(viewWithScroll());
350398
await tester.pumpAndSettle();
399+
expect(find.byType(Scrollbar), isNot(paints..rect()));
351400
await tester.fling(
352401
find.byType(SingleChildScrollView),
353402
const Offset(0.0, -10.0),
@@ -356,8 +405,13 @@ void main() {
356405
expect(find.byType(Scrollbar), paints..rect());
357406

358407
await tester.tap(find.byType(FloatingActionButton));
408+
await tester.pump();
409+
expect(find.byType(Scrollbar), paints..rect());
410+
411+
// Wait for the timer delay to expire.
412+
await tester.pump(const Duration(milliseconds: 600)); // _kScrollbarTimeToFade
359413
await tester.pumpAndSettle();
360-
// Scrollbar is not showing after scroll finishes
414+
// Scrollbar thumb is showing after scroll finishes and timer ends.
361415
expect(find.byType(Scrollbar), paints..rect());
362416
});
363417

0 commit comments

Comments
 (0)