效果图

实现步骤
需要理解的问题点
模块职责
-
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
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});
@override
State<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索引
@override
void initState() {
super.initState();
presetList = EqModel.preset();//获取设备预设EQ
}
//UI构建
@override
Widget 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, //上一个EQ
color: 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, //下一个EQ
color: 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数据
@override
void 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); //创建一个圆角半径常量,值为5
for(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,表示任何时候都需要重新绘制
@override
bool shouldRepaint(covariant DrawEQ oldDelegate) => true;
}
eq_mode.dart
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]); //所有频段增益为0
name = "默认";
}
/// 流行
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()];
}
}