Unverified Commit ac3b77bd authored by Onat Çipli's avatar Onat Çipli Committed by GitHub

Scrollbar display always (#50752)

parent 763f8754
......@@ -60,6 +60,7 @@ class CupertinoScrollbar extends StatefulWidget {
const CupertinoScrollbar({
Key key,
this.controller,
this.isAlwaysShown = false,
@required this.child,
}) : super(key: key);
......@@ -125,6 +126,60 @@ class CupertinoScrollbar extends StatefulWidget {
/// {@endtemplate}
final ScrollController controller;
/// {@template flutter.cupertino.cupertinoScrollbar.isAlwaysShown}
/// Indicates whether the [Scrollbar] should always be visible.
///
/// When false, the scrollbar will be shown during scrolling
/// and will fade out otherwise.
///
/// When true, the scrollbar will always be visible and never fade out.
///
/// The [controller] property must be set in this case.
/// It should be passed the relevant [Scrollable]'s [ScrollController].
///
/// Defaults to false.
///
/// {@tool snippet}
///
/// ```dart
/// final ScrollController _controllerOne = ScrollController();
/// final ScrollController _controllerTwo = ScrollController();
///
/// build(BuildContext context) {
/// return Column(
/// children: <Widget>[
/// Container(
/// height: 200,
/// child: Scrollbar(
/// isAlwaysShown: true,
/// controller: _controllerOne,
/// child: ListView.builder(
/// controller: _controllerOne,
/// itemCount: 120,
/// itemBuilder: (BuildContext context, int index)
/// => Text('item $index'),
/// ),
/// ),
/// ),
/// Container(
/// height: 200,
/// child: CupertinoScrollbar(
/// isAlwaysShown: true,
/// controller: _controllerTwo,
/// child: SingleChildScrollView(
/// controller: _controllerTwo,
/// child: SizedBox(height: 2000, width: 500,),
/// ),
/// ),
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
/// {@endtemplate}
final bool isAlwaysShown;
@override
_CupertinoScrollbarState createState() => _CupertinoScrollbarState();
}
......@@ -183,6 +238,28 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
..color = CupertinoDynamicColor.resolve(_kScrollbarColor, context)
..padding = MediaQuery.of(context).padding;
}
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
if (widget.isAlwaysShown) {
assert(widget.controller != null);
// Wait one frame and cause an empty scroll event. This allows the
// thumb to show immediately when isAlwaysShown is true. A scroll
// event is required in order to paint the thumb.
widget.controller.position.didUpdateScrollPositionBy(0);
}
});
}
@override
void didUpdateWidget(CupertinoScrollbar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isAlwaysShown != oldWidget.isAlwaysShown) {
if (widget.isAlwaysShown == true) {
assert(widget.controller != null);
_fadeoutAnimationController.animateTo(1.0);
} else {
_fadeoutAnimationController.reverse();
}
}
}
/// Returns a [ScrollbarPainter] visually styled like the iOS scrollbar.
......@@ -228,11 +305,13 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
}
void _startFadeoutTimer() {
_fadeoutTimer?.cancel();
_fadeoutTimer = Timer(_kScrollbarTimeToFade, () {
_fadeoutAnimationController.reverse();
_fadeoutTimer = null;
});
if (!widget.isAlwaysShown) {
_fadeoutTimer?.cancel();
_fadeoutTimer = Timer(_kScrollbarTimeToFade, () {
_fadeoutAnimationController.reverse();
_fadeoutTimer = null;
});
}
}
bool _checkVertical() {
......@@ -267,7 +346,7 @@ class _CupertinoScrollbarState extends State<CupertinoScrollbar> with TickerProv
_fadeoutTimer?.cancel();
_thicknessAnimationController.forward().then<void>(
(_) => HapticFeedback.mediumImpact(),
);
);
}
void _handleLongPressMoveUpdate(LongPressMoveUpdateDetails details) {
......
......@@ -37,6 +37,7 @@ class Scrollbar extends StatefulWidget {
Key key,
@required this.child,
this.controller,
this.isAlwaysShown = false,
}) : super(key: key);
/// The widget below this widget in the tree.
......@@ -50,6 +51,9 @@ class Scrollbar extends StatefulWidget {
/// {@macro flutter.cupertino.cupertinoScrollbar.controller}
final ScrollController controller;
/// {@macro flutter.cupertino.cupertinoScrollbar.isAlwaysShown}
final bool isAlwaysShown;
@override
_ScrollbarState createState() => _ScrollbarState();
}
......@@ -102,11 +106,33 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
_textDirection = Directionality.of(context);
_materialPainter = _buildMaterialScrollbarPainter();
_useCupertinoScrollbar = false;
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
if (widget.isAlwaysShown) {
assert(widget.controller != null);
// Wait one frame and cause an empty scroll event. This allows the
// thumb to show immediately when isAlwaysShown is true. A scroll
// event is required in order to paint the thumb.
widget.controller.position.didUpdateScrollPositionBy(0);
}
});
break;
}
assert(_useCupertinoScrollbar != null);
}
@override
void didUpdateWidget(Scrollbar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isAlwaysShown != oldWidget.isAlwaysShown) {
assert(widget.controller != null);
if (widget.isAlwaysShown == false) {
_fadeoutAnimationController.reverse();
} else {
_fadeoutAnimationController.animateTo(1.0);
}
}
}
ScrollbarPainter _buildMaterialScrollbarPainter() {
return ScrollbarPainter(
color: _themeColor,
......@@ -126,17 +152,23 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
// iOS sub-delegates to the CupertinoScrollbar instead and doesn't handle
// scroll notifications here.
if (!_useCupertinoScrollbar &&
(notification is ScrollUpdateNotification || notification is OverscrollNotification)) {
(notification is ScrollUpdateNotification ||
notification is OverscrollNotification)) {
if (_fadeoutAnimationController.status != AnimationStatus.forward) {
_fadeoutAnimationController.forward();
}
_materialPainter.update(notification.metrics, notification.metrics.axisDirection);
_fadeoutTimer?.cancel();
_fadeoutTimer = Timer(_kScrollbarTimeToFade, () {
_fadeoutAnimationController.reverse();
_fadeoutTimer = null;
});
_materialPainter.update(
notification.metrics,
notification.metrics.axisDirection,
);
if (!widget.isAlwaysShown) {
_fadeoutTimer?.cancel();
_fadeoutTimer = Timer(_kScrollbarTimeToFade, () {
_fadeoutAnimationController.reverse();
_fadeoutTimer = null;
});
}
}
return false;
}
......@@ -154,6 +186,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
if (_useCupertinoScrollbar) {
return CupertinoScrollbar(
child: widget.child,
isAlwaysShown: widget.isAlwaysShown,
controller: widget.controller,
);
}
......
......@@ -163,4 +163,237 @@ void main() {
await tester.pump(_kScrollbarTimeToFade);
await tester.pump(_kScrollbarFadeDuration);
});
testWidgets('On first render with isAlwaysShown: true, the thumb shows',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: controller,
child: CupertinoScrollbar(
isAlwaysShown: true,
controller: controller,
child: const SingleChildScrollView(
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
// The scrollbar measures its size on the first frame
// and renders starting in the second,
//
// so pumpAndSettle a frame to allow it to appear.
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rrect());
});
testWidgets('On first render with isAlwaysShown: false, the thumb is hidden',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: controller,
child: CupertinoScrollbar(
isAlwaysShown: false,
controller: controller,
child: const SingleChildScrollView(
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), isNot(paints..rect()));
});
testWidgets(
'With isAlwaysShown: true, fling a scroll. While it is still scrolling, set isAlwaysShown: false. The thumb should not fade out until the scrolling stops.',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool isAlwaysShown = true;
Widget viewWithScroll() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Stack(
children: <Widget>[
CupertinoScrollbar(
isAlwaysShown: isAlwaysShown,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
Positioned(
bottom: 10,
child: CupertinoButton(
onPressed: () {
setState(() {
isAlwaysShown = !isAlwaysShown;
});
},
child: const Text('change isAlwaysShown'),
),
)
],
),
),
);
},
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
await tester.fling(
find.byType(SingleChildScrollView),
const Offset(0.0, -10.0),
10,
);
expect(find.byType(CupertinoScrollbar), paints..rrect());
await tester.tap(find.byType(CupertinoButton));
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
});
testWidgets(
'With isAlwaysShown: false, fling a scroll. While it is still scrolling, set isAlwaysShown: true. The thumb should not fade even after the scrolling stops',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool isAlwaysShown = false;
Widget viewWithScroll() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Stack(
children: <Widget>[
CupertinoScrollbar(
isAlwaysShown: isAlwaysShown,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
Positioned(
bottom: 10,
child: CupertinoButton(
onPressed: () {
setState(() {
isAlwaysShown = !isAlwaysShown;
});
},
child: const Text('change isAlwaysShown'),
),
)
],
),
),
);
},
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
await tester.fling(
find.byType(SingleChildScrollView),
const Offset(0.0, -10.0),
10,
);
expect(find.byType(CupertinoScrollbar), paints..rrect());
await tester.tap(find.byType(CupertinoButton));
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rrect());
});
testWidgets(
'Toggling isAlwaysShown while not scrolling fades the thumb in/out. This works even when you have never scrolled at all yet',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool isAlwaysShown = true;
Widget viewWithScroll() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Stack(
children: <Widget>[
CupertinoScrollbar(
isAlwaysShown: isAlwaysShown,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
Positioned(
bottom: 10,
child: CupertinoButton(
onPressed: () {
setState(() {
isAlwaysShown = !isAlwaysShown;
});
},
child: const Text('change isAlwaysShown'),
),
)
],
),
),
);
},
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rrect());
await tester.tap(find.byType(CupertinoButton));
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
});
}
......@@ -201,4 +201,211 @@ void main() {
expect(scrollbar.controller, isNotNull);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('On first render with isAlwaysShown: true, the thumb shows',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return _buildBoilerplate(
child: Theme(
data: ThemeData(),
child: Scrollbar(
isAlwaysShown: true,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(Scrollbar), paints..rect());
});
testWidgets('On first render with isAlwaysShown: false, the thumb is hidden',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return _buildBoilerplate(
child: Theme(
data: ThemeData(),
child: Scrollbar(
isAlwaysShown: false,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(Scrollbar), isNot(paints..rect()));
});
testWidgets(
'With isAlwaysShown: true, fling a scroll. While it is still scrolling, set isAlwaysShown: false. The thumb should not fade out until the scrolling stops.',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool isAlwaysShown = true;
Widget viewWithScroll() {
return _buildBoilerplate(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Theme(
data: ThemeData(),
child: Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.threed_rotation),
onPressed: () {
setState(() {
isAlwaysShown = !isAlwaysShown;
});
},
),
body: Scrollbar(
isAlwaysShown: isAlwaysShown,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
);
},
),
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
await tester.fling(
find.byType(SingleChildScrollView),
const Offset(0.0, -10.0),
10,
);
expect(find.byType(Scrollbar), paints..rect());
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
// Scrollbar is not showing after scroll finishes
expect(find.byType(Scrollbar), isNot(paints..rect()));
});
testWidgets(
'With isAlwaysShown: false, fling a scroll. While it is still scrolling, set isAlwaysShown: true. The thumb should not fade even after the scrolling stops',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool isAlwaysShown = false;
Widget viewWithScroll() {
return _buildBoilerplate(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Theme(
data: ThemeData(),
child: Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.threed_rotation),
onPressed: () {
setState(() {
isAlwaysShown = !isAlwaysShown;
});
},
),
body: Scrollbar(
isAlwaysShown: isAlwaysShown,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
);
},
),
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
await tester.fling(
find.byType(SingleChildScrollView),
const Offset(0.0, -10.0),
10,
);
expect(find.byType(Scrollbar), paints..rect());
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
// Scrollbar is not showing after scroll finishes
expect(find.byType(Scrollbar), paints..rect());
});
testWidgets(
'Toggling isAlwaysShown while not scrolling fades the thumb in/out. This works even when you have never scrolled at all yet',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool isAlwaysShown = true;
Widget viewWithScroll() {
return _buildBoilerplate(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Theme(
data: ThemeData(),
child: Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.threed_rotation),
onPressed: () {
setState(() {
isAlwaysShown = !isAlwaysShown;
});
},
),
body: Scrollbar(
isAlwaysShown: isAlwaysShown,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
);
},
),
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
final Finder materialScrollbar = find.byType(Scrollbar);
expect(materialScrollbar, paints..rect());
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
expect(materialScrollbar, isNot(paints..rect()));
});
}
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