Flutter自定义圆形音量滑块:指示器角度异常求助
圆形音量滑块指示器角度偏差修复方案
我需要实现一个圆形音量滑块,第三方包无法满足定制需求,自己写了Widget后功能基本可用,但指示器无法随进度正确移动,角度存在偏差。
我的代码如下:
import 'package:flutter/material.dart'; import 'dart:math'; void main() { runApp(MaterialApp(home: CircularVolumeSlider())); } class CircularVolumeSlider extends StatefulWidget { @override _CircularVolumeSliderState createState() => _CircularVolumeSliderState(); } class _CircularVolumeSliderState extends State<CircularVolumeSlider> { double _sliderValue = 0.0; // Initial value (0-100) Offset _previousOffset = Offset(0, 0); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ // Circular Slider GestureDetector( onPanUpdate: (DragUpdateDetails details) { final currentOffset = details.localPosition; final center = Offset(200, 200) / 2.0; // Use Offset // Calculate the vector from the center to the current and previous points final currentVector = currentOffset - center; final previousVector = _previousOffset - center; final distance = currentVector.distance; if (distance <= 75) { return; } // Calculate the angle between the vectors final angle = atan2(currentVector.dy, currentVector.dx) - atan2(previousVector.dy, previousVector.dx); // Calculate the value based on the angle and adjust to the correct range double newValue = (_sliderValue + angle / (2 * pi) * 100.0) % 100.0; if (newValue < 0) { newValue += 100; } setState(() { _sliderValue = newValue; }); // Store the current offset for the next update _previousOffset = currentOffset; }, onPanStart: (details) { _previousOffset = details.localPosition; }, child: Container( width: 230, height: 230, decoration: BoxDecoration( shape: BoxShape.circle, ), child: Stack( alignment: Alignment.center, children: [ Container( width: 200, // Set your desired size height: 200, decoration: const BoxDecoration( color: Colors.yellow, shape: BoxShape.circle, ), child: CircularProgressIndicator( value: _sliderValue / 100, strokeWidth: 12, // Customize the appearance ), ), // Vertical Line Indicator Positioned( // Adjust the position top: _previousOffset.dy, left: _previousOffset.dx, child: Transform.rotate( angle: (_sliderValue / 100 * 2 * pi) , child: Container( width: 10, // Set the line width height: 30, // Set the line height decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular( 6)) // Customize the color ), )), ], ), )), const SizedBox(height: 30), // Indicator View (you can use GestureDetector for swiping) // Percentage Display Column( children: [ const Text( 'Volume', style: TextStyle(fontSize: 20), ), Text( '$_sliderValue%', style: TextStyle(fontSize: 24), ), ], ), ], ), ), ); } }
问题分析
- 指示器位置错误:直接使用触摸点的
_previousOffset作为指示器位置,会导致指示器跟着触摸点乱跑,无法固定在圆形轨迹上;同时Transform.rotate的角度未与CircularProgressIndicator的起始角度对齐。 - 角度基准不匹配:
CircularProgressIndicator的进度起始角度是-π/2(顶部),而atan2的0角度是右侧,两者基准不一致直接导致角度偏差。 - 触摸逻辑累积误差:基于上一次触摸点的角度差计算滑块值,容易产生累积误差,不如直接根据当前触摸点计算对应进度更准确。
修复后的代码
import 'package:flutter/material.dart'; import 'dart:math'; void main() { runApp(const MaterialApp(home: CircularVolumeSlider())); } class CircularVolumeSlider extends StatefulWidget { const CircularVolumeSlider({super.key}); @override _CircularVolumeSliderState createState() => _CircularVolumeSliderState(); } class _CircularVolumeSliderState extends State<CircularVolumeSlider> { double _sliderValue = 0.0; // 0-100 final double _circleRadius = 100; // 黄色圆的半径 final double _indicatorRadius = 115; // 指示器所在轨迹的半径 // 将触摸点转换为滑块值 double _calculateValueFromOffset(Offset offset) { final center = Offset(_circleRadius, _circleRadius); final vector = offset - center; // 计算角度,调整基准到顶部(-π/2),并转换为0-1的范围 double angle = atan2(vector.dy, vector.dx) - (-pi/2); if (angle < 0) angle += 2 * pi; return (angle / (2 * pi)) * 100; } // 计算指示器的位置 Offset _calculateIndicatorPosition() { final center = Offset(_circleRadius, _circleRadius); // 基于滑块值计算角度,起始点为顶部 double angle = (_sliderValue / 100) * 2 * pi - pi/2; final dx = center.dx + _indicatorRadius * cos(angle); final dy = center.dy + _indicatorRadius * sin(angle); return Offset(dx, dy); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onPanUpdate: (DragUpdateDetails details) { final currentOffset = details.localPosition; final center = Offset(_circleRadius, _circleRadius); final distance = (currentOffset - center).distance; // 只响应圆形轨迹范围内的触摸 if (distance < _circleRadius - 10 || distance > _indicatorRadius + 10) return; setState(() { _sliderValue = _calculateValueFromOffset(currentOffset); }); }, onPanStart: (DragStartDetails details) { setState(() { _sliderValue = _calculateValueFromOffset(details.localPosition); }); }, child: Container( width: _indicatorRadius * 2 + 20, height: _indicatorRadius * 2 + 20, child: Stack( alignment: Alignment.center, children: [ // 背景黄色圆 Container( width: _circleRadius * 2, height: _circleRadius * 2, decoration: const BoxDecoration( color: Colors.yellow, shape: BoxShape.circle, ), ), // 进度环 SizedBox( width: _circleRadius * 2, height: _circleRadius * 2, child: CircularProgressIndicator( value: _sliderValue / 100, strokeWidth: 12, strokeCap: StrokeCap.round, ), ), // 指示器 Positioned( left: _calculateIndicatorPosition().dx - 5, // 减去指示器宽度的一半 top: _calculateIndicatorPosition().dy - 15, // 减去指示器高度的一半 child: Container( width: 10, height: 30, decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(6), ), ), ), ], ), ), ), const SizedBox(height: 30), Column( children: [ const Text( '音量', style: TextStyle(fontSize: 20), ), Text( '${_sliderValue.toStringAsFixed(0)}%', style: const TextStyle(fontSize: 24), ), ], ), ], ), ), ); } }
关键修改点
- 指示器位置计算:根据滑块值对应的角度,从中心向外计算指示器的固定轨迹位置,确保指示器始终在圆形边缘。
- 角度基准对齐:将
atan2的计算结果减去-π/2,让进度的起始点和CircularProgressIndicator保持一致(顶部)。 - 触摸逻辑优化:直接根据当前触摸点计算对应的滑块值,避免角度差累积的误差,同时限制触摸响应的范围,提升交互体验。
内容的提问来源于stack exchange,提问作者dev
相关产品推荐
相关产品推荐

