123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651 |
- import 'dart:async';
- import 'package:flutter/material.dart';
- /// Build the Scroll Thumb and label using the current configuration
- typedef ScrollThumbBuilder = Widget Function(
- Color backgroundColor,
- Animation<double> thumbAnimation,
- Animation<double> labelAnimation,
- double height, {
- Text? labelText,
- BoxConstraints? labelConstraints,
- });
- /// Build a Text widget using the current scroll offset
- typedef LabelTextBuilder = Text Function(double offsetY);
- /// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
- /// for quick navigation of the BoxScrollView.
- class DraggableScrollbar extends StatefulWidget {
- /// The view that will be scrolled with the scroll thumb
- final CustomScrollView child;
- /// A function that builds a thumb using the current configuration
- final ScrollThumbBuilder scrollThumbBuilder;
- /// The height of the scroll thumb
- final double heightScrollThumb;
- /// The background color of the label and thumb
- final Color backgroundColor;
- /// The amount of padding that should surround the thumb
- final EdgeInsetsGeometry? padding;
- /// Determines how quickly the scrollbar will animate in and out
- final Duration scrollbarAnimationDuration;
- /// How long should the thumb be visible before fading out
- final Duration scrollbarTimeToFade;
- /// Build a Text widget from the current offset in the BoxScrollView
- final LabelTextBuilder? labelTextBuilder;
- /// Determines box constraints for Container displaying label
- final BoxConstraints? labelConstraints;
- /// The ScrollController for the BoxScrollView
- final ScrollController controller;
- /// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder]
- final bool alwaysVisibleScrollThumb;
- DraggableScrollbar({
- Key? key,
- this.alwaysVisibleScrollThumb = false,
- required this.heightScrollThumb,
- required this.backgroundColor,
- required this.scrollThumbBuilder,
- required this.child,
- required this.controller,
- this.padding,
- this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
- this.scrollbarTimeToFade = const Duration(milliseconds: 600),
- this.labelTextBuilder,
- this.labelConstraints,
- }) : assert(child.scrollDirection == Axis.vertical),
- super(key: key);
- DraggableScrollbar.rrect({
- Key? key,
- Key? scrollThumbKey,
- this.alwaysVisibleScrollThumb = false,
- required this.child,
- required this.controller,
- this.heightScrollThumb = 48.0,
- this.backgroundColor = Colors.white,
- this.padding,
- this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
- this.scrollbarTimeToFade = const Duration(milliseconds: 600),
- this.labelTextBuilder,
- this.labelConstraints,
- }) : assert(child.scrollDirection == Axis.vertical),
- scrollThumbBuilder =
- _thumbRRectBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
- super(key: key);
- DraggableScrollbar.arrows({
- Key? key,
- Key? scrollThumbKey,
- this.alwaysVisibleScrollThumb = false,
- required this.child,
- required this.controller,
- this.heightScrollThumb = 48.0,
- this.backgroundColor = Colors.white,
- this.padding,
- this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
- this.scrollbarTimeToFade = const Duration(milliseconds: 600),
- this.labelTextBuilder,
- this.labelConstraints,
- }) : assert(child.scrollDirection == Axis.vertical),
- scrollThumbBuilder =
- _thumbArrowBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
- super(key: key);
- DraggableScrollbar.semicircle({
- Key? key,
- Key? scrollThumbKey,
- this.alwaysVisibleScrollThumb = false,
- required this.child,
- required this.controller,
- this.heightScrollThumb = 48.0,
- this.backgroundColor = Colors.white,
- this.padding,
- this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
- this.scrollbarTimeToFade = const Duration(milliseconds: 600),
- this.labelTextBuilder,
- this.labelConstraints,
- }) : assert(child.scrollDirection == Axis.vertical),
- scrollThumbBuilder = _thumbSemicircleBuilder(
- heightScrollThumb * 0.6,
- scrollThumbKey,
- alwaysVisibleScrollThumb,
- ),
- super(key: key);
- @override
- DraggableScrollbarState createState() => DraggableScrollbarState();
- static buildScrollThumbAndLabel({
- required Widget scrollThumb,
- required Color backgroundColor,
- required Animation<double>? thumbAnimation,
- required Animation<double>? labelAnimation,
- required Text? labelText,
- required BoxConstraints? labelConstraints,
- required bool alwaysVisibleScrollThumb,
- }) {
- var scrollThumbAndLabel = labelText == null
- ? scrollThumb
- : Row(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- ScrollLabel(
- animation: labelAnimation,
- backgroundColor: backgroundColor,
- constraints: labelConstraints,
- child: labelText,
- ),
- scrollThumb,
- ],
- );
- if (alwaysVisibleScrollThumb) {
- return scrollThumbAndLabel;
- }
- return SlideFadeTransition(
- animation: thumbAnimation!,
- child: scrollThumbAndLabel,
- );
- }
- static ScrollThumbBuilder _thumbSemicircleBuilder(
- double width,
- Key? scrollThumbKey,
- bool alwaysVisibleScrollThumb,
- ) {
- return (
- Color backgroundColor,
- Animation<double> thumbAnimation,
- Animation<double> labelAnimation,
- double height, {
- Text? labelText,
- BoxConstraints? labelConstraints,
- }) {
- final scrollThumb = CustomPaint(
- key: scrollThumbKey,
- foregroundPainter: ArrowCustomPainter(Colors.white),
- child: Material(
- elevation: 4.0,
- color: backgroundColor,
- borderRadius: BorderRadius.only(
- topLeft: Radius.circular(height),
- bottomLeft: Radius.circular(height),
- topRight: const Radius.circular(4.0),
- bottomRight: const Radius.circular(4.0),
- ),
- child: Container(
- constraints: BoxConstraints.tight(Size(width, height)),
- ),
- ),
- );
- return buildScrollThumbAndLabel(
- scrollThumb: scrollThumb,
- backgroundColor: backgroundColor,
- thumbAnimation: thumbAnimation,
- labelAnimation: labelAnimation,
- labelText: labelText,
- labelConstraints: labelConstraints,
- alwaysVisibleScrollThumb: alwaysVisibleScrollThumb,
- );
- };
- }
- static ScrollThumbBuilder _thumbArrowBuilder(
- Key? scrollThumbKey,
- bool alwaysVisibleScrollThumb,
- ) {
- return (
- Color backgroundColor,
- Animation<double> thumbAnimation,
- Animation<double> labelAnimation,
- double height, {
- Text? labelText,
- BoxConstraints? labelConstraints,
- }) {
- final scrollThumb = ClipPath(
- clipper: ArrowClipper(),
- child: Container(
- height: height,
- width: 20.0,
- decoration: BoxDecoration(
- color: backgroundColor,
- borderRadius: const BorderRadius.all(
- Radius.circular(12.0),
- ),
- ),
- ),
- );
- return buildScrollThumbAndLabel(
- scrollThumb: scrollThumb,
- backgroundColor: backgroundColor,
- thumbAnimation: thumbAnimation,
- labelAnimation: labelAnimation,
- labelText: labelText,
- labelConstraints: labelConstraints,
- alwaysVisibleScrollThumb: alwaysVisibleScrollThumb,
- );
- };
- }
- static ScrollThumbBuilder _thumbRRectBuilder(
- Key? scrollThumbKey,
- bool alwaysVisibleScrollThumb,
- ) {
- return (
- Color backgroundColor,
- Animation<double> thumbAnimation,
- Animation<double> labelAnimation,
- double height, {
- Text? labelText,
- BoxConstraints? labelConstraints,
- }) {
- final scrollThumb = Material(
- elevation: 4.0,
- color: backgroundColor,
- borderRadius: const BorderRadius.all(Radius.circular(7.0)),
- child: Container(
- constraints: BoxConstraints.tight(
- Size(16.0, height),
- ),
- ),
- );
- return buildScrollThumbAndLabel(
- scrollThumb: scrollThumb,
- backgroundColor: backgroundColor,
- thumbAnimation: thumbAnimation,
- labelAnimation: labelAnimation,
- labelText: labelText,
- labelConstraints: labelConstraints,
- alwaysVisibleScrollThumb: alwaysVisibleScrollThumb,
- );
- };
- }
- }
- class ScrollLabel extends StatelessWidget {
- final Animation<double>? animation;
- final Color backgroundColor;
- final Text child;
- final BoxConstraints? constraints;
- static const BoxConstraints _defaultConstraints =
- BoxConstraints.tightFor(width: 72.0, height: 28.0);
- const ScrollLabel({
- Key? key,
- required this.child,
- required this.animation,
- required this.backgroundColor,
- this.constraints = _defaultConstraints,
- }) : super(key: key);
- @override
- Widget build(BuildContext context) {
- return FadeTransition(
- opacity: animation!,
- child: Container(
- margin: const EdgeInsets.only(right: 12.0),
- child: Material(
- elevation: 4.0,
- color: backgroundColor,
- borderRadius: const BorderRadius.all(Radius.circular(16.0)),
- child: Container(
- constraints: constraints ?? _defaultConstraints,
- alignment: Alignment.center,
- child: child,
- ),
- ),
- ),
- );
- }
- }
- class DraggableScrollbarState extends State<DraggableScrollbar>
- with TickerProviderStateMixin {
- late double _barOffset;
- late double _viewOffset;
- late bool _isDragInProcess;
- late AnimationController _thumbAnimationController;
- late Animation<double> _thumbAnimation;
- late AnimationController _labelAnimationController;
- late Animation<double> _labelAnimation;
- Timer? _fadeoutTimer;
- @override
- void initState() {
- super.initState();
- _barOffset = 0.0;
- _viewOffset = 0.0;
- _isDragInProcess = false;
- _thumbAnimationController = AnimationController(
- vsync: this,
- duration: widget.scrollbarAnimationDuration,
- );
- _thumbAnimation = CurvedAnimation(
- parent: _thumbAnimationController,
- curve: Curves.fastOutSlowIn,
- );
- _labelAnimationController = AnimationController(
- vsync: this,
- duration: widget.scrollbarAnimationDuration,
- );
- _labelAnimation = CurvedAnimation(
- parent: _labelAnimationController,
- curve: Curves.fastOutSlowIn,
- );
- }
- @override
- void dispose() {
- _thumbAnimationController.dispose();
- _labelAnimationController.dispose();
- _fadeoutTimer?.cancel();
- super.dispose();
- }
- double get barMaxScrollExtent =>
- context.size!.height - widget.heightScrollThumb;
- double get barMinScrollExtent => 0;
- double get viewMaxScrollExtent => widget.controller.position.maxScrollExtent;
- double get viewMinScrollExtent => widget.controller.position.minScrollExtent;
- @override
- Widget build(BuildContext context) {
- Text? labelText;
- if (widget.labelTextBuilder != null && _isDragInProcess) {
- labelText = widget.labelTextBuilder!(
- _viewOffset + _barOffset + widget.heightScrollThumb / 2,
- );
- }
- return LayoutBuilder(
- builder: (BuildContext context, BoxConstraints constraints) {
- //print("LayoutBuilder constraints=$constraints");
- return NotificationListener<ScrollNotification>(
- onNotification: (ScrollNotification notification) {
- changePosition(notification);
- return false;
- },
- child: Stack(
- children: <Widget>[
- RepaintBoundary(
- child: widget.child,
- ),
- RepaintBoundary(
- child: GestureDetector(
- onVerticalDragStart: _onVerticalDragStart,
- onVerticalDragUpdate: _onVerticalDragUpdate,
- onVerticalDragEnd: _onVerticalDragEnd,
- child: Container(
- alignment: Alignment.topRight,
- margin: EdgeInsets.only(top: _barOffset),
- padding: widget.padding,
- child: widget.scrollThumbBuilder(
- widget.backgroundColor,
- _thumbAnimation,
- _labelAnimation,
- widget.heightScrollThumb,
- labelText: labelText,
- labelConstraints: widget.labelConstraints,
- ),
- ),
- ),
- ),
- ],
- ),
- );
- },
- );
- }
- //scroll bar has received notification that it's view was scrolled
- //so it should also changes his position
- //but only if it isn't dragged
- changePosition(ScrollNotification notification) {
- if (_isDragInProcess) {
- return;
- }
- setState(() {
- if (notification is ScrollUpdateNotification) {
- _barOffset += getBarDelta(
- notification.scrollDelta!,
- barMaxScrollExtent,
- viewMaxScrollExtent,
- );
- if (_barOffset < barMinScrollExtent) {
- _barOffset = barMinScrollExtent;
- }
- if (_barOffset > barMaxScrollExtent) {
- _barOffset = barMaxScrollExtent;
- }
- _viewOffset += notification.scrollDelta!;
- if (_viewOffset < widget.controller.position.minScrollExtent) {
- _viewOffset = widget.controller.position.minScrollExtent;
- }
- if (_viewOffset > viewMaxScrollExtent) {
- _viewOffset = viewMaxScrollExtent;
- }
- }
- if (notification is ScrollUpdateNotification ||
- notification is OverscrollNotification) {
- if (_thumbAnimationController.status != AnimationStatus.forward) {
- _thumbAnimationController.forward();
- }
- _fadeoutTimer?.cancel();
- _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () {
- _thumbAnimationController.reverse();
- _labelAnimationController.reverse();
- _fadeoutTimer = null;
- });
- }
- });
- }
- double getBarDelta(
- double scrollViewDelta,
- double barMaxScrollExtent,
- double viewMaxScrollExtent,
- ) {
- return scrollViewDelta * barMaxScrollExtent / viewMaxScrollExtent;
- }
- double getScrollViewDelta(
- double barDelta,
- double barMaxScrollExtent,
- double viewMaxScrollExtent,
- ) {
- return barDelta * viewMaxScrollExtent / barMaxScrollExtent;
- }
- void _onVerticalDragStart(DragStartDetails details) {
- setState(() {
- _isDragInProcess = true;
- _labelAnimationController.forward();
- _fadeoutTimer?.cancel();
- });
- }
- void _onVerticalDragUpdate(DragUpdateDetails details) {
- setState(() {
- if (_thumbAnimationController.status != AnimationStatus.forward) {
- _thumbAnimationController.forward();
- }
- if (_isDragInProcess) {
- _barOffset += details.delta.dy;
- if (_barOffset < barMinScrollExtent) {
- _barOffset = barMinScrollExtent;
- }
- if (_barOffset > barMaxScrollExtent) {
- _barOffset = barMaxScrollExtent;
- }
- double viewDelta = getScrollViewDelta(
- details.delta.dy,
- barMaxScrollExtent,
- viewMaxScrollExtent,
- );
- _viewOffset = widget.controller.position.pixels + viewDelta;
- if (_viewOffset < widget.controller.position.minScrollExtent) {
- _viewOffset = widget.controller.position.minScrollExtent;
- }
- if (_viewOffset > viewMaxScrollExtent) {
- _viewOffset = viewMaxScrollExtent;
- }
- widget.controller.jumpTo(_viewOffset);
- }
- });
- }
- void _onVerticalDragEnd(DragEndDetails details) {
- _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () {
- _thumbAnimationController.reverse();
- _labelAnimationController.reverse();
- _fadeoutTimer = null;
- });
- setState(() {
- _isDragInProcess = false;
- });
- }
- }
- /// Draws 2 triangles like arrow up and arrow down
- class ArrowCustomPainter extends CustomPainter {
- Color color;
- ArrowCustomPainter(this.color);
- @override
- bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
- @override
- void paint(Canvas canvas, Size size) {
- final paint = Paint()..color = color;
- const width = 12.0;
- const height = 8.0;
- final baseX = size.width / 2;
- final baseY = size.height / 2;
- canvas.drawPath(
- _trianglePath(Offset(baseX, baseY - 2.0), width, height, true),
- paint,
- );
- canvas.drawPath(
- _trianglePath(Offset(baseX, baseY + 2.0), width, height, false),
- paint,
- );
- }
- static Path _trianglePath(Offset o, double width, double height, bool isUp) {
- return Path()
- ..moveTo(o.dx, o.dy)
- ..lineTo(o.dx + width, o.dy)
- ..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height)
- ..close();
- }
- }
- ///This cut 2 lines in arrow shape
- class ArrowClipper extends CustomClipper<Path> {
- @override
- Path getClip(Size size) {
- Path path = Path();
- path.lineTo(0.0, size.height);
- path.lineTo(size.width, size.height);
- path.lineTo(size.width, 0.0);
- path.lineTo(0.0, 0.0);
- path.close();
- double arrowWidth = 8.0;
- double startPointX = (size.width - arrowWidth) / 2;
- double startPointY = size.height / 2 - arrowWidth / 2;
- path.moveTo(startPointX, startPointY);
- path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
- path.lineTo(startPointX + arrowWidth, startPointY);
- path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
- path.lineTo(
- startPointX + arrowWidth / 2,
- startPointY - arrowWidth / 2 + 1.0,
- );
- path.lineTo(startPointX, startPointY + 1.0);
- path.close();
- startPointY = size.height / 2 + arrowWidth / 2;
- path.moveTo(startPointX + arrowWidth, startPointY);
- path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
- path.lineTo(startPointX, startPointY);
- path.lineTo(startPointX, startPointY - 1.0);
- path.lineTo(
- startPointX + arrowWidth / 2,
- startPointY + arrowWidth / 2 - 1.0,
- );
- path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
- path.close();
- return path;
- }
- @override
- bool shouldReclip(CustomClipper<Path> oldClipper) => false;
- }
- class SlideFadeTransition extends StatelessWidget {
- final Animation<double> animation;
- final Widget child;
- const SlideFadeTransition({
- Key? key,
- required this.animation,
- required this.child,
- }) : super(key: key);
- @override
- Widget build(BuildContext context) {
- return AnimatedBuilder(
- animation: animation,
- builder: (context, child) =>
- animation.value == 0.0 ? const SizedBox() : child!,
- child: SlideTransition(
- position: Tween(
- begin: const Offset(0.3, 0.0),
- end: const Offset(0.0, 0.0),
- ).animate(animation),
- child: FadeTransition(
- opacity: animation,
- child: child,
- ),
- ),
- );
- }
- }
|