如何在Flutter中基于CustomPainter实现自定义弧形滑块?
自定义任意弧形滑块实现方案(基于CustomPainter)
核心思路
要实现任意角度范围的弧形滑块,核心是通过三角函数计算滑块位置,结合CustomPainter绘制轨迹和滑块,再通过手势监听实现拖动交互。
关键数学逻辑
Flutter坐标系以左上角为原点,y轴向下为正,角度计算使用弧度:
- 弧形轨迹由中心坐标、半径、起始弧度、扫过弧度定义
- 滑块位置计算:已知当前角度
θ,滑块坐标为:x = centerX + radius * cos(θ) y = centerY + radius * sin(θ) - 拖动时通过
atan2(dy, dx)计算触摸点相对于中心的弧度,再映射到滑块的取值范围
完整实现代码
自定义弧形滑块组件
import 'dart:math'; import 'package:flutter/material.dart'; class ArcSlider extends StatefulWidget { final double min; final double max; final double initialValue; final double startAngle; // 弧形起始弧度(如pi/2对应顶部) final double sweepAngle; // 弧形扫过的弧度(正为顺时针,负为逆时针) final double radius; final ValueChanged<double>? onChanged; const ArcSlider({ super.key, this.min = 0, this.max = 100, this.initialValue = 0, required this.startAngle, required this.sweepAngle, required this.radius, this.onChanged, }) : assert(sweepAngle.abs() <= 2 * pi, "扫过角度不能超过整圆"); @override State<ArcSlider> createState() => _ArcSliderState(); } class _ArcSliderState extends State<ArcSlider> { late double _currentValue; @override void initState() { super.initState(); _currentValue = widget.initialValue.clamp(widget.min, widget.max); } // 将数值转换为对应弧度 double _valueToAngle(double value) { final ratio = (value - widget.min) / (widget.max - widget.min); return widget.startAngle + ratio * widget.sweepAngle; } // 将弧度转换为对应数值 double _angleToValue(double angle) { // 限制角度在弧形范围内 final clampedAngle = angle.clamp( widget.startAngle, widget.startAngle + widget.sweepAngle, ); final ratio = (clampedAngle - widget.startAngle) / widget.sweepAngle; return widget.min + ratio * (widget.max - widget.min); } // 计算触摸点相对于中心的弧度 double _getTouchAngle(Offset touchPoint, Size size) { final center = Offset(size.width / 2, size.height / 2); final dx = touchPoint.dx - center.dx; final dy = touchPoint.dy - center.dy; var angle = atan2(dy, dx); // 范围:-π ~ π // 转换为0 ~ 2π范围方便计算 if (angle < 0) angle += 2 * pi; return angle; } @override Widget build(BuildContext context) { return GestureDetector( onPanUpdate: (details) { final angle = _getTouchAngle(details.localPosition, context.size!); final newValue = _angleToValue(angle); setState(() => _currentValue = newValue); widget.onChanged?.call(newValue); }, child: CustomPaint( size: Size(widget.radius * 2, widget.radius * 2), painter: _ArcSliderPainter( startAngle: widget.startAngle, sweepAngle: widget.sweepAngle, currentAngle: _valueToAngle(_currentValue), radius: widget.radius, ), ), ); } } // 负责绘制弧形轨迹和滑块的Painter class _ArcSliderPainter extends CustomPainter { final double startAngle; final double sweepAngle; final double currentAngle; final double radius; _ArcSliderPainter({ required this.startAngle, required this.sweepAngle, required this.currentAngle, required this.radius, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final rect = Rect.fromCircle(center: center, radius: radius); // 绘制背景弧形 final backgroundPaint = Paint() ..color = Colors.grey.shade300 ..style = PaintingStyle.stroke ..strokeWidth = 8 ..strokeCap = StrokeCap.round; canvas.drawArc(rect, startAngle, sweepAngle, false, backgroundPaint); // 绘制已选中的进度弧形 final progressPaint = Paint() ..color = Colors.blue ..style = PaintingStyle.stroke ..strokeWidth = 8 ..strokeCap = StrokeCap.round; final progressSweep = currentAngle - startAngle; canvas.drawArc(rect, startAngle, progressSweep, false, progressPaint); // 绘制滑块 final sliderCenter = Offset( center.dx + radius * cos(currentAngle), center.dy + radius * sin(currentAngle), ); // 滑块填充色 canvas.drawCircle(sliderCenter, 10, Paint()..color = Colors.white); // 滑块边框 canvas.drawCircle( sliderCenter, 10, Paint() ..color = Colors.blue ..style = PaintingStyle.stroke ..strokeWidth = 2, ); } @override bool shouldRepaint(covariant _ArcSliderPainter oldDelegate) { return currentAngle != oldDelegate.currentAngle; } }
使用示例
ArcSlider( startAngle: pi / 3, // 起始角度:60度 sweepAngle: 4 * pi / 3, // 扫过角度:240度 radius: 120, initialValue: 50, onChanged: (value) { print("当前滑块值:$value"); }, )
自定义扩展点
- 修改
Paint参数可以自定义弧形宽度、颜色、滑块样式 - 将
sweepAngle设为负数可实现逆时针弧形 - 增加
onChangeStart/onChangeEnd回调处理拖动开始/结束事件
内容的提问来源于stack exchange,提问作者Navid Shokoufeh
相关产品推荐
相关产品推荐

