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

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

}
相关推荐
LinXunFeng7 小时前
Flutter webview 崩溃率上升怎么办?我的分析与解决方案
flutter·ios·webview
西西学代码10 小时前
Flutter---GridView+自定义控件
flutter
hweiyu0016 小时前
Flutter零基础极速入门到进阶实战(视频教程)
flutter
hweiyu0016 小时前
Flutter高级进阶教程(视频教程)
flutter
SoaringHeart2 天前
Flutter封装:原生路由管理极简封装 AppNavigator
前端·flutter
疯笔码良2 天前
【Flutter】flutter安装并在Xcode上应用
flutter·macos·xcode
西西学代码2 天前
Flutter---两种带输入框的对话框
flutter
西西学代码2 天前
Flutter---Button
flutter