Flutter---EQ均衡器
效果图
实现步骤
需要理解的问题点
模块职责
-
HomePage: 主页面容器
-
DrawEQ: 可视化渲染器
-
EqModel: 数据实体和业务逻辑
-
_HomePageState: 状态管理和用户交互
初始化数据流
initState()
↓
presetList = EqModel.preset()
↓
[默认, 流行, 摇滚, 爵士, 经典, 乡村] 6个预设
↓
eqIndex = 0 (选中第一个)
↓
presetList[0].data → DrawEQ 渲染
用户交互数据流
用户点击"下一个"按钮
↓
eqIndex++ (状态变更)
↓
setState() (触发重建)
↓
build() 重新执行
↓
DrawEQ(presetList[eqIndex].data) (新数据渲染)
↓
UI更新显示新EQ模式
EQ数据处理流
原始EQ数据: [-12dB 到 +12dB]
↓
DrawEQ构造函数: data[i] + 12
↓
转换后数据: [0 到 24] (便于Canvas绘制)
↓
Canvas坐标系转换
↓
视觉渲染: 柱状图高度 ∝ 数据值
学习路径建议
第一阶段:理解数据流
-
跟踪 eqIndex 的变化
-
理解 presetList 的初始化
-
分析 DrawEQ 的数据转换
第二阶段:分析渲染逻辑
-
研究 Canvas 绘制原理
-
理解坐标系统转换
-
分析视觉映射算法
第三阶段:掌握架构设计
-
学习状态管理
-
理解组件通信
-
分析扩展性设计
代码实例
home_page.dart
import 'dart:math' as math;import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:typed_data';
import 'eq_mode.dart';class HomePage extends StatefulWidget{const HomePage({super.key});@overrideState<StatefulWidget> createState() => _HomePageState();}class _HomePageState extends State<HomePage> {late List<EqModel> presetList;//EQ列表final txt = ["8","31","62","125","250","500","1k","2k","4k","16k"];//柱状图的底部X轴文字int eqIndex = 0;//当前选中的EQ索引@overridevoid initState() {super.initState();presetList = EqModel.preset();//获取设备预设EQ}//UI构建@overrideWidget build(BuildContext context) {return Column(crossAxisAlignment: CrossAxisAlignment.center,//水平居中mainAxisAlignment: MainAxisAlignment.center,//垂直居中children: [Text("EQ均衡器",style: const TextStyle(fontSize: 18,color: Colors.black),),//均衡器标题Card(color: const Color(0xFFEFF2F9),//整个均衡器的背景色borderOnForeground: false,margin: EdgeInsets.only(top: 10,left: 10,right: 10),child: Container(height: 321,width: double.infinity,padding: const EdgeInsets.only(left: 10, bottom: 10, right: 10),margin: const EdgeInsets.only(top: 10),child: Column(crossAxisAlignment: CrossAxisAlignment.start,//EQ波形图区域children: [Expanded(child: GestureDetector(child: CustomPaint(//绘制柱状图painter: DrawEQ(presetList[eqIndex].data),size: const Size(double.infinity, double.infinity), //让 CustomPaint 尽可能占据所有可用空间),)),//2.频率标签行(8Hz - 16kHz)Row(mainAxisAlignment: MainAxisAlignment.spaceAround,//10个频率点children: [SizedBox(width: 20,child:Text(txt[0],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8),)),SizedBox(width: 20,child:Text(txt[1],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8))),SizedBox(width: 20,child:Text(txt[2],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8))),SizedBox(width: 20,child:Text(txt[3],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8))),SizedBox(width: 20,child:Text(txt[4],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8))),SizedBox(width: 20,child:Text(txt[5],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8))),SizedBox(width: 20,child:Text(txt[6],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8))),SizedBox(width: 20,child:Text(txt[7],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8))),SizedBox(width: 20,child:Text(txt[8],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8))),SizedBox(width: 20,child:Text(txt[9],textAlign: TextAlign.center,style: const TextStyle(fontSize: 8)))],),const SizedBox(height: 5),/// 底部eq选择//EQ选择器Row(children: [//左边按钮IconButton(onPressed: () {if (eqIndex > 0) {eqIndex--;setState(() {});}},icon: Transform.flip(flipX: true,child: Icon(Icons.play_arrow_rounded, //上一个EQcolor: eqIndex <=0? Colors.grey: Colors.black,),)),//中间文字显示Expanded(child: Container(height: 44,alignment: Alignment.center,decoration: BoxDecoration(borderRadius: BorderRadius.circular(15),color: Colors.black),child: Text( //当前EQ的名称presetList[eqIndex].name,style: const TextStyle(color: Colors.white,fontSize: 24,),),)),//右边按钮IconButton(onPressed: () {if (eqIndex < presetList.length - 1) {eqIndex++;setState(() {});}},icon: Icon(Icons.play_arrow_rounded, //下一个EQcolor: eqIndex >= presetList.length - 1? Colors.grey: Colors.black,)),],)],),),)],);}
}//自定义EQ波形绘制(DrawEQ)
class DrawEQ extends CustomPainter {late final Color myColor; //声明一个延迟初始化的最终颜色变量,用于数据条颜色late final Color dotColor;//声明圆点颜色late final double dotRadius;//声明圆点半径//构造函数//接收int8List类型的EQ数据DrawEQ(Int8List data,{Color? color,Color? dot,this.dotRadius = 20}) {eqList = List.filled(data.length, 0);//创建一个与输入数据长度相同的列表,并用0填充所有位置for (int i = 0; i < data.length; i++) {eqList[i] = data[i] + 12;//将EQ数据从范围[-12, +12]转换到[0, 24],因为传过来的数据有负数,需要转成全部正数来画图}myColor = color?? Colors.black;dotColor = dot?? Colors.white;}late final List<int> eqList;//声明一个延迟初始化的最终整型列表,用于存储转换后的EQ数据@overridevoid paint(Canvas canvas, Size size) {/// 间隔final margin = size.width / eqList.length; //计算每个频段之间的水平间距var x = margin / 2.0; //初始化X坐标,从每个频段区域的中心开始final path = Path(); //创建一个 Path 对象,用于绘制灰色背景条final paint = Paint()//创建一个 Path 对象,用于配置绘制样式..color = Colors.grey.withOpacity(0.7) //设置颜色为70%透明度的灰色(柱状图的底色)..style = PaintingStyle.fill; //设置绘制样式为填充(非描边)final dataPath = Path();final dataPaint = Paint()//创建Path对象,用于绘制蓝色数据条..color = myColor..style = PaintingStyle.fill;final scale = size.height / 25; //计算缩放比例// 圆点final dotPath = Path(); //创建Path对象,用于绘制蓝色圆点final dotPaint = Paint()//创建圆点的Paint对象,使用dotColor颜色..color = dotColor..style = PaintingStyle.fill;// 外框final dotBgPath = Path();//创建圆点的Paint对象,使用dotColor颜色final dotBgPaint = Paint()//创建Path对象,用于绘制圆点的白色外框..color = Colors.white..strokeWidth = 1 //描边宽度..style = PaintingStyle.stroke; //描边样式const radius = Radius.circular(5); //创建一个圆角半径常量,值为5for(int i = 0; i < eqList.length; i ++) { //循环绘制final y = scale * eqList[i]; //遍历EQ数据的每个频段path.addRRect(RRect.fromRectAndRadius(Rect.fromLTWH(x, 0, dotRadius / 2, size.height), radius)); //绘制灰色背景条dataPath.addRRect(RRect.fromRectAndRadius(Rect.fromLTWH(x,size.height , dotRadius / 2, - y),radius)); //绘制蓝色数据条dotPath.addArc(Rect.fromLTWH(x - 2.5, size.height - y , dotRadius , dotRadius), 0, math.pi * 2);//绘制圆点和外框dotBgPath.addArc(Rect.fromLTWH(x - 2.5, size.height - y , dotRadius , dotRadius), 0, math.pi * 2);x += margin;}//实际执行绘制操作canvas.drawPath(path, paint);canvas.drawPath(dataPath, dataPaint);canvas.drawPath(dotPath, dotPaint);canvas.drawPath(dotBgPath, dotBgPaint);}//始终返回true,表示任何时候都需要重新绘制@overridebool shouldRepaint(covariant DrawEQ oldDelegate) => true;
}
eq_mode.dart
import 'dart:typed_data';class EqModel {// 可调节段数(通常是10段)late final int count;late int mode;// 当前EQ模式标识late Int8List data;//存储10个频段的增益值var name = ""; //EQ模式名称//默认构造函数EqModel() {data = Int8List(25);//分配25个字节的缓冲区count = 0;mode = 0;}EqModel.fromCustom(this.mode,this.data,this.name) {count = data.length;//频段数等于数据长度}/// 默认EqModel.fromDefault() {mode = 0;count = 10;data = Int8List.fromList([0,0,0,0,0,0,0,0,0,0]); //所有频段增益为0name = "默认";}/// 流行EqModel.fromPop() {mode = 1;count = 10;data = Int8List.fromList([3, 1, 0, -2, -4, -4, -2, 0, 1, 2]);name = "流行";}/// 摇滚EqModel.fromRock() {mode = 2;count = 10;data = Int8List.fromList([-2, 0, 2, 4, -2, -2, 0, 0, 4, 4,]);name = "摇滚";}EqModel.fromJazz() {mode = 3;count = 10;data = Int8List.fromList([ 0, 0, 0, 4, 4, 4, 0, 2, 3, 4]);name = "爵士";}EqModel.fromClassic() {mode = 4;count = 10;data = Int8List.fromList([0, 8, 8, 4, 0, 0, 0, 0, 2, 2,]);name = "经典";}/// 乡村EqModel.fromCountry() {mode = 5;count = 10;data = Int8List.fromList([-2, 0, 0, 2, 2, 0, 0, 0, 4, 4]);name = "乡村";}//静态方法--获取所有预设static List<EqModel> preset() {//模式0-模式1-模式2-模式3-模式4-模式5-return [EqModel.fromDefault(),EqModel.fromPop(),EqModel.fromRock(),EqModel.fromJazz(),EqModel.fromClassic(),EqModel.fromCountry()];}}