You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中基于CustomPainter实现自定义弧形滑块?

自定义任意弧形滑块实现方案(基于CustomPainter)

核心思路

要实现任意角度范围的弧形滑块,核心是通过三角函数计算滑块位置,结合CustomPainter绘制轨迹和滑块,再通过手势监听实现拖动交互。

关键数学逻辑

Flutter坐标系以左上角为原点,y轴向下为正,角度计算使用弧度:

  1. 弧形轨迹由中心坐标、半径、起始弧度、扫过弧度定义
  2. 滑块位置计算:已知当前角度θ,滑块坐标为:
    x = centerX + radius * cos(θ)
    y = centerY + radius * sin(θ)
    
  3. 拖动时通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 22:33:32