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

如何在Flutter中实现该自定义滑块UI设计?

在Flutter中实现自定义环形选择器UI

问题分析

你需要实现的环形UI(参考图:环形选择器UI)包含这些核心特征:

  • 带分段刻度标记的环形轨道
  • 可交互的进度/滑块区域
  • 中间显示当前数值
  • 轨道支持差异化配色

普通circular_slider包仅提供基础环形滑动能力,无法满足这类精细样式定制需求,因此推荐通过自定义绘制或灵活的第三方包方案实现。

解决方案1:完全自定义绘制(推荐)

通过CustomPaint配合GestureDetector,可以完全控制UI细节,适配设计要求。

1. 定义绘制逻辑的CustomPainter

class RingPainter extends CustomPainter {
  final double value; // 0-1范围的当前值
  final double strokeWidth; // 轨道宽度
  final Color trackColor; // 轨道底色
  final Color progressColor; // 进度高亮色
  final int divisions; // 刻度分段数

  RingPainter({
    required this.value,
    required this.strokeWidth,
    required this.trackColor,
    required this.progressColor,
    required this.divisions,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = (size.width - strokeWidth) / 2;

    // 绘制底层轨道
    final trackPaint = Paint()
      ..color = trackColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;
    canvas.drawCircle(center, radius, trackPaint);

    // 绘制进度轨道
    final progressPaint = Paint()
      ..color = progressColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;
    final arcAngle = 2 * pi * value;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -pi / 2, // 从顶部开始绘制
      arcAngle,
      false,
      progressPaint,
    );

    // 绘制刻度标记
    final tickPaint = Paint()
      ..color = Colors.grey[400]!
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;
    for (int i = 0; i < divisions; i++) {
      final angle = -pi / 2 + (2 * pi * i / divisions);
      final start = Offset(
        center.dx + radius * cos(angle),
        center.dy + radius * sin(angle),
      );
      final end = Offset(
        center.dx + (radius - 10) * cos(angle),
        center.dy + (radius - 10) * sin(angle),
      );
      canvas.drawLine(start, end, tickPaint);
    }

    // 绘制中间数值文本
    final textPainter = TextPainter(
      text: TextSpan(
        text: '${(value * 100).toInt()}', // 按需转换数值范围
        style: const TextStyle(
          fontSize: 40,
          fontWeight: FontWeight.bold,
          color: Colors.black,
        ),
      ),
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
    textPainter.layout(minWidth: size.width, maxWidth: size.width);
    textPainter.paint(canvas, Offset(0, center.dy - textPainter.height / 2));
  }

  @override
  bool shouldRepaint(covariant RingPainter oldDelegate) {
    return oldDelegate.value != value;
  }
}

2. 封装可交互的Widget

class RingSelector extends StatefulWidget {
  final double initialValue;
  final int divisions;
  final ValueChanged<double>? onChanged;

  const RingSelector({
    super.key,
    this.initialValue = 0.5,
    this.divisions = 20,
    this.onChanged,
  });

  @override
  State<RingSelector> createState() => _RingSelectorState();
}

class _RingSelectorState extends State<RingSelector> {
  late double _currentValue;

  @override
  void initState() {
    super.initState();
    _currentValue = widget.initialValue.clamp(0.0, 1.0);
  }

  void _handlePanUpdate(DragUpdateDetails details) {
    final renderBox = context.findRenderObject() as RenderBox;
    final localPosition = renderBox.globalToLocal(details.globalPosition);
    final center = Offset(renderBox.size.width / 2, renderBox.size.height / 2);
    
    // 计算触摸位置对应的角度,转换为0-1范围的值
    final angle = atan2(localPosition.dy - center.dy, localPosition.dx - center.dx);
    double value = (angle + pi / 2) / (2 * pi);
    if (value < 0) value += 1;
    
    // 按分段数取整,确保刻度对齐
    final step = 1 / widget.divisions;
    value = (value / step).round() * step;
    
    setState(() => _currentValue = value);
    widget.onChanged?.call(_currentValue);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: _handlePanUpdate,
      child: CustomPaint(
        size: const Size(300, 300),
        painter: RingPainter(
          value: _currentValue,
          strokeWidth: 20,
          trackColor: Colors.grey[200]!,
          progressColor: Colors.blue,
          divisions: widget.divisions,
        ),
      ),
    );
  }
}

3. 使用示例

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: RingSelector(
          initialValue: 0.3,
          divisions: 24,
          onChanged: (value) => print('当前值:${value * 100}'),
        ),
      ),
    );
  }
}

解决方案2:使用第三方包(快速实现)

如果不想完全自定义,可以尝试flutter_circular_slider包,通过参数配置接近设计效果:

CircularSlider(
  min: 0,
  max: 100,
  initialValue: 30,
  trackColor: Colors.grey[200],
  progressColor: Colors.blue,
  thumbColor: Colors.white,
  thumbStrokeColor: Colors.blue,
  divisions: 20,
  onSelectionChange: (value) {
    // 处理数值变化逻辑
  },
)

注:此方式可能需要额外自定义刻度样式,灵活性不如完全自定义。

关键调整点

  • 根据设计稿修改strokeWidth、配色、刻度长度、文字样式等参数
  • 若数值范围不是0-1,修改_handlePanUpdate和RingPainter中的数值转换逻辑
  • 如需显示圆形滑块,可在RingPainter的paint方法中额外绘制滑块

内容的提问来源于stack exchange,提问作者mahedi hasan utshow

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 17:05:24