Flutter简易测振仪:打造专业级振动监测应用

项目概述

在工业监测、建筑检测、设备维护等领域,振动监测是一项重要的技术手段。本教程将带你使用Flutter开发一个功能完整的简易测振仪应用,实现实时振动监测、数据记录、波形显示等专业功能。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用特色

  • 实时监测:模拟加速度计数据,实时显示振动强度
  • 可视化显示:动态圆形指示器和实时波形图
  • 数据记录:完整的测量记录管理系统
  • 统计分析:最大值、最小值、平均值等统计数据
  • 专业界面:仿真专业测振设备的用户界面

技术架构

核心技术栈

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

项目结构

lib/
├── main.dart              # 应用入口和主要逻辑
├── models/               # 数据模型(集成在main.dart中)
│   ├── vibration_data.dart # 振动数据模型
│   └── measurement_record.dart # 测量记录模型
├── pages/                # 页面组件(集成在main.dart中)
│   ├── vibration_meter_page.dart # 主测量页面
│   ├── records_list_page.dart    # 记录列表页面
│   └── record_detail_page.dart   # 记录详情页面
└── widgets/              # 自定义组件(集成在main.dart中)
    ├── waveform_painter.dart     # 波形绘制器
    └── detail_waveform_painter.dart # 详细波形绘制器

数据模型设计

振动数据模型(VibrationData)

振动数据模型是应用的核心数据结构,记录三轴加速度和振动强度:

class VibrationData {
  final double x;              // X轴加速度 (m/s²)
  final double y;              // Y轴加速度 (m/s²)
  final double z;              // Z轴加速度 (m/s²)
  final double magnitude;      // 振动强度 (m/s²)
  final DateTime timestamp;    // 时间戳

  VibrationData({
    required this.x,
    required this.y,
    required this.z,
    required this.magnitude,
    required this.timestamp,
  });
}

振动强度通过三轴加速度计算得出:

final magnitude = sqrt(x * x + y * y + z * z);

测量记录模型(MeasurementRecord)

测量记录模型存储完整的测量会话数据:

class MeasurementRecord {
  final String id;                    // 记录唯一标识
  final DateTime startTime;           // 开始时间
  final DateTime endTime;             // 结束时间
  final double maxMagnitude;          // 最大振动强度
  final double avgMagnitude;          // 平均振动强度
  final double minMagnitude;          // 最小振动强度
  final List<VibrationData> dataPoints; // 数据点列表
  final String location;              // 测量位置
  final String notes;                 // 备注信息

  MeasurementRecord({
    required this.id,
    required this.startTime,
    required this.endTime,
    required this.maxMagnitude,
    required this.avgMagnitude,
    required this.minMagnitude,
    required this.dataPoints,
    required this.location,
    required this.notes,
  });

  // 计算测量时长
  Duration get duration => endTime.difference(startTime);
  
  // 格式化时长显示
  String get durationText {
    final seconds = duration.inSeconds;
    final minutes = seconds ~/ 60;
    final remainingSeconds = seconds % 60;
    return '${minutes}:${remainingSeconds.toString().padLeft(2, '0')}';
  }
}

应用主体结构

应用入口

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '简易测振仪',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const VibrationMeterPage(),
    );
  }
}

应用采用蓝色作为主题色,营造专业、科技的视觉效果。

主页面结构

主页面包含状态栏、振动显示、数据卡片、实时图表和控制按钮:

class VibrationMeterPage extends StatefulWidget {
  const VibrationMeterPage({super.key});

  
  State<VibrationMeterPage> createState() => _VibrationMeterPageState();
}

class _VibrationMeterPageState extends State<VibrationMeterPage>
    with TickerProviderStateMixin {
  
  // 测量状态
  bool _isRecording = false;
  bool _isPaused = false;
  
  // 当前振动数据
  VibrationData? _currentData;
  
  // 历史数据和记录
  final List<VibrationData> _dataHistory = [];
  final List<MeasurementRecord> _records = [];
  
  // 动画控制器
  late AnimationController _pulseController;
  late AnimationController _waveController;
  
  // 测量参数
  DateTime? _recordingStartTime;
  int _recordingDuration = 0;
  
  // 统计数据
  double _maxMagnitude = 0.0;
  double _minMagnitude = double.infinity;
  double _avgMagnitude = 0.0;
  
  // 设置参数
  double _sensitivity = 1.0;
  int _sampleRate = 50; // Hz
  bool _showRealTimeGraph = true;
}

