Created
March 4, 2019 18:20
-
-
Save slightfoot/beb74749bf2e743a6da294b37a7dcf8d to your computer and use it in GitHub Desktop.
Always Visible Scrollbar for Flutter - 4th March 2019
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
@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)
...
════════════════════════════════════════════════════════════════════════════════════════════════════
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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?