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

如何在Flutter中实现弧形滑块式速度计UI及体重选择弧形控件?

Flutter弧形体重选择控件与速度计UI实现方案

一、弧形体重选择控件实现

核心思路

用CustomPainter绘制弧形选择轨道,通过GestureDetector监听拖动手势计算选中的体重值,同步更新显示文本。

完整代码示例

import 'package:flutter/material.dart';

class ArcWeightPicker extends StatefulWidget {
  const ArcWeightPicker({super.key});

  @override
  State<ArcWeightPicker> createState() => _ArcWeightPickerState();
}

class _ArcWeightPickerState extends State<ArcWeightPicker> {
  // 体重范围:30kg - 150kg
  final double minWeight = 30;
  final double maxWeight = 150;
  // 当前选中体重
  double currentWeight = 60;
  // 弧形角度范围:从120度到240度(下半圆)
  final double startAngle = 120 * (3.1415926 / 180);
  final double sweepAngle = 120 * (3.1415926 / 180);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // 显示选中体重
        Text(
          '${currentWeight.toStringAsFixed(0)}kg',
          style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 20),
        // 弧形选择器
        SizedBox(
          width: 300,
          height: 150,
          child: GestureDetector(
            onPanUpdate: (details) {
              setState(() {
                // 获取触摸点相对于控件中心的坐标
                RenderBox box = context.findRenderObject() as RenderBox;
                Offset localPos = box.globalToLocal(details.globalPosition);
                Offset center = Offset(150, 150);
                // 计算触摸点对应的角度
                double angle =
                    (localPos - center).direction - startAngle;
                // 限制角度在0到sweepAngle之间
                angle = angle.clamp(0, sweepAngle);
                // 转换为体重值
                currentWeight = minWeight + (maxWeight - minWeight) * (angle / sweepAngle);
              });
            },
            child: CustomPaint(
              painter: _WeightArcPainter(
                startAngle: startAngle,
                sweepAngle: sweepAngle,
                currentAngle: startAngle + (currentWeight - minWeight)/(maxWeight - minWeight)*sweepAngle,
              ),
            ),
          ),
        ),
      ],
    );
  }
}

class _WeightArcPainter extends CustomPainter {
  final double startAngle;
  final double sweepAngle;
  final double currentAngle;

  _WeightArcPainter({
    required this.startAngle,
    required this.sweepAngle,
    required this.currentAngle,
  });

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

    // 绘制背景弧形轨道
    final backgroundPaint = Paint()
      ..color = Colors.grey[300]!
      ..style = PaintingStyle.stroke
      ..strokeWidth = 20;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      startAngle,
      sweepAngle,
      false,
      backgroundPaint,
    );

    // 绘制选中的弧形进度
    final progressPaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 20
      ..strokeCap = StrokeCap.round;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      startAngle,
      currentAngle - startAngle,
      false,
      progressPaint,
    );

    // 绘制选中指针
    final pointerPaint = Paint()..color = Colors.blue;
    canvas.drawCircle(
      Offset(
        center.dx + radius * cos(currentAngle),
        center.dy + radius * sin(currentAngle),
      ),
      10,
      pointerPaint,
    );
  }

  @override
  bool shouldRepaint(covariant _WeightArcPainter oldDelegate) {
    return currentAngle != oldDelegate.currentAngle;
  }
}

使用方式

直接将ArcWeightPicker作为子控件添加到页面中即可,拖动弧形区域就能实时更新显示的体重值。


二、弧形滑块式速度计UI实现

核心思路

基于CustomPainter绘制刻度弧形轨道、刻度值和指针,通过绑定数值控制指针旋转角度,模拟速度计效果。

完整代码示例

import 'package:flutter/material.dart';

class Speedometer extends StatefulWidget {
  const Speedometer({super.key});

  @override
  State<Speedometer> createState() => _SpeedometerState();
}