核心功能实现

振动数据模拟

由于移动设备的加速度计API在模拟器中不可用,我们使用数学函数模拟真实的振动数据:

void _simulateVibrationData() {
  final random = Random();
  
  // 模拟加速度计数据 (m/s²)
  final baseNoise = 0.1;
  final x = (random.nextDouble() - 0.5) * baseNoise * _sensitivity;
  final y = (random.nextDouble() - 0.5) * baseNoise * _sensitivity;
  final z = 9.8 + (random.nextDouble() - 0.5) * baseNoise * _sensitivity;
  
  // 添加振动信号
  final vibrationIntensity = _generateVibrationIntensity();
  final vibX = x + vibrationIntensity * (random.nextDouble() - 0.5);
  final vibY = y + vibrationIntensity * (random.nextDouble() - 0.5);
  final vibZ = z + vibrationIntensity * (random.nextDouble() - 0.5);
  
  final magnitude = sqrt(vibX * vibX + vibY * vibY + vibZ * vibZ);
  
  final data = VibrationData(
    x: vibX,
    y: vibY,
    z: vibZ,
    magnitude: magnitude,
    timestamp: DateTime.now(),
  );

  setState(() {
    _currentData = data;
    
    if (_isRecording && !_isPaused) {
      _dataHistory.add(data);
      _updateStatistics(magnitude);
    }
  });
}

double _generateVibrationIntensity() {
  final random = Random();
  final time = DateTime.now().millisecondsSinceEpoch / 1000.0;
  
  // 基础振动 + 随机脉冲
  double intensity = 0.5 * sin(time * 2 * pi * 0.5); // 0.5Hz 基础振动
  intensity += 0.3 * sin(time * 2 * pi * 2.0); // 2Hz 振动
  
  // 随机脉冲
  if (random.nextDouble() < 0.05) {
    intensity += random.nextDouble() * 3.0;
  }
  
  return intensity.abs();
}

状态栏设计

状态栏显示当前测量状态和时长:

Widget _buildStatusBar() {
  return Container(
    width: double.infinity,
    padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
    color: _isRecording 
        ? (_isPaused ? Colors.orange.withValues(alpha: 0.1) : Colors.red.withValues(alpha: 0.1))
        : Colors.green.withValues(alpha: 0.1),
    child: Row(
      children: [
        Icon(
          _isRecording 
              ? (_isPaused ? Icons.pause : Icons.fiber_manual_record)
              : Icons.sensors,
          color: _isRecording 
              ? (_isPaused ? Colors.orange : Colors.red)
              : Colors.green,
          size: 16,
        ),
        const SizedBox(width: 8),
        Text(
          _isRecording 
              ? (_isPaused ? '测量已暂停' : '正在测量中...')
              : '准备就绪',
          style: TextStyle(
            color: _isRecording 
                ? (_isPaused ? Colors.orange : Colors.red)
                : Colors.green,
            fontWeight: FontWeight.w500,
          ),
        ),
        const Spacer(),
        if (_isRecording) ...[
          Text(
            _formatDuration(_recordingDuration),
            style: const TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 16,
            ),
          ),
        ],
      ],
    ),
  );
}

振动强度显示

核心的振动强度显示采用动态圆形指示器:

