styled_scroll_bar.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import 'dart:math';
  2. import 'package:flowy_style/mouse_hover_builder.dart';
  3. import 'package:flowy_style/size.dart';
  4. import 'package:flowy_style/theme.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:provider/provider.dart';
  7. import 'package:styled_widget/styled_widget.dart';
  8. class StyledScrollbar extends StatefulWidget {
  9. final double? size;
  10. final Axis axis;
  11. final ScrollController controller;
  12. final Function(double)? onDrag;
  13. final bool showTrack;
  14. final Color? handleColor;
  15. final Color? trackColor;
  16. // ignore: todo
  17. // TODO: Remove contentHeight if we can fix this issue
  18. // https://stackoverflow.com/questions/60855712/flutter-how-to-force-scrollcontroller-to-recalculate-position-maxextents
  19. final double? contentSize;
  20. const StyledScrollbar(
  21. {Key? key,
  22. this.size,
  23. required this.axis,
  24. required this.controller,
  25. this.onDrag,
  26. this.contentSize,
  27. this.showTrack = false,
  28. this.handleColor,
  29. this.trackColor})
  30. : super(key: key);
  31. @override
  32. ScrollbarState createState() => ScrollbarState();
  33. }
  34. class ScrollbarState extends State<StyledScrollbar> {
  35. double _viewExtent = 100;
  36. @override
  37. void initState() {
  38. widget.controller.addListener(() => setState(() {}));
  39. super.initState();
  40. }
  41. @override
  42. void dispose() {
  43. super.dispose();
  44. }
  45. @override
  46. void didUpdateWidget(StyledScrollbar oldWidget) {
  47. if (oldWidget.contentSize != widget.contentSize) setState(() {});
  48. super.didUpdateWidget(oldWidget);
  49. }
  50. // void calculateSize() {
  51. // //[SB] Only hack I can find to make the ScrollController update it's maxExtents.
  52. // //Call this whenever the content changes, so the scrollbar can recalculate it's size
  53. // widget.controller.jumpTo(widget.controller.position.pixels + 1);
  54. // Future.microtask(() => widget.controller
  55. // .animateTo(widget.controller.position.pixels - 1, duration: 100.milliseconds, curve: Curves.linear));
  56. // }
  57. @override
  58. Widget build(BuildContext context) {
  59. final theme = context.watch<AppTheme>();
  60. return LayoutBuilder(
  61. builder: (_, BoxConstraints constraints) {
  62. double maxExtent;
  63. final contentSize = widget.contentSize;
  64. switch (widget.axis) {
  65. case Axis.vertical:
  66. // Use supplied contentSize if we have it, otherwise just fallback to maxScrollExtents
  67. if (contentSize != null && contentSize > 0) {
  68. maxExtent = contentSize - constraints.maxHeight;
  69. } else {
  70. maxExtent = widget.controller.position.maxScrollExtent;
  71. }
  72. _viewExtent = constraints.maxHeight;
  73. break;
  74. case Axis.horizontal:
  75. // Use supplied contentSize if we have it, otherwise just fallback to maxScrollExtents
  76. if (contentSize != null && contentSize > 0) {
  77. maxExtent = contentSize - constraints.maxWidth;
  78. } else {
  79. maxExtent = widget.controller.position.maxScrollExtent;
  80. }
  81. _viewExtent = constraints.maxWidth;
  82. break;
  83. }
  84. final contentExtent = maxExtent + _viewExtent;
  85. // Calculate the alignment for the handle, this is a value between 0 and 1,
  86. // it automatically takes the handle size into acct
  87. // ignore: omit_local_variable_types
  88. double handleAlignment =
  89. maxExtent == 0 ? 0 : widget.controller.offset / maxExtent;
  90. // Convert handle alignment from [0, 1] to [-1, 1]
  91. handleAlignment *= 2.0;
  92. handleAlignment -= 1.0;
  93. // Calculate handleSize by comparing the total content size to our viewport
  94. var handleExtent = _viewExtent;
  95. if (contentExtent > _viewExtent) {
  96. //Make sure handle is never small than the minSize
  97. handleExtent = max(60, _viewExtent * _viewExtent / contentExtent);
  98. }
  99. // Hide the handle if content is < the viewExtent
  100. var showHandle = contentExtent > _viewExtent && contentExtent > 0;
  101. // Handle color
  102. var handleColor = widget.handleColor ??
  103. (theme.isDark ? theme.greyWeak.withOpacity(.2) : theme.greyWeak);
  104. // Track color
  105. var trackColor = widget.trackColor ??
  106. (theme.isDark
  107. ? theme.greyWeak.withOpacity(.1)
  108. : theme.greyWeak.withOpacity(.3));
  109. //Layout the stack, it just contains a child, and
  110. return Stack(children: <Widget>[
  111. /// TRACK, thin strip, aligned along the end of the parent
  112. if (widget.showTrack)
  113. Align(
  114. alignment: const Alignment(1, 1),
  115. child: Container(
  116. color: trackColor,
  117. width: widget.axis == Axis.vertical
  118. ? widget.size
  119. : double.infinity,
  120. height: widget.axis == Axis.horizontal
  121. ? widget.size
  122. : double.infinity,
  123. ),
  124. ),
  125. /// HANDLE - Clickable shape that changes scrollController when dragged
  126. Align(
  127. // Use calculated alignment to position handle from -1 to 1, let Alignment do the rest of the work
  128. alignment: Alignment(
  129. widget.axis == Axis.vertical ? 1 : handleAlignment,
  130. widget.axis == Axis.horizontal ? 1 : handleAlignment,
  131. ),
  132. child: GestureDetector(
  133. onVerticalDragUpdate: _handleVerticalDrag,
  134. onHorizontalDragUpdate: _handleHorizontalDrag,
  135. // HANDLE SHAPE
  136. child: MouseHoverBuilder(
  137. builder: (_, isHovered) => Container(
  138. width:
  139. widget.axis == Axis.vertical ? widget.size : handleExtent,
  140. height: widget.axis == Axis.horizontal
  141. ? widget.size
  142. : handleExtent,
  143. decoration: BoxDecoration(
  144. color: handleColor.withOpacity(isHovered ? 1 : .85),
  145. borderRadius: Corners.s3Border),
  146. ),
  147. ),
  148. ),
  149. )
  150. ]).opacity(showHandle ? 1.0 : 0.0, animate: false);
  151. },
  152. );
  153. }
  154. void _handleHorizontalDrag(DragUpdateDetails details) {
  155. var pos = widget.controller.offset;
  156. var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) /
  157. _viewExtent;
  158. widget.controller.jumpTo((pos + details.delta.dx * pxRatio)
  159. .clamp(0.0, widget.controller.position.maxScrollExtent));
  160. widget.onDrag?.call(details.delta.dx);
  161. }
  162. void _handleVerticalDrag(DragUpdateDetails details) {
  163. var pos = widget.controller.offset;
  164. var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) /
  165. _viewExtent;
  166. widget.controller.jumpTo((pos + details.delta.dy * pxRatio)
  167. .clamp(0.0, widget.controller.position.maxScrollExtent));
  168. widget.onDrag?.call(details.delta.dy);
  169. }
  170. }
  171. class ScrollbarListStack extends StatelessWidget {
  172. final double barSize;
  173. final Axis axis;
  174. final Widget child;
  175. final ScrollController controller;
  176. final double? contentSize;
  177. final EdgeInsets? scrollbarPadding;
  178. final Color? handleColor;
  179. final Color? trackColor;
  180. const ScrollbarListStack(
  181. {Key? key,
  182. required this.barSize,
  183. required this.axis,
  184. required this.child,
  185. required this.controller,
  186. this.contentSize,
  187. this.scrollbarPadding,
  188. this.handleColor,
  189. this.trackColor})
  190. : super(key: key);
  191. @override
  192. Widget build(BuildContext context) {
  193. return Stack(
  194. children: <Widget>[
  195. /// LIST
  196. /// Wrap with a bit of padding on the right
  197. child.padding(
  198. right: axis == Axis.vertical ? barSize + Insets.sm : 0,
  199. bottom: axis == Axis.horizontal ? barSize + Insets.sm : 0,
  200. ),
  201. /// SCROLLBAR
  202. Padding(
  203. padding: scrollbarPadding ?? EdgeInsets.zero,
  204. child: StyledScrollbar(
  205. size: barSize,
  206. axis: axis,
  207. controller: controller,
  208. contentSize: contentSize,
  209. trackColor: trackColor,
  210. handleColor: handleColor,
  211. showTrack: true,
  212. ),
  213. ),
  214. ],
  215. );
  216. }
  217. }