当前位置: 首页 > news >正文

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坐标系转换
    ↓
视觉渲染: 柱状图高度 ∝ 数据值

学习路径建议

第一阶段:理解数据流

  1. 跟踪 eqIndex 的变化

  2. 理解 presetList 的初始化

  3. 分析 DrawEQ 的数据转换

第二阶段:分析渲染逻辑

  1. 研究 Canvas 绘制原理

  2. 理解坐标系统转换

  3. 分析视觉映射算法

第三阶段:掌握架构设计

  1. 学习状态管理

  2. 理解组件通信

  3. 分析扩展性设计

代码实例

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()];}}

http://www.dtcms.com/a/499514.html

相关文章:

  • 响应式食品企业网站网站的外链是什么
  • 【Protobuf】proto3语法详解1
  • 网站备案要做家居网站设计
  • VS2022+DirectX9之创建DirectX9设备
  • unordered_map和unordered_set的封装与简单测试
  • (Kotlin协程十六)try/catch 可以捕获子协程的异常吗?为什么?
  • 网站移动端怎么做的做外国网站怎么买空间
  • 图像的脉冲噪声和中值滤波
  • 3.4特殊矩阵的压缩存储
  • SpringAI+DeepSeek大模型应用开发
  • 递归-24.两两交换链表中的节点-力扣(LeetCode)
  • 【Java零基础·第12章】Lambda与Stream API
  • Qemu-NUC980(八):GPIO Controller
  • 外贸型企业网站建设开源商城源码
  • JS逆向-安全辅助项目Yakit热加载魔术方法模版插件语法JSRpc进阶调用接口联动
  • 使用IOT-Tree接入各种数据转BACnet模拟设备输出
  • 网站搭建说明北京海淀区是几环
  • 基于多模态AI技术的传统行业智能化升级路径研究——以开源AI大模型、AI智能名片与S2B2C商城小程序为例
  • 【C语言进阶】指针进阶_数组指针的使用,数组参数和指针参数
  • PySide6 控件插入日期时间(QDateTime)
  • 网站建设 jsp php垂直网站建设
  • 招商网站大全企业官方网站建设的流程
  • 征程 6 | 工具链如何支持 Matmul/Conv 双 int16 输入量化?
  • 【案例实战】鸿蒙分布式调度:跨设备协同实战
  • 中英文网站设计网站开发投标文件
  • Langgraph译文1:让AI自主决策的代理架构
  • 如何让百度能查到自己衡阳专业的关键词优化终报价
  • 为什么.NET的System.IO.Compression无法解压zlib流
  • 微信小程序:日常零售供应系统
  • 安卓如何查看settings是被哪个进程更新的?相关dumpsys命令剖析