Widget _buildVibrationDisplay() {
  final magnitude = _currentData?.magnitude ?? 0.0;
  final normalizedMagnitude = (magnitude / 15.0).clamp(0.0, 1.0);
  
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(20),
      child: Column(
        children: [
          const Text(
            '振动强度',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 20),
          AnimatedBuilder(
            animation: _pulseController,
            builder: (context, child) {
              return Container(
                width: 200,
                height: 200,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  gradient: RadialGradient(
                    colors: [
                      _getVibrationColor(magnitude).withValues(alpha: 0.8),
                      _getVibrationColor(magnitude).withValues(alpha: 0.3),
                      _getVibrationColor(magnitude).withValues(alpha: 0.1),
                    ],
                    stops: [
                      0.0,
                      0.7 + normalizedMagnitude * 0.3,
                      1.0,
                    ],
                  ),
                  boxShadow: [
                    BoxShadow(
                      color: _getVibrationColor(magnitude).withValues(alpha: 0.3),
                      blurRadius: 20 + normalizedMagnitude * 30,
                      spreadRadius: 5 + normalizedMagnitude * 10,
                    ),
                  ],
                ),
                child: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        magnitude.toStringAsFixed(3),
                        style: const TextStyle(
                          fontSize: 24,
                          fontWeight: FontWeight.bold,
                          color: Colors.white,
                        ),
                      ),
                      const Text(
                        'm/s²',
                        style: TextStyle(
                          fontSize: 14,
                          color: Colors.white70,
                        ),
                      ),
                    ],
                  ),
                ),
              );
            },
          ),
          const SizedBox(height: 20),
          Text(
            _getVibrationLevel(magnitude),
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.w500,
              color: _getVibrationColor(magnitude),
            ),
          ),
        ],
      ),
    ),
  );
}

振动等级判断

根据振动强度判断振动等级和颜色:

Color _getVibrationColor(double magnitude) {
  if (magnitude < 9.9) return Colors.green;
  if (magnitude < 10.5) return Colors.yellow;
  if (magnitude < 11.0) return Colors.orange;
  return Colors.red;
}

String _getVibrationLevel(double magnitude) {
  if (magnitude < 9.9) return '正常';
  if (magnitude < 10.5) return '轻微振动';
  if (magnitude < 11.0) return '中等振动';
  return '强烈振动';
}

数据统计卡片

显示最大值、平均值、最小值的统计信息:

Widget _buildDataCards() {
  return Row(
    children: [
      Expanded(
        child: _buildDataCard(
          '最大值',
          '${_maxMagnitude.toStringAsFixed(3)} m/s²',
          Icons.trending_up,
          Colors.red,
        ),
      ),
      const SizedBox(width: 8),
      Expanded(
        child: _buildDataCard(
          '平均值',
          '${_avgMagnitude.toStringAsFixed(3)} m/s²',
          Icons.show_chart,
          Colors.blue,
        ),
      ),
      const SizedBox(width: 8),
      Expanded(
        child: _buildDataCard(
          '最小值',
          '${_minMagnitude == double.infinity ? 0.0 : _minMagnitude.toStringAsFixed(3)} m/s²',
          Icons.trending_down,
          Colors.green,
        ),
      ),
    ],
  );
}

Widget _buildDataCard(String title, String value, IconData icon, Color color) {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(12),
      child: Column(
        children: [
          Icon(icon, color: color, size: 24),
          const SizedBox(height: 8),
          Text(
            title,
            style: TextStyle(
              fontSize: 12,
              color: Colors.grey[600],
            ),
          ),
          const SizedBox(height: 4),
          Text(
            value,
            style: TextStyle(
              fontSize: 14,
              fontWeight: FontWeight.bold,
              color: color,
            ),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    ),
  );
}

实时波形图表

实时波形图表使用CustomPainter绘制振动波形:

Widget _buildRealTimeGraph() {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '实时波形',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 16),
          Container(
            height: 150,
            width: double.infinity,
            decoration: BoxDecoration(
              color: Colors.black,
              borderRadius: BorderRadius.circular(8),
            ),
            child: CustomPaint(
              painter: WaveformPainter(_dataHistory.take(100).toList()),
            ),
          ),
        ],
      ),
    ),
  );
}

波形绘制器实现

WaveformPainter类负责绘制实时振动波形:

class WaveformPainter extends CustomPainter {
  final List<VibrationData> data;

