draggable_scrollbar.dart 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import 'dart:async';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:photos/ui/huge_listview/scroll_bar_thumb.dart';
  6. class DraggableScrollbar extends StatefulWidget {
  7. final Widget child;
  8. final Color backgroundColor;
  9. final Color drawColor;
  10. final double heightScrollThumb;
  11. final EdgeInsetsGeometry? padding;
  12. final int totalCount;
  13. final int initialScrollIndex;
  14. final double bottomSafeArea;
  15. final int currentFirstIndex;
  16. final ValueChanged<double>? onChange;
  17. final String Function(int) labelTextBuilder;
  18. final bool isEnabled;
  19. const DraggableScrollbar({
  20. Key? key,
  21. required this.child,
  22. this.backgroundColor = Colors.white,
  23. this.drawColor = Colors.grey,
  24. this.heightScrollThumb = 80.0,
  25. this.bottomSafeArea = 120,
  26. this.padding,
  27. this.totalCount = 1,
  28. this.initialScrollIndex = 0,
  29. this.currentFirstIndex = 0,
  30. required this.labelTextBuilder,
  31. this.onChange,
  32. this.isEnabled = true,
  33. }) : super(key: key);
  34. @override
  35. DraggableScrollbarState createState() => DraggableScrollbarState();
  36. }
  37. class DraggableScrollbarState extends State<DraggableScrollbar>
  38. with TickerProviderStateMixin {
  39. static const thumbAnimationDuration = Duration(milliseconds: 1000);
  40. static const labelAnimationDuration = Duration(milliseconds: 1000);
  41. double thumbOffset = 0.0;
  42. bool isDragging = false;
  43. late int currentFirstIndex;
  44. double get thumbMin => 0.0;
  45. double get thumbMax =>
  46. context.size!.height - widget.heightScrollThumb - widget.bottomSafeArea;
  47. late AnimationController _thumbAnimationController;
  48. Animation<double>? _thumbAnimation;
  49. late AnimationController _labelAnimationController;
  50. Animation<double>? _labelAnimation;
  51. Timer? _fadeoutTimer;
  52. @override
  53. void initState() {
  54. super.initState();
  55. currentFirstIndex = widget.currentFirstIndex;
  56. if (widget.initialScrollIndex > 0 && widget.totalCount > 1) {
  57. WidgetsBinding.instance.addPostFrameCallback((_) {
  58. setState(
  59. () => thumbOffset = (widget.initialScrollIndex / widget.totalCount) *
  60. (thumbMax - thumbMin),
  61. );
  62. });
  63. }
  64. _thumbAnimationController = AnimationController(
  65. vsync: this,
  66. duration: thumbAnimationDuration,
  67. );
  68. _thumbAnimation = CurvedAnimation(
  69. parent: _thumbAnimationController,
  70. curve: Curves.fastOutSlowIn,
  71. );
  72. _labelAnimationController = AnimationController(
  73. vsync: this,
  74. duration: labelAnimationDuration,
  75. );
  76. _labelAnimation = CurvedAnimation(
  77. parent: _labelAnimationController,
  78. curve: Curves.fastOutSlowIn,
  79. );
  80. }
  81. @override
  82. void dispose() {
  83. _thumbAnimationController.dispose();
  84. _labelAnimationController.dispose();
  85. _fadeoutTimer?.cancel();
  86. super.dispose();
  87. }
  88. @override
  89. Widget build(BuildContext context) {
  90. if (widget.isEnabled) {
  91. return Stack(
  92. children: [
  93. RepaintBoundary(child: widget.child),
  94. RepaintBoundary(child: buildThumb()),
  95. ],
  96. );
  97. } else {
  98. return widget.child;
  99. }
  100. }
  101. Widget buildKeyboard() {
  102. if (defaultTargetPlatform == TargetPlatform.windows) {
  103. return RawKeyboardListener(
  104. focusNode: FocusNode(),
  105. onKey: keyHandler,
  106. child: buildThumb(),
  107. );
  108. } else {
  109. return buildThumb();
  110. }
  111. }
  112. Widget buildThumb() => Padding(
  113. padding: widget.padding!,
  114. child: Container(
  115. alignment: Alignment.topRight,
  116. margin: EdgeInsets.only(top: thumbOffset),
  117. child: ScrollBarThumb(
  118. widget.backgroundColor,
  119. widget.drawColor,
  120. widget.heightScrollThumb,
  121. widget.labelTextBuilder.call(currentFirstIndex),
  122. _labelAnimation,
  123. _thumbAnimation,
  124. onDragStart,
  125. onDragUpdate,
  126. onDragEnd,
  127. ),
  128. ),
  129. );
  130. void setPosition(double position, int currentFirstIndex) {
  131. setState(() {
  132. this.currentFirstIndex = currentFirstIndex;
  133. thumbOffset = position * (thumbMax - thumbMin);
  134. if (_thumbAnimationController.status != AnimationStatus.forward) {
  135. _thumbAnimationController.forward();
  136. }
  137. _fadeoutTimer?.cancel();
  138. _fadeoutTimer = Timer(thumbAnimationDuration, () {
  139. _thumbAnimationController.reverse();
  140. _labelAnimationController.reverse();
  141. _fadeoutTimer = null;
  142. });
  143. });
  144. }
  145. void onDragStart(DragStartDetails details) {
  146. setState(() {
  147. isDragging = true;
  148. _labelAnimationController.forward();
  149. _fadeoutTimer?.cancel();
  150. });
  151. }
  152. void onDragUpdate(DragUpdateDetails details) {
  153. setState(() {
  154. if (_thumbAnimationController.status != AnimationStatus.forward) {
  155. _thumbAnimationController.forward();
  156. }
  157. if (isDragging && details.delta.dy != 0) {
  158. thumbOffset += details.delta.dy;
  159. thumbOffset = thumbOffset.clamp(thumbMin, thumbMax);
  160. final double position = thumbOffset / (thumbMax - thumbMin);
  161. widget.onChange?.call(position);
  162. }
  163. });
  164. }
  165. void onDragEnd(DragEndDetails details) {
  166. _fadeoutTimer = Timer(thumbAnimationDuration, () {
  167. _thumbAnimationController.reverse();
  168. _labelAnimationController.reverse();
  169. _fadeoutTimer = null;
  170. });
  171. setState(() => isDragging = false);
  172. }
  173. void keyHandler(RawKeyEvent value) {
  174. if (value.runtimeType == RawKeyDownEvent) {
  175. if (value.logicalKey == LogicalKeyboardKey.arrowDown) {
  176. onDragUpdate(
  177. DragUpdateDetails(
  178. globalPosition: Offset.zero,
  179. delta: const Offset(0, 2),
  180. ),
  181. );
  182. } else if (value.logicalKey == LogicalKeyboardKey.arrowUp) {
  183. onDragUpdate(
  184. DragUpdateDetails(
  185. globalPosition: Offset.zero,
  186. delta: const Offset(0, -2),
  187. ),
  188. );
  189. } else if (value.logicalKey == LogicalKeyboardKey.pageDown) {
  190. onDragUpdate(
  191. DragUpdateDetails(
  192. globalPosition: Offset.zero,
  193. delta: const Offset(0, 25),
  194. ),
  195. );
  196. } else if (value.logicalKey == LogicalKeyboardKey.pageUp) {
  197. onDragUpdate(
  198. DragUpdateDetails(
  199. globalPosition: Offset.zero,
  200. delta: const Offset(0, -25),
  201. ),
  202. );
  203. }
  204. }
  205. }
  206. }