class _SpeedometerState extends State<Speedometer> {
  // 速度范围:0-180km/h
  double currentSpeed = 60;
  final double minSpeed = 0;
  final double maxSpeed = 180;
  // 弧形角度范围:从150度到30度(左半部分到右半部分的弧形)
  final double startAngle = 150 * (3.1415926 / 180);
  final double sweepAngle = 240 * (3.1415926 / 180);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 300,
      height: 200,
      child: Column(
        children: [
          Text(
            '${currentSpeed.toStringAsFixed(0)} km/h',
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          Expanded(
            child: CustomPaint(
              painter: _SpeedometerPainter(
                startAngle: startAngle,
                sweepAngle: sweepAngle,
                currentSpeed: currentSpeed,
                minSpeed: minSpeed,
                maxSpeed: maxSpeed,
              ),
            ),
          ),
          // 调试用滑块,控制速度值
          Slider(
            min: minSpeed,
            max: maxSpeed,
            value: currentSpeed,
            onChanged: (value) {
              setState(() {
                currentSpeed = value;
              });
            },
          ),
        ],
      ),
    );
  }
}

class _SpeedometerPainter extends CustomPainter {
  final double startAngle;
  final double sweepAngle;
  final double currentSpeed;
  final double minSpeed;
  final double maxSpeed;

  _SpeedometerPainter({
    required this.startAngle,
    required this.sweepAngle,
    required this.currentSpeed,
    required this.minSpeed,
    required this.maxSpeed,
  });

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

    // 绘制背景弧形轨道
    final backgroundPaint = Paint()
      ..color = Colors.grey[300]!
      ..style = PaintingStyle.stroke
      ..strokeWidth = 15;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      startAngle,
      sweepAngle,
      false,
      backgroundPaint,
    );

    // 绘制彩色进度弧形(分三段:绿色、黄色、红色)
    final greenPaint = Paint()..color = Colors.green;
    final yellowPaint = Paint()..color = Colors.yellow;
    final redPaint = Paint()..color = Colors.red;
    // 每段对应速度范围
    double greenSweep = sweepAngle * (60 / maxSpeed);
    double yellowSweep = sweepAngle * (60 / maxSpeed);
    double redSweep = sweepAngle * (60 / maxSpeed);

    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      startAngle,
      greenSweep,
      false,
      greenPaint..style = PaintingStyle.stroke..strokeWidth = 15,
    );
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      startAngle + greenSweep,
      yellowSweep,
      false,
      yellowPaint..style = PaintingStyle.stroke..strokeWidth = 15,
    );
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      startAngle + greenSweep + yellowSweep,
      redSweep,
      false,
      redPaint..style = PaintingStyle.stroke..strokeWidth = 15,
    );

    // 绘制刻度线和刻度值
    final textPainter = TextPainter(
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
    final linePaint = Paint()..color = Colors.black..strokeWidth = 2;
    // 每30km/h一个大刻度,共6个
    for (int i = 0; i <= 6; i++) {
      double angle = startAngle + (sweepAngle / 6) * i;
      // 大刻度线
      canvas.drawLine(
        Offset(
          center.dx + radius * cos(angle),
          center.dy + radius * sin(angle),
        ),
        Offset(
          center.dx + (radius - 15) * cos(angle),
          center.dy + (radius - 15) * sin(angle),
        ),
        linePaint,
      );
      // 刻度值
      textPainter.text = TextSpan(
        text: '${i * 30}',
        style: const TextStyle(fontSize: 14, color: Colors.black),
      );
      textPainter.layout();
      textPainter.paint(
        canvas,
        Offset(
          center.dx + (radius - 35) * cos(angle) - textPainter.width / 2,
          center.dy + (radius - 35) * sin(angle) - textPainter.height / 2,
        ),
      );
    }

    // 计算指针角度
    double pointerAngle = startAngle + (currentSpeed / maxSpeed) * sweepAngle;
    // 绘制指针
    final pointerPaint = Paint()..color = Colors.black..strokeWidth = 3..strokeCap = StrokeCap.round;
    canvas.drawLine(
      center,
      Offset(
        center.dx + (radius - 20) * cos(pointerAngle),
        center.dy + (radius - 20) * sin(pointerAngle),
      ),
      pointerPaint,
    );
    // 指针中心圆点
    canvas.drawCircle(center, 8, Paint()..color = Colors.black);
  }

  @override
  bool shouldRepaint(covariant _SpeedometerPainter oldDelegate) {
    return currentSpeed != oldDelegate.currentSpeed;
  }
}

使用方式

将Speedometer添加到页面中,拖动下方滑块就能看到指针跟随速度值转动,模拟速度计效果。


内容的提问来源于stack exchange,提问作者Sreerekha K V

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 23:26:07