  WaveformPainter(this.data);

  
  void paint(Canvas canvas, Size size) {
    if (data.isEmpty) return;

    final paint = Paint()
      ..color = Colors.green
      ..strokeWidth = 2
      ..style = PaintingStyle.stroke;

    final path = Path();
    
    for (int i = 0; i < data.length; i++) {
      final x = (i / data.length) * size.width;
      final normalizedMagnitude = ((data[i].magnitude - 9.8) / 2.0 + 0.5).clamp(0.0, 1.0);
      final y = size.height - (normalizedMagnitude * size.height);
      
      if (i == 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    }

    canvas.drawPath(path, paint);
    
    // 绘制中心线
    final centerLinePaint = Paint()
      ..color = Colors.grey.withValues(alpha: 0.5)
      ..strokeWidth = 1;
    
    canvas.drawLine(
      Offset(0, size.height / 2),
      Offset(size.width, size.height / 2),
      centerLinePaint,
    );
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

测量控制功能

开始测量
void _startRecording() {
  setState(() {
    _isRecording = true;
    _isPaused = false;
    _recordingStartTime = DateTime.now();
    _recordingDuration = 0;
    _dataHistory.clear();
    _maxMagnitude = 0.0;
    _minMagnitude = double.infinity;
    _avgMagnitude = 0.0;
  });

  _recordingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
    setState(() {
      _recordingDuration++;
    });
  });
}
暂停和继续
void _pauseRecording() {
  setState(() {
    _isPaused = !_isPaused;
  });
}
停止测量并保存记录
void _stopRecording() {
  if (_recordingStartTime != null && _dataHistory.isNotEmpty) {
    final record = MeasurementRecord(
      id: 'record_${DateTime.now().millisecondsSinceEpoch}',
      startTime: _recordingStartTime!,
      endTime: DateTime.now(),
      maxMagnitude: _maxMagnitude,
      avgMagnitude: _avgMagnitude,
      minMagnitude: _minMagnitude,
      dataPoints: List.from(_dataHistory),
      location: '未知位置',
      notes: '',
    );

    setState(() {
      _records.insert(0, record);
      _isRecording = false;
      _isPaused = false;
      _recordingDuration = 0;
    });

    _recordingTimer?.cancel();
    
    _showRecordSavedDialog(record);
  }
}

控制按钮设计

Widget _buildControlButtons() {
  return Column(
    children: [
      Row(
        children: [
          Expanded(
            child: ElevatedButton.icon(
              onPressed: _isRecording ? null : _startRecording,
              icon: const Icon(Icons.play_arrow),
              label: const Text('开始测量'),
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.green,
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 12),
              ),
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: ElevatedButton.icon(
              onPressed: _isRecording ? _pauseRecording : null,
              icon: Icon(_isPaused ? Icons.play_arrow : Icons.pause),
              label: Text(_isPaused ? '继续' : '暂停'),
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.orange,
                foregroundColor: Colors.white,
                padding: const EdgeInsets.symmetric(vertical: 12),
              ),
            ),
          ),
        ],
      ),
      const SizedBox(height: 8),
      SizedBox(
        width: double.infinity,
        child: ElevatedButton.icon(
          onPressed: _isRecording ? _stopRecording : null,
          icon: const Icon(Icons.stop),
          label: const Text('停止测量'),
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.red,
            foregroundColor: Colors.white,
            padding: const EdgeInsets.symmetric(vertical: 12),
          ),
        ),
      ),
    ],
  );
}

设置功能

设置对话框

