flutter_ruler_picker.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import 'dart:math';
  2. import 'package:flutter/cupertino.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/widgets.dart';
  5. import 'package:flutter_screenutil/flutter_screenutil.dart';
  6. /// a triangle painter
  7. class _TrianglePainter extends CustomPainter {
  8. // final double lineSize;
  9. // _TrianglePainter({this.lineSize = 16});
  10. @override
  11. void paint(Canvas canvas, Size size) {
  12. Path path = Path();
  13. path.moveTo(0, 0);
  14. path.lineTo(size.width, 0);
  15. path.lineTo(size.width / 2, tan(pi / 3) * size.width / 2);
  16. path.close();
  17. Paint paint = Paint();
  18. paint.color = const Color.fromARGB(255, 118, 165, 248);
  19. paint.style = PaintingStyle.fill;
  20. canvas.drawPath(path, paint);
  21. }
  22. @override
  23. bool shouldRepaint(CustomPainter oldDelegate) {
  24. return false;
  25. }
  26. }
  27. /// The controller for the ruler picker
  28. /// init the ruler value from the controller
  29. /// 用于 RulerPicker 的控制器,可以在构造函数里初始化默认值
  30. class RulerPickerController extends ValueNotifier<num> {
  31. RulerPickerController({num value = 0}) : super(value);
  32. num get value => super.value;
  33. set value(num newValue) {
  34. super.value = newValue;
  35. }
  36. }
  37. typedef void ValueChangedCallback(num value);
  38. /// RulerPicker 标尺选择器
  39. /// [width] 必须是具体的值,包括父级container的width,不能是 double.infinity,
  40. /// 可以传入MediaQuery.of(context).size.width
  41. class RulerPicker extends StatefulWidget {
  42. final ValueChangedCallback onValueChanged;
  43. final String Function(int index, num rulerScaleValue) onBuildRulerScaleText;
  44. final double width;
  45. final double height;
  46. final TextStyle rulerScaleTextStyle;
  47. final List<ScaleLineStyle> scaleLineStyleList;
  48. final List<RulerRange> ranges;
  49. final Widget? marker;
  50. final double rulerMarginTop;
  51. final Color rulerBackgroundColor;
  52. final RulerPickerController? controller;
  53. RulerPicker({
  54. required this.onValueChanged,
  55. required this.width,
  56. required this.height,
  57. required this.onBuildRulerScaleText,
  58. this.ranges = const [],
  59. this.rulerMarginTop = 0,
  60. this.scaleLineStyleList = const [
  61. ScaleLineStyle(
  62. scale: 0,
  63. color: Color.fromARGB(255, 188, 194, 203),
  64. width: 2,
  65. height: 32),
  66. ScaleLineStyle(
  67. color: Color.fromARGB(255, 188, 194, 203), width: 1, height: 20),
  68. ],
  69. this.rulerScaleTextStyle = const TextStyle(
  70. color: Color.fromARGB(255, 188, 194, 203),
  71. fontSize: 14,
  72. ),
  73. this.marker,
  74. this.rulerBackgroundColor = Colors.white,
  75. this.controller,
  76. });
  77. @override
  78. State<StatefulWidget> createState() {
  79. return RulerPickerState();
  80. }
  81. }
  82. class RulerPickerState extends State<RulerPicker> {
  83. double lastOffset = 0;
  84. bool isPosFixed = false;
  85. late ScrollController scrollController;
  86. Map<int, ScaleLineStyle> _scaleLineStyleMap = {};
  87. int itemCount = 0;
  88. // 每个刻度间距
  89. final double _ruleScaleInterval = 15.w;
  90. @override
  91. void initState() {
  92. super.initState();
  93. itemCount = _calculateItemCount();
  94. for (var element in widget.scaleLineStyleList) {
  95. _scaleLineStyleMap[element.scale] = element;
  96. }
  97. double initValueOffset = getPositionByValue(widget.controller?.value ?? 0);
  98. scrollController = ScrollController(
  99. initialScrollOffset: initValueOffset > 0 ? initValueOffset : 0,
  100. );
  101. scrollController.addListener(_onValueChanged);
  102. widget.controller?.addListener(() {
  103. setPositionByValue(widget.controller?.value ?? 0);
  104. });
  105. }
  106. int _calculateItemCount() {
  107. int itemCount = 0;
  108. for (var element in widget.ranges) {
  109. itemCount += ((element.end - element.begin) / element.scale).truncate();
  110. }
  111. itemCount += 1;
  112. return itemCount;
  113. }
  114. void _onValueChanged() {
  115. int currentIndex = scrollController.offset ~/ _ruleScaleInterval.toInt();
  116. if (currentIndex < 0) currentIndex = 0;
  117. num currentValue = getRulerScaleValue(currentIndex);
  118. var lastConfig = widget.ranges.last;
  119. if (currentValue > lastConfig.end) currentValue = lastConfig.end;
  120. widget.onValueChanged(currentValue);
  121. }
  122. void fixOffset() {
  123. final double rawOffset = scrollController.offset;
  124. final double fixedOffset =
  125. (rawOffset / _ruleScaleInterval).round() * _ruleScaleInterval;
  126. scrollController.animateTo(
  127. fixedOffset,
  128. duration: const Duration(milliseconds: 100),
  129. curve: Curves.easeOut,
  130. );
  131. Future.delayed(const Duration(milliseconds: 120), () {
  132. _onValueChanged(); // 确保最终值是准确的
  133. });
  134. }
  135. num getRulerScaleValue(int index) {
  136. num rulerScaleValue = 0;
  137. RulerRange? currentConfig;
  138. for (RulerRange config in widget.ranges) {
  139. currentConfig = config;
  140. if (currentConfig == widget.ranges.last) {
  141. break;
  142. }
  143. var totalCount =
  144. ((config.end - config.begin) / config.scale).truncate();
  145. if (index <= totalCount) {
  146. break;
  147. } else {
  148. index -= totalCount;
  149. }
  150. }
  151. rulerScaleValue = index * currentConfig!.scale + currentConfig.begin;
  152. return rulerScaleValue;
  153. }
  154. double getPositionByValue(num value) {
  155. double offsetValue = 0;
  156. for (RulerRange config in widget.ranges) {
  157. if (config.begin <= value && config.end >= value) {
  158. offsetValue +=
  159. ((value - config.begin) / config.scale) * _ruleScaleInterval;
  160. break;
  161. } else if (value >= config.begin) {
  162. var totalCount =
  163. ((config.end - config.begin) / config.scale).truncate();
  164. offsetValue += totalCount * _ruleScaleInterval;
  165. }
  166. }
  167. return offsetValue;
  168. }
  169. void setPositionByValue(num value) {
  170. double offsetValue = getPositionByValue(value);
  171. scrollController.jumpTo(offsetValue);
  172. fixOffset();
  173. }
  174. Widget _buildRulerScaleLine(int index) {
  175. bool isMajorScale = index % 5 == 0;
  176. return Container(
  177. width: 8.w,
  178. height: isMajorScale ? 48.w : 24.w,
  179. decoration: BoxDecoration(
  180. borderRadius: BorderRadius.circular(40.w),
  181. color: const Color(0xffd0d1d6),
  182. ),
  183. );
  184. }
  185. Widget _buildRulerScale(BuildContext context, int index) {
  186. return Container(
  187. width: _ruleScaleInterval,
  188. child: Stack(
  189. clipBehavior: Clip.none,
  190. children: [
  191. Align(
  192. alignment: Alignment.topCenter,
  193. child: _buildRulerScaleLine(index),
  194. ),
  195. Positioned(
  196. bottom: 5,
  197. width: 100,
  198. left: -50 + _ruleScaleInterval / 2,
  199. child: index % 10 == 0
  200. ? Container(
  201. alignment: Alignment.center,
  202. child: Text(
  203. widget.onBuildRulerScaleText(
  204. index, getRulerScaleValue(index)),
  205. style: widget.rulerScaleTextStyle,
  206. ),
  207. )
  208. : const SizedBox(),
  209. ),
  210. ],
  211. ),
  212. );
  213. }
  214. Widget _buildMark() {
  215. Widget triangle() {
  216. return SizedBox(
  217. width: 15,
  218. height: 15,
  219. child: CustomPaint(
  220. painter: _TrianglePainter(),
  221. ),
  222. );
  223. }
  224. return SizedBox(
  225. width: _ruleScaleInterval * 2,
  226. height: 45,
  227. child: Stack(
  228. children: [
  229. Align(alignment: Alignment.topCenter, child: triangle()),
  230. Align(
  231. child: Container(
  232. width: 3,
  233. height: 34,
  234. color: const Color.fromARGB(255, 118, 165, 248),
  235. ),
  236. ),
  237. ],
  238. ),
  239. );
  240. }
  241. bool isRangesChanged(RulerPicker oldWidget) {
  242. if (oldWidget.ranges.length != widget.ranges.length) {
  243. return true;
  244. }
  245. for (int i = 0; i < widget.ranges.length; i++) {
  246. RulerRange oldRange = oldWidget.ranges[i];
  247. RulerRange range = widget.ranges[i];
  248. if (oldRange.begin != range.begin ||
  249. oldRange.end != range.end ||
  250. oldRange.scale != range.scale) {
  251. return true;
  252. }
  253. }
  254. return false;
  255. }
  256. @override
  257. void didUpdateWidget(RulerPicker oldWidget) {
  258. super.didUpdateWidget(oldWidget);
  259. if (mounted && isRangesChanged(oldWidget)) {
  260. Future.delayed(Duration.zero, () {
  261. setState(() {
  262. itemCount = _calculateItemCount();
  263. });
  264. _onValueChanged();
  265. });
  266. }
  267. }
  268. @override
  269. Widget build(BuildContext context) {
  270. return Container(
  271. width: widget.width,
  272. height: widget.height + widget.rulerMarginTop,
  273. child: Stack(
  274. children: [
  275. Align(
  276. alignment: Alignment.bottomCenter,
  277. child: Padding(padding: EdgeInsets.only(top: widget.rulerMarginTop),child: Listener(
  278. onPointerDown: (_) {
  279. FocusScope.of(context).unfocus();
  280. isPosFixed = false;
  281. },
  282. child: NotificationListener<ScrollNotification>(
  283. onNotification: (scrollNotification) {
  284. if (scrollNotification is ScrollStartNotification) {
  285. isPosFixed = false;
  286. } else if (scrollNotification is ScrollEndNotification) {
  287. if (!isPosFixed) {
  288. isPosFixed = true;
  289. fixOffset();
  290. }
  291. }
  292. return true;
  293. },
  294. child: ListView.builder(
  295. padding: EdgeInsets.symmetric(
  296. horizontal: (widget.width - _ruleScaleInterval) / 2,
  297. ),
  298. itemExtent: _ruleScaleInterval,
  299. itemCount: itemCount,
  300. controller: scrollController,
  301. scrollDirection: Axis.horizontal,
  302. itemBuilder: _buildRulerScale,
  303. ),
  304. ),
  305. ),)
  306. ),
  307. Align(
  308. alignment: Alignment.topCenter,
  309. child: widget.marker ?? _buildMark(),
  310. ),
  311. ],
  312. ),
  313. );
  314. }
  315. @override
  316. void dispose() {
  317. scrollController.dispose();
  318. super.dispose();
  319. }
  320. }
  321. class ScaleLineStyle {
  322. final int scale;
  323. final Color color;
  324. final double width;
  325. final double height;
  326. const ScaleLineStyle({
  327. this.scale = -1,
  328. required this.color,
  329. required this.width,
  330. required this.height,
  331. });
  332. }
  333. class RulerRange {
  334. final double scale;
  335. final int begin;
  336. final int end;
  337. const RulerRange({
  338. required this.begin,
  339. required this.end,
  340. this.scale = 1,
  341. });
  342. }