|
|
@@ -0,0 +1,383 @@
|
|
|
+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<num> {
|
|
|
+ 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<ScaleLineStyle> scaleLineStyleList;
|
|
|
+ final List<RulerRange> 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<StatefulWidget> createState() {
|
|
|
+ return RulerPickerState();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class RulerPickerState extends State<RulerPicker> {
|
|
|
+ double lastOffset = 0;
|
|
|
+ bool isPosFixed = false;
|
|
|
+ late ScrollController scrollController;
|
|
|
+ Map<int, ScaleLineStyle> _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<ScrollNotification>(
|
|
|
+ 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,
|
|
|
+ });
|
|
|
+}
|