void _showSettings() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('设置'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            title: const Text('灵敏度'),
            subtitle: Slider(
              value: _sensitivity,
              min: 0.1,
              max: 5.0,
              divisions: 49,
              label: _sensitivity.toStringAsFixed(1),
              onChanged: (value) {
                setState(() {
                  _sensitivity = value;
                });
              },
            ),
          ),
          ListTile(
            title: const Text('采样率 (Hz)'),
            subtitle: Slider(
              value: _sampleRate.toDouble(),
              min: 10,
              max: 100,
              divisions: 18,
              label: _sampleRate.toString(),
              onChanged: (value) {
                setState(() {
                  _sampleRate = value.toInt();
                });
                _measurementTimer?.cancel();
                _startMeasurement();
              },
            ),
          ),
          SwitchListTile(
            title: const Text('显示实时图表'),
            value: _showRealTimeGraph,
            onChanged: (value) {
              setState(() {
                _showRealTimeGraph = value;
              });
            },
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

参数说明

  • 灵敏度:调整振动信号的放大倍数,范围0.1-5.0
  • 采样率:数据采集频率,范围10-100Hz
  • 实时图表:控制是否显示实时波形图

记录管理功能

记录列表页面

class RecordsListPage extends StatelessWidget {
  final List<MeasurementRecord> records;

  const RecordsListPage({super.key, required this.records});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('测量记录'),
      ),
      body: records.isEmpty
          ? const Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.history, size: 64, color: Colors.grey),
                  SizedBox(height: 16),
                  Text('暂无测量记录', style: TextStyle(color: Colors.grey)),
                ],
              ),
            )
          : ListView.builder(
              padding: const EdgeInsets.all(16),
              itemCount: records.length,
              itemBuilder: (context, index) {
                final record = records[index];
                return Card(
                  margin: const EdgeInsets.only(bottom: 8),
                  child: ListTile(
                    leading: CircleAvatar(
                      backgroundColor: Colors.blue.withValues(alpha: 0.1),
                      child: const Icon(Icons.analytics, color: Colors.blue),
                    ),
                    title: Text(
                      '${_formatDateTime(record.startTime)}',
                      style: const TextStyle(fontWeight: FontWeight.w500),
                    ),
                    subtitle: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text('时长: ${record.durationText}'),
                        Text('最大振幅: ${record.maxMagnitude.toStringAsFixed(3)} m/s²'),
                        if (record.location.isNotEmpty)
                          Text('位置: ${record.location}'),
                      ],
                    ),
                    trailing: const Icon(Icons.chevron_right),
                    onTap: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => RecordDetailPage(record: record),
                        ),
                      );
                    },
                  ),
                );
              },
            ),
    );
  }
}

记录详情页面

记录详情页面展示完整的测量数据和分析结果:

class RecordDetailPage extends StatelessWidget {
  final MeasurementRecord record;

