Flutter实现半圆形滚轮:滚动改值+静态指针方案咨询
Flutter 半圆形滚动滚轮实现方案
核心思路
- 用
CustomPainter绘制半圆形刻度与数值,通过三角函数计算每个数值的坐标位置 - 借助
GestureDetector监听拖拽手势,根据偏移量更新选中的数值 - 用
Stack布局将静态指针放在滚轮上层,保持固定位置
具体实现步骤
- 布局搭建
- 以
Stack为容器,底层放置可交互的半圆形数值区域,上层叠加静态指针组件
- 以
- 数值区域绘制
- 利用三角函数(
cos/sin)计算每个数值在半圆路径上的坐标,角度范围设为从90°到270°(对应上半圆到下半圆) - 通过
CustomPainter绘制半圆边框和所有数值,选中的数值可设置高亮样式
- 利用三角函数(
- 手势交互处理
- 监听
onPanUpdate事件,根据拖拽的偏移量计算数值切换的步长 - 限制索引范围,避免超出数值列表边界,同时触发数值变更回调
- 监听
- 体验优化
- 添加吸附效果,滚动停止时自动将最近的数值对齐到指针位置
- 实现按需渲染,只绘制指针可视范围内的数值,降低性能消耗
简化版代码示例
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
相关产品推荐
相关产品推荐

