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

Flutter实现半圆形滚轮:滚动改值+静态指针方案咨询

Flutter 半圆形滚动滚轮实现方案

核心思路

  • 用CustomPainter绘制半圆形刻度与数值,通过三角函数计算每个数值的坐标位置
  • 借助GestureDetector监听拖拽手势,根据偏移量更新选中的数值
  • 用Stack布局将静态指针放在滚轮上层,保持固定位置

具体实现步骤

  1. 布局搭建
    • 以Stack为容器,底层放置可交互的半圆形数值区域,上层叠加静态指针组件
  2. 数值区域绘制
    • 利用三角函数(cos/sin)计算每个数值在半圆路径上的坐标,角度范围设为从90°到270°(对应上半圆到下半圆)
    • 通过CustomPainter绘制半圆边框和所有数值,选中的数值可设置高亮样式
  3. 手势交互处理
    • 监听onPanUpdate事件,根据拖拽的偏移量计算数值切换的步长
    • 限制索引范围,避免超出数值列表边界,同时触发数值变更回调
  4. 体验优化
    • 添加吸附效果,滚动停止时自动将最近的数值对齐到指针位置
    • 实现按需渲染,只绘制指针可视范围内的数值,降低性能消耗

简化版代码示例

import 'package:flutter/material.dart';

class SemiCircleRoller extends StatefulWidget {
  final List<int> values;
  final ValueChanged<int> onValueChanged;

  const SemiCircleRoller({
    super.key,
    required this.values,
    required this.onValueChanged,
  });

  @override
  State<SemiCircleRoller> createState() => _SemiCircleRollerState();
}

class _SemiCircleRollerState extends State<SemiCircleRoller> {
  double _offset = 0.0;
  int _selectedIndex = 0;
  final double _radius = 150.0;
  final double _startAngle = 3.14 / 2; // 90°(顶部)
  final double _endAngle = 3 * 3.14 / 2; // 270°(底部)

  @override
  void initState() {
    super.initState();
    _selectedIndex = widget.values.length ~/ 2;
    widget.onValueChanged(widget.values[_selectedIndex]);
  }

  double _getAngleForIndex(int index) {
    final angleRange = _endAngle - _startAngle;
    final step = angleRange / (widget.values.length - 1);
    return _startAngle + step * index;
  }

  void _handlePanUpdate(DragUpdateDetails details) {
    setState(() {
      _offset += details.delta.dy;
      // 自定义拖拽步长,可根据需求调整
      const stepThreshold = 20.0;
      final indexChange = (_offset ~/ stepThreshold).toInt();
      if (indexChange != 0) {
        int newIndex = _selectedIndex + indexChange;
        newIndex = newIndex.clamp(0, widget.values.length - 1);
        if (newIndex != _selectedIndex) {
          _selectedIndex = newIndex;
          widget.onValueChanged(widget.values[_selectedIndex]);
        }
        _offset = 0.0;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        // 可拖拽的半圆形数值区域
        GestureDetector(
          onPanUpdate: _handlePanUpdate,
          child: CustomPaint(
            size: Size(_radius * 2, _radius),
            painter: _SemiCirclePainter(
              values: widget.values,
              radius: _radius,
              startAngle: _startAngle,
              endAngle: _endAngle,
              selectedIndex: _selectedIndex,
            ),
          ),
        ),
        // 静态指针
        const Icon(
          Icons.arrow_downward,
          color: Colors.red,
          size: 30,
        ),
      ],
    );
  }
}

class _SemiCirclePainter extends CustomPainter {
  final List<int> values;
  final double radius;
  final double startAngle;
  final double endAngle;
  final int selectedIndex;

  _SemiCirclePainter({
    required this.values,
    required this.radius,
    required this.startAngle,
    required this.endAngle,
    required this.selectedIndex,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height);
    final angleRange = endAngle - startAngle;
    final step = angleRange / (values.length - 1);

    // 文本样式
    final defaultTextStyle = const TextStyle(
      color: Colors.black,
      fontSize: 16,
      fontWeight: FontWeight.normal,
    );
    final selectedTextStyle = defaultTextStyle.copyWith(
      color: Colors.blue,
      fontWeight: FontWeight.bold,
      fontSize: 18,
    );

    // 绘制所有数值
    for (int i = 0; i < values.length; i++) {
      final angle = startAngle + step * i;
      final posX = center.dx + radius * cos(angle);
      final posY = center.dy + radius * sin(angle);
      
      final textSpan = TextSpan(
        text: values[i].toString(),
        style: i == selectedIndex ? selectedTextStyle : defaultTextStyle,
      );
      final textPainter = TextPainter(
        text: textSpan,
        textDirection: TextDirection.ltr,
        textAlign: TextAlign.center,
      );
      textPainter.layout();
      // 将文本居中绘制在目标坐标
      textPainter.paint(
        canvas,
        Offset(posX - textPainter.width / 2, posY - textPainter.height / 2),
      );
    }

    // 绘制半圆边框
    final borderPaint = Paint()
      ..color = Colors.grey.shade400
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      startAngle,
      angleRange,
      false,
      borderPaint,
    );
  }

  @override
  bool shouldRepaint(covariant _SemiCirclePainter oldDelegate) {
    return oldDelegate.selectedIndex != selectedIndex;
  }
}

// 调用示例
void main() {
  runApp(const MaterialApp(
    home: Scaffold(
      backgroundColor: Colors.white,
      body: Center(
        child: SemiCircleRoller(
          values: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
          onValueChanged: (value) => print('当前选中数值: $value'),
        ),
      ),
    ),
  ));
}

额外优化建议

  • 加入动画效果:使用AnimationController在拖拽停止时,平滑将选中数值过渡到指针位置
  • 自定义参数支持:开放半圆半径、数值样式、指针样式等配置项,提升组件复用性
  • 性能优化:当数值数量较多时,只绘制指针附近的3-5个数值,减少不必要的绘制操作

内容的提问来源于stack exchange,提问作者Asif Shaikh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 23:22:56