-
-
Save slightfoot/beb74749bf2e743a6da294b37a7dcf8d to your computer and use it in GitHub Desktop.
import 'package:flutter/gestures.dart' show DragStartBehavior; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/widgets.dart'; | |
void main() { | |
runApp( | |
MaterialApp( | |
debugShowCheckedModeBanner: false, | |
theme: ThemeData( | |
primaryColor: Colors.indigo, | |
accentColor: Colors.pinkAccent, | |
), | |
home: ExampleScreen(), | |
), | |
); | |
} | |
class ExampleScreen extends StatefulWidget { | |
@override | |
_ExampleScreenState createState() => _ExampleScreenState(); | |
} | |
class _ExampleScreenState extends State<ExampleScreen> { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('SingleChildScrollView With Scrollbar'), | |
), | |
body: SingleChildScrollViewWithScrollbar( | |
scrollbarColor: Theme.of(context).accentColor.withOpacity(0.75), | |
scrollbarThickness: 8.0, | |
child: Container( | |
height: 1500, | |
child: Placeholder(), | |
), | |
), | |
); | |
} | |
} | |
class SingleChildScrollViewWithScrollbar extends StatefulWidget { | |
const SingleChildScrollViewWithScrollbar({ | |
Key key, | |
this.scrollDirection = Axis.vertical, | |
this.reverse = false, | |
this.padding, | |
this.primary, | |
this.physics, | |
this.controller, | |
this.child, | |
this.dragStartBehavior = DragStartBehavior.down, | |
this.scrollbarColor, | |
this.scrollbarThickness = 6.0, | |
}) : super(key: key); | |
final Axis scrollDirection; | |
final bool reverse; | |
final EdgeInsets padding; | |
final bool primary; | |
final ScrollPhysics physics; | |
final ScrollController controller; | |
final Widget child; | |
final DragStartBehavior dragStartBehavior; | |
final Color scrollbarColor; | |
final double scrollbarThickness; | |
@override | |
_SingleChildScrollViewWithScrollbarState createState() => _SingleChildScrollViewWithScrollbarState(); | |
} | |
class _SingleChildScrollViewWithScrollbarState extends State<SingleChildScrollViewWithScrollbar> { | |
AlwaysVisibleScrollbarPainter _scrollbarPainter; | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
rebuildPainter(); | |
} | |
@override | |
void didUpdateWidget(SingleChildScrollViewWithScrollbar oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
rebuildPainter(); | |
} | |
void rebuildPainter() { | |
final theme = Theme.of(context); | |
_scrollbarPainter = AlwaysVisibleScrollbarPainter( | |
color: widget.scrollbarColor ?? theme.highlightColor.withOpacity(1.0), | |
textDirection: Directionality.of(context), | |
thickness: widget.scrollbarThickness, | |
); | |
} | |
@override | |
void dispose() { | |
_scrollbarPainter?.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return RepaintBoundary( | |
child: CustomPaint( | |
foregroundPainter: _scrollbarPainter, | |
child: RepaintBoundary( | |
child: SingleChildScrollView( | |
scrollDirection: widget.scrollDirection, | |
reverse: widget.reverse, | |
padding: widget.padding, | |
primary: widget.primary, | |
physics: widget.physics, | |
controller: widget.controller, | |
dragStartBehavior: widget.dragStartBehavior, | |
child: Builder( | |
builder: (BuildContext context) { | |
_scrollbarPainter.scrollable = Scrollable.of(context); | |
return widget.child; | |
}, | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
class AlwaysVisibleScrollbarPainter extends ScrollbarPainter { | |
AlwaysVisibleScrollbarPainter({ | |
@required Color color, | |
@required TextDirection textDirection, | |
@required double thickness, | |
}) : super( | |
color: color, | |
textDirection: textDirection, | |
thickness: thickness, | |
fadeoutOpacityAnimation: const AlwaysStoppedAnimation(1.0), | |
); | |
ScrollableState _scrollable; | |
ScrollableState get scrollable => _scrollable; | |
set scrollable(ScrollableState value) { | |
_scrollable?.position?.removeListener(_onScrollChanged); | |
_scrollable = value; | |
_scrollable?.position?.addListener(_onScrollChanged); | |
_onScrollChanged(); | |
} | |
void _onScrollChanged() { | |
update(_scrollable.position, _scrollable.axisDirection); | |
} | |
@override | |
void dispose() { | |
_scrollable?.position?.removeListener(notifyListeners); | |
super.dispose(); | |
} | |
} |
This is great and I integrated it in a demo project I made. I did find a bug, when you press on the back button on the application it crashes if you have not touched the pane with the scrollbar. This is because your disposed method on your _SingleChildScrollViewWithScrollbarState is removing the AlwaysVisibleScrollbarPainter when it should not. Deleting the dispose override on the_SingleChildScrollViewWithScrollbarState fixes it. It would be awesome if you could update and revise my finding (this happened on the Android devices I tested with).
@override void dispose() { _scrollbarPainter?.dispose(); super.dispose(); }
Thank you so much!
Alex
I know this thing is named "always visible scrollbar" but is there a possibility to hide it if there are not enough elements that it's scrollable?
@marcelser I don't know how to make the AlwaysVisibleScrollbarPainter
paint nothing ("hide") but i think the way to determine whether or not we need a scrollbar at all would be to check _scrollable?.position?.pixels == 0
(preferentially in _onScrollChanged()
?).
Fantastic example guys, helping me a lot with my project
Actually, I have found using the answer at https://stackoverflow.com/questions/51069712/how-to-know-if-a-widget-is-visible-within-a-viewport/57252652#57252652, I wrapped the last widget inside my ScrollView
a VisibilityDetector()
and if it was in fact visible, simply change the color of the scrollbar to transparent.
Why is this not a Flutter Package yet, friend of mine?
ββββββββ Exception caught by widgets library βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The following assertion was thrown while finalizing the widget tree:
A ScrollPositionWithSingleContext was used after being disposed.
Once you have called dispose() on a ScrollPositionWithSingleContext, it can no longer be used.
When the exception was thrown, this was the stack:
#0 ChangeNotifier._debugAssertNotDisposed.<anonymous closure> (package:flutter/src/foundation/change_notifier.dart:106:9)
#1 ChangeNotifier._debugAssertNotDisposed (package:flutter/src/foundation/change_notifier.dart:112:6)
#2 ChangeNotifier.removeListener (package:flutter/src/foundation/change_notifier.dart:167:12)
#3 AlwaysVisibleScrollbarPainter.dispose (package:MY_APP/parts/always_scrollbar.dart:123:28)
#4 _SingleChildScrollViewWithScrollbarState.dispose (package:MY_APP/parts/always_scrollbar.dart:63:24)
...
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π