  const RecordDetailPage({super.key, required this.record});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('测量详情'),
        actions: [
          IconButton(
            onPressed: () => _shareRecord(context),
            icon: const Icon(Icons.share),
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildInfoCard(),
            const SizedBox(height: 16),
            _buildStatisticsCard(),
            const SizedBox(height: 16),
            _buildGraphCard(),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '基本信息',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),
            _buildInfoRow('开始时间', _formatDateTime(record.startTime)),
            _buildInfoRow('结束时间', _formatDateTime(record.endTime)),
            _buildInfoRow('测量时长', record.durationText),
            _buildInfoRow('数据点数', '${record.dataPoints.length}'),
            if (record.location.isNotEmpty)
              _buildInfoRow('测量位置', record.location),
            if (record.notes.isNotEmpty)
              _buildInfoRow('备注', record.notes),
          ],
        ),
      ),
    );
  }

  Widget _buildStatisticsCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '统计数据',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: _buildStatItem(
                    '最大值',
                    '${record.maxMagnitude.toStringAsFixed(3)} m/s²',
                    Colors.red,
                  ),
                ),
                Expanded(
                  child: _buildStatItem(
                    '平均值',
                    '${record.avgMagnitude.toStringAsFixed(3)} m/s²',
                    Colors.blue,
                  ),
                ),
                Expanded(
                  child: _buildStatItem(
                    '最小值',
                    '${record.minMagnitude.toStringAsFixed(3)} m/s²',
                    Colors.green,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

详细波形图表

详情页面使用更精细的波形绘制器:

class DetailWaveformPainter extends CustomPainter {
  final List<VibrationData> data;

  DetailWaveformPainter(this.data);

  
  void paint(Canvas canvas, Size size) {
    if (data.isEmpty) return;

    final paint = Paint()
      ..color = Colors.cyan
      ..strokeWidth = 1.5
      ..style = PaintingStyle.stroke;

    final path = Path();
    
    // 找到数据范围
    final minMag = data.map((d) => d.magnitude).reduce(min);
    final maxMag = data.map((d) => d.magnitude).reduce(max);
    final range = maxMag - minMag;
    
    for (int i = 0; i < data.length; i++) {
      final x = (i / (data.length - 1)) * size.width;
      final normalizedY = range > 0 ? (data[i].magnitude - minMag) / range : 0.5;
      final y = size.height - (normalizedY * size.height);
      
      if (i == 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    }

    canvas.drawPath(path, paint);
    
    // 绘制网格线
    final gridPaint = Paint()
      ..color = Colors.grey.withValues(alpha: 0.3)
      ..strokeWidth = 0.5;
    
    // 水平网格线
    for (int i = 1; i < 5; i++) {
      final y = (i / 5) * size.height;
      canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
    }
    
    // 垂直网格线
    for (int i = 1; i < 10; i++) {
      final x = (i / 10) * size.width;
      canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
    }
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

动画效果实现

脉冲动画

振动指示器使用脉冲动画增强视觉效果:

void _initializeAnimations() {
  _pulseController = AnimationController(
    duration: const Duration(milliseconds: 1000),
    vsync: this,
  )..repeat(reverse: true);
  
  _waveController = AnimationController(
    duration: const Duration(milliseconds: 2000),
    vsync: this,
  )..repeat();
}

动画应用

AnimatedBuilder(
  animation: _pulseController,
  builder: (context, child) {
    return Container(
      decoration: BoxDecoration(
        boxShadow: [
          BoxShadow(
            color: _getVibrationColor(magnitude).withValues(alpha: 0.3),
            blurRadius: 20 + normalizedMagnitude * 30,
            spreadRadius: 5 + normalizedMagnitude * 10,
          ),
        ],
      ),
      child: // 振动指示器内容
    );
  },
),

数据处理和统计

统计数据更新

void _updateStatistics(double magnitude) {
  if (magnitude > _maxMagnitude) {
    _maxMagnitude = magnitude;
  }
  if (magnitude < _minMagnitude) {
    _minMagnitude = magnitude;
  }
  
  // 计算平均值
  if (_dataHistory.isNotEmpty) {
    final sum = _dataHistory.fold<double>(0, (sum, data) => sum + data.magnitude);
    _avgMagnitude = sum / _dataHistory.length;
  }
}

数据采样控制

void _startMeasurement() {
  _measurementTimer = Timer.periodic(
    Duration(milliseconds: 1000 ~/ _sampleRate),
    (timer) => _simulateVibrationData(),
  );
}

用户界面优化

响应式设计

应用采用响应式设计,适配不同屏幕尺寸:

// 振动显示器自适应大小
Container(
  width: 200,
  height: 200,
  // 在小屏幕上可以调整为更小的尺寸
)

// 数据卡片使用Expanded平均分布
Row(
  children: [
    Expanded(child: _buildDataCard(...)),
    const SizedBox(width: 8),
    Expanded(child: _buildDataCard(...)),
    const SizedBox(width: 8),
    Expanded(child: _buildDataCard(...)),
  ],
)

颜色主题

应用使用一致的颜色主题:

// 主题色
Colors.blue - 主要UI元素
Colors.green - 正常状态
Colors.yellow - 警告状态
Colors.orange - 中等警告
Colors.red - 危险状态

// 背景色
Colors.blue.withValues(alpha: 0.1) - 浅色背景
Colors.black - 图表背景
Colors.grey - 辅助信息

图标使用

应用使用Material Design图标:

Icons.sensors - 传感器状态
Icons.fiber_manual_record - 录制状态
Icons.pause - 暂停状态
Icons.play_arrow - 播放/开始
Icons.stop - 停止
Icons.trending_up - 上升趋势
Icons.trending_down - 下降趋势
Icons.show_chart - 图表
Icons.analytics - 分析
Icons.history - 历史记录
Icons.settings - 设置
Icons.share - 分享

性能优化

数据管理

// 限制历史数据数量,避免内存溢出
if (_dataHistory.length > 1000) {
  _dataHistory.removeAt(0);
}

// 实时图表只显示最近100个数据点
CustomPaint(
  painter: WaveformPainter(_dataHistory.take(100).toList()),
)

动画优化

// 使用SingleTickerProviderStateMixin优化动画性能
class _VibrationMeterPageState extends State<VibrationMeterPage>
    with TickerProviderStateMixin {
  
  // 及时释放动画控制器
  
  void dispose() {
    _measurementTimer?.cancel();
    _recordingTimer?.cancel();
    _pulseController.dispose();
    _waveController.dispose();
    super.dispose();
  }
}

绘制优化

// CustomPainter中的优化

bool shouldRepaint(covariant CustomPainter oldDelegate) {
  // 实时波形需要重绘
  return true; // 对于WaveformPainter
  // 静态波形不需要重绘
  return false; // 对于DetailWaveformPainter
}

扩展功能建议

真实传感器集成

在真实设备上可以集成加速度计传感器:

// 添加依赖
dependencies:
  sensors_plus: ^4.0.2

// 使用真实传感器数据
import 'package:sensors_plus/sensors_plus.dart';

void _startRealSensorMeasurement() {
  accelerometerEvents.listen((AccelerometerEvent event) {
    final magnitude = sqrt(event.x * event.x + event.y * event.y + event.z * event.z);
    
    final data = VibrationData(
      x: event.x,
      y: event.y,
      z: event.z,
      magnitude: magnitude,
      timestamp: DateTime.now(),
    );
    
    _processVibrationData(data);
  });
}

数据导出功能

// 添加依赖
dependencies:
  path_provider: ^2.1.1
  share_plus: ^7.2.1

// 导出CSV格式数据
Future<void> _exportToCSV(MeasurementRecord record) async {
  final directory = await getApplicationDocumentsDirectory();
  final file = File('${directory.path}/vibration_${record.id}.csv');
  
  final csvContent = StringBuffer();
  csvContent.writeln('Timestamp,X,Y,Z,Magnitude');
  
  for (final data in record.dataPoints) {
    csvContent.writeln(
      '${data.timestamp.toIso8601String()},'
      '${data.x},${data.y},${data.z},${data.magnitude}'
    );
  }
  
  await file.writeAsString(csvContent.toString());
  
  // 分享文件
  await Share.shareXFiles([XFile(file.path)]);
}

频谱分析

// 添加依赖
dependencies:
  fftea: ^1.0.0

// FFT频谱分析
List<double> _performFFT(List<double> timeData) {
  final fft = FFT(timeData.length);
  final complexData = timeData.map((x) => Complex(x, 0)).toList();
  final result = fft.run(complexData);
  
  return result.map((c) => c.magnitude).toList();
}

阈值报警

// 添加振动阈值监测
class VibrationAlarm {
  static const double WARNING_THRESHOLD = 10.5;
  static const double DANGER_THRESHOLD = 12.0;
  
  static void checkThreshold(double magnitude) {
    if (magnitude > DANGER_THRESHOLD) {
      _showDangerAlert();
    } else if (magnitude > WARNING_THRESHOLD) {
      _showWarningAlert();
    }
  }
  
  static void _showDangerAlert() {
    // 显示危险警报
    // 可以添加声音、振动等提醒
  }
  
  static void _showWarningAlert() {
    // 显示警告提醒
  }
}

总结

本教程完整展示了如何使用Flutter开发一个功能丰富的简易测振仪应用。应用包含了实时振动监测、数据记录、波形显示、统计分析等专业功能,采用了现代化的UI设计和流畅的用户体验。

主要特点

  1. 专业功能:完整的振动监测和数据分析功能
  2. 直观界面:仿真专业设备的用户界面设计
  3. 实时显示:动态振动指示器和实时波形图
  4. 数据管理:完整的测量记录管理系统
  5. 可扩展性:便于集成真实传感器和高级功能

技术亮点

  • 自定义绘制:使用CustomPainter绘制专业波形图
  • 动画效果:脉冲动画增强视觉反馈
  • 数据模拟:数学函数模拟真实振动信号
  • 状态管理:完整的测量状态控制
  • 性能优化:合理的数据管理和绘制优化

学习收获

通过本教程,你将掌握:

  • Flutter自定义绘制技术
  • 动画控制和视觉效果
  • 数据采集和处理方法
  • 专业应用界面设计
  • 性能优化技巧

这个简易测振仪应用为你提供了一个完整的Flutter专业应用开发实践案例,可以作为学习Flutter高级功能的重要参考,也可以作为实际工程应用的基础框架。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