import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; /// a triangle painter class _TrianglePainter extends CustomPainter { // final double lineSize; // _TrianglePainter({this.lineSize = 16}); @override void paint(Canvas canvas, Size size) { Path path = Path(); path.moveTo(0, 0); path.lineTo(size.width, 0); path.lineTo(size.width / 2, tan(pi / 3) * size.width / 2); path.close(); Paint paint = Paint(); paint.color = const Color.fromARGB(255, 118, 165, 248); paint.style = PaintingStyle.fill; canvas.drawPath(path, paint); } @override bool shouldRepaint(CustomPainter oldDelegate) { return false; } } /// The controller for the ruler picker /// init the ruler value from the controller /// 用于 RulerPicker 的控制器,可以在构造函数里初始化默认值 class RulerPickerController extends ValueNotifier { RulerPickerController({num value = 0}) : super(value); num get value => super.value; set value(num newValue) { super.value = newValue; } } typedef void ValueChangedCallback(num value); /// RulerPicker 标尺选择器 /// [width] 必须是具体的值,包括父级container的width,不能是 double.infinity, /// 可以传入MediaQuery.of(context).size.width class RulerPicker extends StatefulWidget { final ValueChangedCallback onValueChanged; final String Function(int index, num rulerScaleValue) onBuildRulerScaleText; final double width; final double height; final TextStyle rulerScaleTextStyle; final List scaleLineStyleList; final List ranges; final Widget? marker; final double rulerMarginTop; final Color rulerBackgroundColor; final RulerPickerController? controller; RulerPicker({ required this.onValueChanged, required this.width, required this.height, required this.onBuildRulerScaleText, this.ranges = const [], this.rulerMarginTop = 0, this.scaleLineStyleList = const [ ScaleLineStyle( scale: 0, color: Color.fromARGB(255, 188, 194, 203), width: 2, height: 32), ScaleLineStyle( color: Color.fromARGB(255, 188, 194, 203), width: 1, height: 20), ], this.rulerScaleTextStyle = const TextStyle( color: Color.fromARGB(255, 188, 194, 203), fontSize: 14, ), this.marker, this.rulerBackgroundColor = Colors.white, this.controller, }); @override State createState() { return RulerPickerState(); } } class RulerPickerState extends State { double lastOffset = 0; bool isPosFixed = false; late ScrollController scrollController; Map _scaleLineStyleMap = {}; int itemCount = 0; // 每个刻度间距 final double _ruleScaleInterval = 15.w; @override void initState() { super.initState(); itemCount = _calculateItemCount(); for (var element in widget.scaleLineStyleList) { _scaleLineStyleMap[element.scale] = element; } double initValueOffset = getPositionByValue(widget.controller?.value ?? 0); scrollController = ScrollController( initialScrollOffset: initValueOffset > 0 ? initValueOffset : 0, ); scrollController.addListener(_onValueChanged); widget.controller?.addListener(() { setPositionByValue(widget.controller?.value ?? 0); }); } int _calculateItemCount() { int itemCount = 0; for (var element in widget.ranges) { itemCount += ((element.end - element.begin) / element.scale).truncate(); } itemCount += 1; return itemCount; } void _onValueChanged() { int currentIndex = scrollController.offset ~/ _ruleScaleInterval.toInt(); if (currentIndex < 0) currentIndex = 0; num currentValue = getRulerScaleValue(currentIndex); var lastConfig = widget.ranges.last; if (currentValue > lastConfig.end) currentValue = lastConfig.end; widget.onValueChanged(currentValue); } void fixOffset() { final double rawOffset = scrollController.offset; final double fixedOffset = (rawOffset / _ruleScaleInterval).round() * _ruleScaleInterval; scrollController.animateTo( fixedOffset, duration: const Duration(milliseconds: 100), curve: Curves.easeOut, ); Future.delayed(const Duration(milliseconds: 120), () { _onValueChanged(); // 确保最终值是准确的 }); } num getRulerScaleValue(int index) { num rulerScaleValue = 0; RulerRange? currentConfig; for (RulerRange config in widget.ranges) { currentConfig = config; if (currentConfig == widget.ranges.last) { break; } var totalCount = ((config.end - config.begin) / config.scale).truncate(); if (index <= totalCount) { break; } else { index -= totalCount; } } rulerScaleValue = index * currentConfig!.scale + currentConfig.begin; return rulerScaleValue; } double getPositionByValue(num value) { double offsetValue = 0; for (RulerRange config in widget.ranges) { if (config.begin <= value && config.end >= value) { offsetValue += ((value - config.begin) / config.scale) * _ruleScaleInterval; break; } else if (value >= config.begin) { var totalCount = ((config.end - config.begin) / config.scale).truncate(); offsetValue += totalCount * _ruleScaleInterval; } } return offsetValue; } void setPositionByValue(num value) { double offsetValue = getPositionByValue(value); scrollController.jumpTo(offsetValue); fixOffset(); } Widget _buildRulerScaleLine(int index) { bool isMajorScale = index % 5 == 0; return Container( width: 8.w, height: isMajorScale ? 48.w : 24.w, decoration: BoxDecoration( borderRadius: BorderRadius.circular(40.w), color: const Color(0xffd0d1d6), ), ); } Widget _buildRulerScale(BuildContext context, int index) { return Container( width: _ruleScaleInterval, child: Stack( clipBehavior: Clip.none, children: [ Align( alignment: Alignment.topCenter, child: _buildRulerScaleLine(index), ), Positioned( bottom: 5, width: 100, left: -50 + _ruleScaleInterval / 2, child: index % 10 == 0 ? Container( alignment: Alignment.center, child: Text( widget.onBuildRulerScaleText( index, getRulerScaleValue(index)), style: widget.rulerScaleTextStyle, ), ) : const SizedBox(), ), ], ), ); } Widget _buildMark() { Widget triangle() { return SizedBox( width: 15, height: 15, child: CustomPaint( painter: _TrianglePainter(), ), ); } return SizedBox( width: _ruleScaleInterval * 2, height: 45, child: Stack( children: [ Align(alignment: Alignment.topCenter, child: triangle()), Align( child: Container( width: 3, height: 34, color: const Color.fromARGB(255, 118, 165, 248), ), ), ], ), ); } bool isRangesChanged(RulerPicker oldWidget) { if (oldWidget.ranges.length != widget.ranges.length) { return true; } for (int i = 0; i < widget.ranges.length; i++) { RulerRange oldRange = oldWidget.ranges[i]; RulerRange range = widget.ranges[i]; if (oldRange.begin != range.begin || oldRange.end != range.end || oldRange.scale != range.scale) { return true; } } return false; } @override void didUpdateWidget(RulerPicker oldWidget) { super.didUpdateWidget(oldWidget); if (mounted && isRangesChanged(oldWidget)) { Future.delayed(Duration.zero, () { setState(() { itemCount = _calculateItemCount(); }); _onValueChanged(); }); } } @override Widget build(BuildContext context) { return Container( width: widget.width, height: widget.height + widget.rulerMarginTop, child: Stack( children: [ Align( alignment: Alignment.bottomCenter, child: Padding(padding: EdgeInsets.only(top: widget.rulerMarginTop),child: Listener( onPointerDown: (_) { FocusScope.of(context).unfocus(); isPosFixed = false; }, child: NotificationListener( onNotification: (scrollNotification) { if (scrollNotification is ScrollStartNotification) { isPosFixed = false; } else if (scrollNotification is ScrollEndNotification) { if (!isPosFixed) { isPosFixed = true; fixOffset(); } } return true; }, child: ListView.builder( padding: EdgeInsets.symmetric( horizontal: (widget.width - _ruleScaleInterval) / 2, ), itemExtent: _ruleScaleInterval, itemCount: itemCount, controller: scrollController, scrollDirection: Axis.horizontal, itemBuilder: _buildRulerScale, ), ), ),) ), Align( alignment: Alignment.topCenter, child: widget.marker ?? _buildMark(), ), ], ), ); } @override void dispose() { scrollController.dispose(); super.dispose(); } } class ScaleLineStyle { final int scale; final Color color; final double width; final double height; const ScaleLineStyle({ this.scale = -1, required this.color, required this.width, required this.height, }); } class RulerRange { final double scale; final int begin; final int end; const RulerRange({ required this.begin, required this.end, this.scale = 1, }); }