Flutter屏幕尺子工具应用开发教程

项目简介

这是一款功能完整的屏幕尺子工具应用,为用户提供精确的屏幕测量功能。应用采用Material Design 3设计风格,支持多种测量单位、智能校准、测量历史记录等功能,界面简洁专业,操作直观便捷。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

核心特性

  • 精确测量:支持厘米、毫米、英寸、像素、点等多种单位
  • 智能校准:基于设备信息自动校准,支持手动精确校准
  • 实时显示:实时显示距离、角度、像素等测量信息
  • 测量历史:保存测量记录,支持查看、复制、删除操作
  • 网格背景:可选显示测量网格和刻度数字
  • 个性化设置:自定义尺子颜色、粗细、反馈方式
  • 触觉反馈:支持震动和声音反馈增强体验
  • 设备信息:显示屏幕尺寸、像素密度等设备参数
  • 专业界面:渐变设计和动画效果提升用户体验

技术栈

  • Flutter 3.x
  • Material Design 3
  • CustomPainter 自定义绘制
  • 手势识别与处理
  • 动画控制器
  • 设备信息获取

项目架构

RulerHomePage

RulerPage

HistoryPage

CalibrationPage

SettingsPage

MeasurementArea

GridBackground

MeasurementInfo

GestureDetector

MeasurementPainter

PointIndicator

HistoryList

HistoryItem

AutoCalibration

ManualCalibration

DeviceInfo

UnitSettings

AppearanceSettings

FeedbackSettings

MeasurementUnit

RulerSettings

MeasurementResult

GridPainter

MeasurementPainter

数据模型设计

MeasurementUnit(测量单位模型)

class MeasurementUnit {
  final String name;                 // 单位名称(中文)
  final String symbol;               // 单位符号
  final double pixelsPerUnit;        // 每单位像素数
  final int precision;               // 显示精度(小数位数)
}

支持的测量单位

  • 厘米 (cm):37.8 像素/厘米,精度1位小数
  • 毫米 (mm):3.78 像素/毫米,精度0位小数
  • 英寸 (in):96.0 像素/英寸,精度2位小数
  • 像素 (px):1.0 像素/像素,精度0位小数
  • 点 (pt):1.33 像素/点,精度1位小数

RulerSettings(尺子设置模型)

class RulerSettings {
  final MeasurementUnit unit;        // 当前测量单位
  final bool showGrid;               // 是否显示网格
  final bool showNumbers;            // 是否显示刻度数字
  final Color rulerColor;            // 尺子颜色
  final double rulerWidth;           // 尺子线条粗细
  final bool vibrationEnabled;       // 是否启用震动反馈
  final bool soundEnabled;           // 是否启用声音反馈
}

MeasurementResult(测量结果模型)

class MeasurementResult {
  final double distance;             // 测量距离
  final MeasurementUnit unit;        // 测量单位
  final Offset startPoint;           // 起始点坐标
  final Offset endPoint;             // 结束点坐标
  final DateTime timestamp;          // 测量时间
  
  String get formattedDistance;      // 格式化距离显示
  double get angle;                  // 测量角度
}

核心功能实现

1. 手势识别与测量

实现触摸拖拽测量功能,支持实时显示测量结果。

Widget _buildRulerPage() {
  return Column(
    children: [
      _buildRulerHeader(),
      Expanded(
        child: Stack(
          children: [
            // 背景网格
            if (_settings.showGrid) _buildGridBackground(),
            
            // 测量区域
            GestureDetector(
              onPanStart: _onPanStart,
              onPanUpdate: _onPanUpdate,
              onPanEnd: _onPanEnd,
              child: Container(
                width: double.infinity,
                height: double.infinity,
                color: Colors.transparent,
              ),
            ),
            
            // 测量线和标注
            if (_startPoint != null && _endPoint != null)
              CustomPaint(
                painter: MeasurementPainter(
                  startPoint: _startPoint!,
                  endPoint: _endPoint!,
                  settings: _settings,
                  calibrationFactor: _calibrationFactor,
                ),
                size: Size.infinite,
              ),
            
            // 起始点指示器
            if (_startPoint != null) _buildStartPointIndicator(),
          ],
        ),
      ),
      _buildMeasurementInfo(),
    ],
  );
}

手势处理逻辑

void _onPanStart(DragStartDetails details) {
  setState(() {
    _startPoint = details.localPosition;
    _endPoint = details.localPosition;
    _isMeasuring = true;
  });
  
  if (_settings.vibrationEnabled) {
    HapticFeedback.lightImpact(); // 轻触觉反馈
  }
}

void _onPanUpdate(DragUpdateDetails details) {
  setState(() {
    _endPoint = details.localPosition;
  });
}

void _onPanEnd(DragEndDetails details) {
  setState(() {
    _isMeasuring = false;
  });
  
  if (_settings.vibrationEnabled) {
    HapticFeedback.mediumImpact(); // 中等触觉反馈
  }
}

2. 自定义绘制器

使用CustomPainter实现测量线、标注和网格的绘制。

class MeasurementPainter extends CustomPainter {
  final Offset startPoint;
  final Offset endPoint;
  final RulerSettings settings;
  final double calibrationFactor;

  
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = settings.rulerColor
      ..strokeWidth = settings.rulerWidth
      ..style = PaintingStyle.stroke;

    // 绘制测量线
    canvas.drawLine(startPoint, endPoint, paint);

    // 绘制端点圆圈
    final pointPaint = Paint()
      ..color = settings.rulerColor
      ..style = PaintingStyle.fill;

    canvas.drawCircle(startPoint, 6, pointPaint);
    canvas.drawCircle(endPoint, 6, pointPaint);

    // 绘制距离标签
    final dx = endPoint.dx - startPoint.dx;
    final dy = endPoint.dy - startPoint.dy;
    final distance = sqrt(dx * dx + dy * dy);
    final actualDistance = distance / (settings.unit.pixelsPerUnit * calibrationFactor);
    
    final midPoint = Offset(
      (startPoint.dx + endPoint.dx) / 2,
      (startPoint.dy + endPoint.dy) / 2,
    );

    // 绘制距离文本
    final textPainter = TextPainter(
      text: TextSpan(
        text: '${actualDistance.toStringAsFixed(settings.unit.precision)} ${settings.unit.symbol}',
        style: TextStyle(
          color: settings.rulerColor,
          fontSize: 16,
          fontWeight: FontWeight.bold,
          backgroundColor: Colors.white.withValues(alpha: 0.8),
        ),
      ),
      textDirection: TextDirection.ltr,
    );

    textPainter.layout();
    textPainter.paint(canvas, Offset(
      midPoint.dx - textPainter.width / 2,
      midPoint.dy - textPainter.height / 2 - 20,
    ));

    // 绘制角度标识
    if (distance > 50) {
      final angle = atan2(dy, dx);
      final angleText = TextPainter(
        text: TextSpan(
          text: '${(angle * 180 / pi).toStringAsFixed(1)}°',
          style: TextStyle(
            color: settings.rulerColor.withValues(alpha: 0.7),
            fontSize: 12,
            backgroundColor: Colors.white.withValues(alpha: 0.8),
          ),
        ),
        textDirection: TextDirection.ltr,
      );

      angleText.layout();
      angleText.paint(canvas, Offset(
        midPoint.dx - angleText.width / 2,
        midPoint.dy + 25,
      ));
    }
  }

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

3. 网格背景绘制

绘制测量网格和刻度标识,提供视觉参考。

class GridPainter extends CustomPainter {
  final MeasurementUnit unit;
  final double calibrationFactor;
  final bool showNumbers;

  
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.grey.withValues(alpha: 0.3)
      ..strokeWidth = 0.5;

    final unitSize = unit.pixelsPerUnit * calibrationFactor;
    
    // 绘制垂直线
    for (double x = 0; x <= size.width; x += unitSize) {
      canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);

      // 绘制数字标签
      if (showNumbers && x > 0) {
        final value = x / unitSize;
        final textPainter = TextPainter(
          text: TextSpan(
            text: value.toStringAsFixed(unit.precision == 0 ? 0 : 1),
            style: TextStyle(color: Colors.grey.shade600, fontSize: 10),
          ),
          textDirection: TextDirection.ltr,
        );
        textPainter.layout();
        textPainter.paint(canvas, Offset(x + 2, 2));
      }
    }

    // 绘制水平线
    for (double y = 0; y <= size.height; y += unitSize) {
      canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);

      // 绘制数字标签
      if (showNumbers && y > 0) {
        final value = y / unitSize;
        final textPainter = TextPainter(
          text: TextSpan(
            text: value.toStringAsFixed(unit.precision == 0 ? 0 : 1),
            style: TextStyle(color: Colors.grey.shade600, fontSize: 10),
          ),
          textDirection: TextDirection.ltr,
        );
        textPainter.layout();
        textPainter.paint(canvas, Offset(2, y + 2));
      }
    }
  }

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

4. 智能校准系统

基于设备信息自动计算校准系数,提高测量精度。

void _autoCalibrate() {
  final mediaQuery = MediaQuery.of(context);
  final devicePixelRatio = mediaQuery.devicePixelRatio;
  
  // 基于设备像素密度进行校准
  final dpi = 160 * devicePixelRatio;
  final actualPixelsPerCm = dpi / 2.54;
  
  setState(() {
    _calibrationFactor = actualPixelsPerCm / _settings.unit.pixelsPerUnit;
  });
}

校准原理

  • 获取设备像素密度比例
  • 计算实际DPI(每英寸点数)
  • 转换为每厘米像素数
  • 与理论值比较得出校准系数

5. 测量结果计算

实时计算距离、角度等测量参数。

MeasurementResult? _getCurrentMeasurement() {
  if (_startPoint == null || _endPoint == null) return null;
  
  final pixelDistance = _getPixelDistance();
  final distance = pixelDistance / (_settings.unit.pixelsPerUnit * _calibrationFactor);
  
  return MeasurementResult(
    distance: distance,
    unit: _settings.unit,
    startPoint: _startPoint!,
    endPoint: _endPoint!,
    timestamp: DateTime.now(),
  );
}

double _getPixelDistance() {
  if (_startPoint == null || _endPoint == null) return 0.0;
  
  final dx = _endPoint!.dx - _startPoint!.dx;
  final dy = _endPoint!.dy - _startPoint!.dy;
  return sqrt(dx * dx + dy * dy);
}

6. 动画效果

为起始点添加脉冲动画效果,增强视觉反馈。


void initState() {
  super.initState();
  _pulseController = AnimationController(
    duration: const Duration(milliseconds: 1000),
    vsync: this,
  );
  _pulseAnimation = Tween<double>(begin: 0.8, end: 1.2).animate(
    CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
  );
  _pulseController.repeat(reverse: true);
}

Widget _buildStartPointIndicator() {
  return Positioned(
    left: _startPoint!.dx - 15,
    top: _startPoint!.dy - 15,
    child: AnimatedBuilder(
      animation: _pulseAnimation,
      builder: (context, child) {
        return Transform.scale(
          scale: _isMeasuring ? _pulseAnimation.value : 1.0,
          child: Container(
            width: 30, height: 30,
            decoration: BoxDecoration(
              color: _settings.rulerColor.withValues(alpha: 0.3),
              shape: BoxShape.circle,
              border: Border.all(color: _settings.rulerColor, width: 2),
            ),
            child: Icon(Icons.my_location, color: _settings.rulerColor, size: 16),
          ),
        );
      },
    ),
  );
}

7. 测量历史管理

保存和管理测量记录,支持查看、复制、删除操作。

Widget _buildHistoryPage() {
  return Column(
    children: [
      _buildHistoryHeader(),
      Expanded(
        child: _measurementHistory.isEmpty
            ? _buildEmptyHistoryState()
            : ListView.builder(
                padding: const EdgeInsets.all(16),
                itemCount: _measurementHistory.length,
                itemBuilder: (context, index) {
                  final measurement = _measurementHistory[_measurementHistory.length - 1 - index];
                  return _buildHistoryItem(measurement, index);
                },
              ),
      ),
    ],
  );
}

Widget _buildHistoryItem(MeasurementResult measurement, int index) {
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                width: 40, height: 40,
                decoration: BoxDecoration(
                  color: Colors.blue.shade100,
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Center(
                  child: Text('${index + 1}', 
                             style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue.shade700)),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(measurement.formattedDistance, 
                         style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                    Text(_formatDateTime(measurement.timestamp), 
                         style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
                  ],
                ),
              ),
              PopupMenuButton<String>(
                onSelected: (value) {
                  if (value == 'delete') {
                    _deleteMeasurement(measurement);
                  } else if (value == 'copy') {
                    _copyMeasurement(measurement);
                  }
                },
                itemBuilder: (context) => [
                  const PopupMenuItem(value: 'copy', child: Row(children: [Icon(Icons.copy, size: 20), SizedBox(width: 8), Text('复制')])),
                  const PopupMenuItem(value: 'delete', child: Row(children: [Icon(Icons.delete, size: 20, color: Colors.red), SizedBox(width: 8), Text('删除', style: TextStyle(color: Colors.red))])),
                ],
              ),
            ],
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              _buildMeasurementTag('角度', '${measurement.angle.toStringAsFixed(1)}°', Icons.rotate_right, Colors.green),
              const SizedBox(width: 8),
              _buildMeasurementTag('单位', measurement.unit.symbol, Icons.straighten, Colors.blue),
              const SizedBox(width: 8),
              _buildMeasurementTag('像素', '${_calculatePixelDistance(measurement).toStringAsFixed(0)}px', Icons.grid_on, Colors.orange),
            ],
          ),
        ],
      ),
    ),
  );
}

历史记录功能

  • 自动保存测量结果
  • 按时间倒序显示
  • 支持复制到剪贴板
  • 支持单个删除和批量清除
  • 显示详细测量信息

8. 校准页面实现

提供自动校准和手动校准功能,显示设备信息。

Widget _buildCalibrationPage() {
  return Column(
    children: [
      _buildCalibrationHeader(),
      Expanded(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 自动校准卡片
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Icon(Icons.auto_fix_high, color: Colors.orange.shade600),
                          const SizedBox(width: 8),
                          const Text('自动校准', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                        ],
                      ),
                      const SizedBox(height: 12),
                      const Text('基于设备屏幕信息自动计算校准系数,适用于大多数设备。', style: TextStyle(color: Colors.grey)),
                      const SizedBox(height: 16),
                      SizedBox(
                        width: double.infinity,
                        child: ElevatedButton.icon(
                          onPressed: _autoCalibrate,
                          icon: const Icon(Icons.refresh),
                          label: const Text('重新自动校准'),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              
              const SizedBox(height: 16),
              
              // 手动校准卡片
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Icon(Icons.straighten, color: Colors.blue.shade600),
                          const SizedBox(width: 8),
                          const Text('手动校准', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                        ],
                      ),
                      const SizedBox(height: 12),
                      const Text('使用已知长度的物体进行精确校准,获得更高的测量精度。', style: TextStyle(color: Colors.grey)),
                      const SizedBox(height: 16),
                      SizedBox(
                        width: double.infinity,
                        child: OutlinedButton.icon(
                          onPressed: _showManualCalibrationDialog,
                          icon: const Icon(Icons.rule),
                          label: const Text('开始手动校准'),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              
              const SizedBox(height: 16),
              
              // 校准系数调整
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Icon(Icons.settings, color: Colors.green.shade600),
                          const SizedBox(width: 8),
                          const Text('校准系数调整', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                        ],
                      ),
                      const SizedBox(height: 12),
                      Text('当前校准系数: ${_calibrationFactor.toStringAsFixed(3)}', 
                           style: const TextStyle(fontWeight: FontWeight.bold)),
                      const SizedBox(height: 8),
                      Slider(
                        value: _calibrationFactor,
                        min: 0.5, max: 2.0, divisions: 150,
                        label: _calibrationFactor.toStringAsFixed(3),
                        onChanged: (value) { setState(() { _calibrationFactor = value; }); },
                      ),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          TextButton(onPressed: () { setState(() { _calibrationFactor = 0.5; }); }, child: const Text('最小')),
                          TextButton(onPressed: () { setState(() { _calibrationFactor = 1.0; }); }, child: const Text('重置')),
                          TextButton(onPressed: () { setState(() { _calibrationFactor = 2.0; }); }, child: const Text('最大')),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
              
              const SizedBox(height: 16),
              
              // 设备信息
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Icon(Icons.info_outline, color: Colors.blue.shade600),
                          const SizedBox(width: 8),
                          const Text('设备信息', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                        ],
                      ),
                      const SizedBox(height: 12),
                      _buildDeviceInfoRow('屏幕宽度', '${MediaQuery.of(context).size.width.toStringAsFixed(1)} px'),
                      _buildDeviceInfoRow('屏幕高度', '${MediaQuery.of(context).size.height.toStringAsFixed(1)} px'),
                      _buildDeviceInfoRow('像素密度', '${MediaQuery.of(context).devicePixelRatio.toStringAsFixed(2)}'),
                      _buildDeviceInfoRow('DPI', '${(160 * MediaQuery.of(context).devicePixelRatio).toStringAsFixed(0)}'),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    ],
  );
}

9. 设置页面实现

提供个性化设置选项,包括单位、外观、反馈等。

Widget _buildSettingsPage() {
  return Column(
    children: [
      _buildSettingsHeader(),
      Expanded(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            // 测量单位设置
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(Icons.straighten, color: Colors.blue.shade600),
                        const SizedBox(width: 8),
                        const Text('测量单位', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                      ],
                    ),
                    const SizedBox(height: 16),
                    Wrap(
                      spacing: 8, runSpacing: 8,
                      children: _units.map((unit) {
                        final isSelected = unit.symbol == _settings.unit.symbol;
                        return FilterChip(
                          label: Text('${unit.name} (${unit.symbol})'),
                          selected: isSelected,
                          onSelected: (selected) {
                            if (selected) {
                              setState(() { _settings = _settings.copyWith(unit: unit); });
                              _autoCalibrate();
                            }
                          },
                        );
                      }).toList(),
                    ),
                  ],
                ),
              ),
            ),
            
            const SizedBox(height: 16),
            
            // 外观设置
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(Icons.palette, color: Colors.green.shade600),
                        const SizedBox(width: 8),
                        const Text('外观设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                      ],
                    ),
                    const SizedBox(height: 16),
                    SwitchListTile(
                      title: const Text('显示网格'),
                      subtitle: const Text('在背景显示测量网格'),
                      value: _settings.showGrid,
                      onChanged: (value) { setState(() { _settings = _settings.copyWith(showGrid: value); }); },
                    ),
                    SwitchListTile(
                      title: const Text('显示数字'),
                      subtitle: const Text('在网格上显示刻度数字'),
                      value: _settings.showNumbers,
                      onChanged: (value) { setState(() { _settings = _settings.copyWith(showNumbers: value); }); },
                    ),
                    const SizedBox(height: 16),
                    const Text('尺子颜色', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    Wrap(
                      spacing: 8,
                      children: [Colors.blue, Colors.red, Colors.green, Colors.orange, Colors.purple, Colors.teal].map((color) {
                        final isSelected = color.value == _settings.rulerColor.value;
                        return GestureDetector(
                          onTap: () { setState(() { _settings = _settings.copyWith(rulerColor: color); }); },
                          child: Container(
                            width: 40, height: 40,
                            decoration: BoxDecoration(
                              color: color, shape: BoxShape.circle,
                              border: isSelected ? Border.all(color: Colors.black, width: 3) : null,
                            ),
                            child: isSelected ? const Icon(Icons.check, color: Colors.white) : null,
                          ),
                        );
                      }).toList(),
                    ),
                    const SizedBox(height: 16),
                    Text('尺子粗细: ${_settings.rulerWidth.toStringAsFixed(1)}px'),
                    Slider(
                      value: _settings.rulerWidth, min: 1.0, max: 5.0, divisions: 8,
                      label: _settings.rulerWidth.toStringAsFixed(1),
                      onChanged: (value) { setState(() { _settings = _settings.copyWith(rulerWidth: value); }); },
                    ),
                  ],
                ),
              ),
            ),
            
            const SizedBox(height: 16),
            
            // 反馈设置
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Icon(Icons.feedback, color: Colors.orange.shade600),
                        const SizedBox(width: 8),
                        const Text('反馈设置', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                      ],
                    ),
                    const SizedBox(height: 16),
                    SwitchListTile(
                      title: const Text('震动反馈'),
                      subtitle: const Text('测量时提供触觉反馈'),
                      value: _settings.vibrationEnabled,
                      onChanged: (value) { setState(() { _settings = _settings.copyWith(vibrationEnabled: value); }); },
                    ),
                    SwitchListTile(
                      title: const Text('声音反馈'),
                      subtitle: const Text('测量时播放提示音'),
                      value: _settings.soundEnabled,
                      onChanged: (value) { setState(() { _settings = _settings.copyWith(soundEnabled: value); }); },
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    ],
  );
}

10. 工具方法实现

实现复制、删除、格式化等辅助功能。

// 复制测量结果到剪贴板
void _copyMeasurement(MeasurementResult measurement) {
  final text = '距离: ${measurement.formattedDistance}\n'
      '角度: ${measurement.angle.toStringAsFixed(1)}°\n'
      '时间: ${_formatDateTime(measurement.timestamp)}';
  
  Clipboard.setData(ClipboardData(text: text));
  
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('测量结果已复制到剪贴板'), backgroundColor: Colors.blue),
  );
}

// 删除单个测量记录
void _deleteMeasurement(MeasurementResult measurement) {
  setState(() {
    _measurementHistory.remove(measurement);
  });
  
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('测量记录已删除'), backgroundColor: Colors.orange),
  );
}

// 清除所有历史记录
void _clearHistory() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('清除历史记录'),
      content: const Text('确定要清除所有测量历史记录吗?此操作不可撤销。'),
      actions: [
        TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
        ElevatedButton(
          onPressed: () {
            setState(() { _measurementHistory.clear(); });
            Navigator.pop(context);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('历史记录已清除'), backgroundColor: Colors.orange),
            );
          },
          style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
          child: const Text('确定', style: TextStyle(color: Colors.white)),
        ),
      ],
    ),
  );
}

// 格式化日期时间
String _formatDateTime(DateTime dateTime) {
  return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} '
      '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}

// 显示手动校准对话框
void _showManualCalibrationDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('手动校准'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('请按照以下步骤进行手动校准:'),
          const SizedBox(height: 12),
          const Text('1. 准备一个已知长度的物体(如尺子、硬币等)'),
          const Text('2. 将物体放在屏幕上'),
          const Text('3. 测量物体的长度'),
          const Text('4. 输入物体的实际长度进行校准'),
          const SizedBox(height: 16),
          const Text('建议使用标准尺子或硬币进行校准以获得最佳精度。', 
                     style: TextStyle(fontSize: 12, color: Colors.grey, fontStyle: FontStyle.italic)),
        ],
      ),
      actions: [
        TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
        ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
            setState(() { _selectedIndex = 0; }); // 切换到尺子页面
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('请在尺子页面测量已知长度的物体'), backgroundColor: Colors.blue),
            );
          },
          child: const Text('开始校准'),
        ),
      ],
    ),
  );
}

UI组件设计

1. 渐变头部组件

Widget _buildRulerHeader() {
  return Container(
    padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [Colors.blue.shade600, Colors.blue.shade400],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
    ),
    child: Column(
      children: [
        Row(
          children: [
            const Icon(Icons.straighten, color: Colors.white, size: 32),
            const SizedBox(width: 12),
            const Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('屏幕尺子工具', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white)),
                  Text('精确测量屏幕上的距离', style: TextStyle(fontSize: 14, color: Colors.white70)),
                ],
              ),
            ),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
              decoration: BoxDecoration(color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16)),
              child: Text(_settings.unit.symbol, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
            ),
          ],
        ),
        const SizedBox(height: 16),
        Row(
          children: [
            Expanded(child: _buildHeaderCard('当前单位', _settings.unit.name, Icons.straighten)),
            const SizedBox(width: 12),
            Expanded(child: _buildHeaderCard('测量次数', '${_measurementHistory.length}', Icons.analytics)),
          ],
        ),
      ],
    ),
  );
}

2. 信息展示卡片

Widget _buildInfoItem(String label, String value, IconData icon, Color color) {
  return Column(
    children: [
      Container(
        width: 50, height: 50,
        decoration: BoxDecoration(color: color.withValues(alpha: 0.1), shape: BoxShape.circle),
        child: Icon(icon, color: color, size: 24),
      ),
      const SizedBox(height: 8),
      Text(value, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: color)),
      Text(label, style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
    ],
  );
}

3. 测量标签组件

Widget _buildMeasurementTag(String label, String value, IconData icon, Color color) {
  return Container(
    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
    decoration: BoxDecoration(color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12)),
    child: Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(icon, size: 12, color: color),
        const SizedBox(width: 4),
        Text('$label: $value', style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.w500)),
      ],
    ),
  );
}

4. NavigationBar底部导航

NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: (index) { setState(() { _selectedIndex = index; }); },
  destinations: const [
    NavigationDestination(icon: Icon(Icons.straighten_outlined), selectedIcon: Icon(Icons.straighten), label: '尺子'),
    NavigationDestination(icon: Icon(Icons.history_outlined), selectedIcon: Icon(Icons.history), label: '历史'),
    NavigationDestination(icon: Icon(Icons.tune_outlined), selectedIcon: Icon(Icons.tune), label: '校准'),
    NavigationDestination(icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: '设置'),
  ],
)

功能扩展建议

1. 高级测量功能

class AdvancedMeasurementTools {
  // 面积测量
  double calculateArea(List<Offset> points) {
    if (points.length < 3) return 0.0;
    
    double area = 0.0;
    for (int i = 0; i < points.length; i++) {
      final j = (i + 1) % points.length;
      area += points[i].dx * points[j].dy;
      area -= points[j].dx * points[i].dy;
    }
    return area.abs() / 2.0;
  }
  
  // 角度测量
  double calculateAngle(Offset center, Offset point1, Offset point2) {
    final vector1 = Offset(point1.dx - center.dx, point1.dy - center.dy);
    final vector2 = Offset(point2.dx - center.dx, point2.dy - center.dy);
    
    final dot = vector1.dx * vector2.dx + vector1.dy * vector2.dy;
    final det = vector1.dx * vector2.dy - vector1.dy * vector2.dx;
    
    return atan2(det, dot) * 180 / pi;
  }
  
  // 圆形测量
  Widget buildCircleMeasurementTool() {
    return GestureDetector(
      onPanStart: (details) {
        // 开始圆形测量
      },
      onPanUpdate: (details) {
        // 更新圆形半径
      },
      child: CustomPaint(
        painter: CircleMeasurementPainter(),
        size: Size.infinite,
      ),
    );
  }
}

2. 测量数据导出

class MeasurementExporter {
  // 导出为CSV格式
  String exportToCSV(List<MeasurementResult> measurements) {
    final buffer = StringBuffer();
    buffer.writeln('序号,距离,单位,角度,像素,时间');
    
    for (int i = 0; i < measurements.length; i++) {
      final measurement = measurements[i];
      buffer.writeln('${i + 1},${measurement.distance.toStringAsFixed(measurement.unit.precision)},'
          '${measurement.unit.symbol},${measurement.angle.toStringAsFixed(1)},'
          '${_calculatePixelDistance(measurement).toStringAsFixed(0)},'
          '${_formatDateTime(measurement.timestamp)}');
    }
    
    return buffer.toString();
  }
  
  // 导出为JSON格式
  String exportToJSON(List<MeasurementResult> measurements) {
    final data = measurements.map((measurement) => {
      'distance': measurement.distance,
      'unit': measurement.unit.symbol,
      'angle': measurement.angle,
      'pixelDistance': _calculatePixelDistance(measurement),
      'timestamp': measurement.timestamp.toIso8601String(),
      'startPoint': {'x': measurement.startPoint.dx, 'y': measurement.startPoint.dy},
      'endPoint': {'x': measurement.endPoint.dx, 'y': measurement.endPoint.dy},
    }).toList();
    
    return jsonEncode({'measurements': data, 'exportTime': DateTime.now().toIso8601String()});
  }
  
  // 分享测量结果
  void shareMeasurements(List<MeasurementResult> measurements) {
    final text = measurements.map((m) => 
        '距离: ${m.formattedDistance}, 角度: ${m.angle.toStringAsFixed(1)}°'
    ).join('\n');
    
    Share.share(text, subject: '测量结果分享');
  }
}

3. 智能识别功能

class SmartRecognition {
  // 物体边缘检测
  List<Offset> detectEdges(ui.Image image) {
    // 使用图像处理算法检测物体边缘
    // 返回边缘点坐标列表
    return [];
  }
  
  // 自动测量建议
  Widget buildMeasurementSuggestions() {
    return Card(
      child: Column(
        children: [
          const Text('智能测量建议', style: TextStyle(fontWeight: FontWeight.bold)),
          ListTile(
            leading: const Icon(Icons.smartphone),
            title: const Text('手机屏幕'),
            subtitle: const Text('建议使用厘米或英寸单位'),
            onTap: () => _applySuggestion('phone'),
          ),
          ListTile(
            leading: const Icon(Icons.credit_card),
            title: const Text('信用卡'),
            subtitle: const Text('标准尺寸: 8.56cm × 5.4cm'),
            onTap: () => _applySuggestion('card'),
          ),
          ListTile(
            leading: const Icon(Icons.monetization_on),
            title: const Text('硬币'),
            subtitle: const Text('1元硬币直径: 2.5cm'),
            onTap: () => _applySuggestion('coin'),
          ),
        ],
      ),
    );
  }
  
  void _applySuggestion(String type) {
    switch (type) {
      case 'phone':
        // 设置为厘米单位,调整校准
        break;
      case 'card':
        // 提供信用卡标准尺寸参考
        break;
      case 'coin':
        // 提供硬币尺寸参考
        break;
    }
  }
}

4. 增强现实(AR)测量

class ARMeasurement {
  // AR相机预览
  Widget buildARMeasurementView() {
    return Stack(
      children: [
        CameraPreview(_cameraController),
        CustomPaint(
          painter: ARMeasurementPainter(),
          size: Size.infinite,
        ),
        Positioned(
          bottom: 20,
          left: 20,
          right: 20,
          child: _buildARControls(),
        ),
      ],
    );
  }
  
  // AR测量控制
  Widget _buildARControls() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        FloatingActionButton(
          onPressed: _captureARMeasurement,
          child: const Icon(Icons.camera),
        ),
        FloatingActionButton(
          onPressed: _resetARMeasurement,
          child: const Icon(Icons.refresh),
        ),
        FloatingActionButton(
          onPressed: _saveARMeasurement,
          child: const Icon(Icons.save),
        ),
      ],
    );
  }
  
  // 3D空间测量
  void _captureARMeasurement() {
    // 使用ARCore/ARKit进行3D空间测量
    // 计算真实世界坐标
    // 显示3D测量结果
  }
}

5. 云端同步功能

class CloudSync {
  // 上传测量数据
  Future<void> uploadMeasurements(List<MeasurementResult> measurements) async {
    try {
      final data = {
        'measurements': measurements.map((m) => m.toJson()).toList(),
        'deviceInfo': await _getDeviceInfo(),
        'timestamp': DateTime.now().toIso8601String(),
      };
      
      final response = await http.post(
        Uri.parse('https://api.ruler-app.com/measurements'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode(data),
      );
      
      if (response.statusCode == 200) {
        _showSuccessMessage('测量数据已同步到云端');
      }
    } catch (e) {
      _showErrorMessage('同步失败: $e');
    }
  }
  
  // 下载测量数据
  Future<List<MeasurementResult>> downloadMeasurements() async {
    try {
      final response = await http.get(
        Uri.parse('https://api.ruler-app.com/measurements'),
        headers: {'Authorization': 'Bearer ${await _getAuthToken()}'},
      );
      
      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return (data['measurements'] as List)
            .map((json) => MeasurementResult.fromJson(json))
            .toList();
      }
    } catch (e) {
      _showErrorMessage('下载失败: $e');
    }
    
    return [];
  }
  
  // 设备间同步
  Widget buildSyncSettings() {
    return Card(
      child: Column(
        children: [
          SwitchListTile(
            title: const Text('自动同步'),
            subtitle: const Text('自动将测量数据同步到云端'),
            value: _autoSyncEnabled,
            onChanged: (value) {
              setState(() { _autoSyncEnabled = value; });
            },
          ),
          ListTile(
            leading: const Icon(Icons.cloud_upload),
            title: const Text('手动同步'),
            subtitle: const Text('立即同步所有测量数据'),
            onTap: () => uploadMeasurements(_measurementHistory),
          ),
          ListTile(
            leading: const Icon(Icons.cloud_download),
            title: const Text('恢复数据'),
            subtitle: const Text('从云端恢复测量数据'),
            onTap: _restoreFromCloud,
          ),
        ],
      ),
    );
  }
}

6. 专业工具集成

class ProfessionalTools {
  // CAD导入功能
  Widget buildCADImporter() {
    return Card(
      child: Column(
        children: [
          const Text('CAD图纸导入', style: TextStyle(fontWeight: FontWeight.bold)),
          const SizedBox(height: 12),
          ElevatedButton.icon(
            onPressed: _importCADFile,
            icon: const Icon(Icons.upload_file),
            label: const Text('导入DWG/DXF文件'),
          ),
          const SizedBox(height: 8),
          const Text('支持AutoCAD、SolidWorks等格式', style: TextStyle(fontSize: 12, color: Colors.grey)),
        ],
      ),
    );
  }
  
  // 工程计算器
  Widget buildEngineeringCalculator() {
    return Card(
      child: Column(
        children: [
          const Text('工程计算器', style: TextStyle(fontWeight: FontWeight.bold)),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(
                child: ElevatedButton(
                  onPressed: () => _showCalculator('area'),
                  child: const Text('面积计算'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: ElevatedButton(
                  onPressed: () => _showCalculator('volume'),
                  child: const Text('体积计算'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: ElevatedButton(
                  onPressed: () => _showCalculator('angle'),
                  child: const Text('角度计算'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: ElevatedButton(
                  onPressed: () => _showCalculator('scale'),
                  child: const Text('比例换算'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
  
  // 测量报告生成
  Future<void> generateMeasurementReport(List<MeasurementResult> measurements) async {
    final pdf = pw.Document();
    
    pdf.addPage(
      pw.Page(
        build: (pw.Context context) {
          return pw.Column(
            crossAxisAlignment: pw.CrossAxisAlignment.start,
            children: [
              pw.Text('测量报告', style: pw.TextStyle(fontSize: 24, fontWeight: pw.FontWeight.bold)),
              pw.SizedBox(height: 20),
              pw.Text('生成时间: ${DateTime.now().toString()}'),
              pw.SizedBox(height: 20),
              pw.Table(
                border: pw.TableBorder.all(),
                children: [
                  pw.TableRow(
                    children: [
                      pw.Text('序号', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
                      pw.Text('距离', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
                      pw.Text('角度', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
                      pw.Text('时间', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
                    ],
                  ),
                  ...measurements.asMap().entries.map((entry) {
                    final index = entry.key;
                    final measurement = entry.value;
                    return pw.TableRow(
                      children: [
                        pw.Text('${index + 1}'),
                        pw.Text(measurement.formattedDistance),
                        pw.Text('${measurement.angle.toStringAsFixed(1)}°'),
                        pw.Text(_formatDateTime(measurement.timestamp)),
                      ],
                    );
                  }),
                ],
              ),
            ],
          );
        },
      ),
    );
    
    final bytes = await pdf.save();
    await _savePDFFile(bytes, 'measurement_report.pdf');
  }
}

7. 多语言支持

class LocalizationManager {
  static const Map<String, Map<String, String>> _localizedValues = {
    'en': {
      'app_title': 'Screen Ruler Tool',
      'ruler': 'Ruler',
      'history': 'History',
      'calibration': 'Calibration',
      'settings': 'Settings',
      'distance': 'Distance',
      'angle': 'Angle',
      'pixels': 'Pixels',
      'save_measurement': 'Save Measurement',
      'clear': 'Clear',
      'auto_calibration': 'Auto Calibration',
      'manual_calibration': 'Manual Calibration',
      'measurement_units': 'Measurement Units',
      'appearance_settings': 'Appearance Settings',
      'feedback_settings': 'Feedback Settings',
    },
    'zh': {
      'app_title': '屏幕尺子工具',
      'ruler': '尺子',
      'history': '历史',
      'calibration': '校准',
      'settings': '设置',
      'distance': '距离',
      'angle': '角度',
      'pixels': '像素',
      'save_measurement': '保存测量',
      'clear': '清除',
      'auto_calibration': '自动校准',
      'manual_calibration': '手动校准',
      'measurement_units': '测量单位',
      'appearance_settings': '外观设置',
      'feedback_settings': '反馈设置',
    },
  };
  
  static String translate(String key, String locale) {
    return _localizedValues[locale]?[key] ?? key;
  }
  
  Widget buildLanguageSelector() {
    return Card(
      child: Column(
        children: [
          const Text('语言设置', style: TextStyle(fontWeight: FontWeight.bold)),
          RadioListTile<String>(
            title: const Text('中文'),
            value: 'zh',
            groupValue: _currentLocale,
            onChanged: (value) => _changeLocale(value!),
          ),
          RadioListTile<String>(
            title: const Text('English'),
            value: 'en',
            groupValue: _currentLocale,
            onChanged: (value) => _changeLocale(value!),
          ),
        ],
      ),
    );
  }
}

8. 无障碍功能

class AccessibilityFeatures {
  // 语音播报
  Widget buildVoiceAnnouncement() {
    return Card(
      child: Column(
        children: [
          SwitchListTile(
            title: const Text('语音播报'),
            subtitle: const Text('测量时语音播报结果'),
            value: _voiceEnabled,
            onChanged: (value) {
              setState(() { _voiceEnabled = value; });
            },
          ),
          SwitchListTile(
            title: const Text('高对比度'),
            subtitle: const Text('提高界面对比度'),
            value: _highContrastEnabled,
            onChanged: (value) {
              setState(() { _highContrastEnabled = value; });
            },
          ),
          SwitchListTile(
            title: const Text('大字体'),
            subtitle: const Text('使用更大的字体显示'),
            value: _largeTextEnabled,
            onChanged: (value) {
              setState(() { _largeTextEnabled = value; });
            },
          ),
        ],
      ),
    );
  }
  
  // 语音播报测量结果
  void _announceMeasurement(MeasurementResult measurement) {
    if (_voiceEnabled) {
      final text = '测量距离 ${measurement.formattedDistance},角度 ${measurement.angle.toStringAsFixed(1)} 度';
      _textToSpeech.speak(text);
    }
  }
  
  // 触觉导航
  void _provideTactileGuidance(Offset position) {
    if (_tactileGuidanceEnabled) {
      // 根据位置提供不同强度的震动反馈
      final intensity = _calculateVibrationIntensity(position);
      HapticFeedback.vibrate();
    }
  }
}

性能优化建议

1. 绘制性能优化

class OptimizedPainter extends CustomPainter {
  final Path _cachedPath = Path();
  bool _pathCached = false;
  
  
  void paint(Canvas canvas, Size size) {
    // 使用缓存路径减少计算
    if (!_pathCached) {
      _buildPath();
      _pathCached = true;
    }
    
    // 使用图层减少重绘
    canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint());
    
    // 批量绘制操作
    _batchDrawOperations(canvas);
    
    canvas.restore();
  }
  
  void _buildPath() {
    _cachedPath.reset();
    // 构建复杂路径
  }
  
  void _batchDrawOperations(Canvas canvas) {
    // 批量执行绘制操作以提高性能
  }
  
  
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // 精确控制重绘条件
    return oldDelegate != this;
  }
}

2. 内存管理优化

class MemoryOptimizedRuler {
  final List<MeasurementResult> _measurements = [];
  static const int _maxHistorySize = 1000;
  
  void addMeasurement(MeasurementResult measurement) {
    _measurements.add(measurement);
    
    // 限制历史记录数量
    if (_measurements.length > _maxHistorySize) {
      _measurements.removeAt(0);
    }
  }
  
  
  void dispose() {
    // 清理资源
    _pulseController.dispose();
    _measurements.clear();
    super.dispose();
  }
}

3. 响应性能优化

class ResponsiveRuler {
  Timer? _debounceTimer;
  
  void _onPanUpdateDebounced(DragUpdateDetails details) {
    _debounceTimer?.cancel();
    _debounceTimer = Timer(const Duration(milliseconds: 16), () {
      setState(() {
        _endPoint = details.localPosition;
      });
    });
  }
  
  
  void dispose() {
    _debounceTimer?.cancel();
    super.dispose();
  }
}

测试建议

1. 单元测试

// test/measurement_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:ruler_app/models/measurement_result.dart';

void main() {
  group('MeasurementResult Tests', () {
    test('should calculate distance correctly', () {
      final result = MeasurementResult(
        distance: 5.0,
        unit: MeasurementUnit(name: '厘米', symbol: 'cm', pixelsPerUnit: 37.8, precision: 1),
        startPoint: const Offset(0, 0),
        endPoint: const Offset(100, 0),
        timestamp: DateTime.now(),
      );
      
      expect(result.formattedDistance, equals('5.0 cm'));
    });
    
    test('should calculate angle correctly', () {
      final result = MeasurementResult(
        distance: 5.0,
        unit: MeasurementUnit(name: '厘米', symbol: 'cm', pixelsPerUnit: 37.8, precision: 1),
        startPoint: const Offset(0, 0),
        endPoint: const Offset(100, 100),
        timestamp: DateTime.now(),
      );
      
      expect(result.angle, closeTo(45.0, 0.1));
    });
  });
}

2. Widget测试

// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ruler_app/main.dart';

void main() {
  group('Ruler App Widget Tests', () {
    testWidgets('should display navigation bar with 4 tabs', (WidgetTester tester) async {
      await tester.pumpWidget(const RulerApp());
      
      expect(find.byType(NavigationBar), findsOneWidget);
      expect(find.text('尺子'), findsOneWidget);
      expect(find.text('历史'), findsOneWidget);
      expect(find.text('校准'), findsOneWidget);
      expect(find.text('设置'), findsOneWidget);
    });
    
    testWidgets('should navigate between tabs correctly', (WidgetTester tester) async {
      await tester.pumpWidget(const RulerApp());
      
      // 点击历史标签
      await tester.tap(find.text('历史'));
      await tester.pumpAndSettle();
      
      expect(find.text('测量历史'), findsOneWidget);
      
      // 点击设置标签
      await tester.tap(find.text('设置'));
      await tester.pumpAndSettle();
      
      expect(find.text('测量单位'), findsOneWidget);
    });
  });
}

3. 集成测试

// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:ruler_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  group('Ruler App Integration Tests', () {
    testWidgets('complete measurement flow test', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();
      
      // 1. 验证应用启动
      expect(find.text('屏幕尺子工具'), findsOneWidget);
      
      // 2. 执行测量操作
      final measurementArea = find.byType(GestureDetector).first;
      await tester.dragFrom(const Offset(100, 100), const Offset(200, 200));
      await tester.pumpAndSettle();
      
      // 3. 验证测量结果显示
      expect(find.text('距离'), findsOneWidget);
      expect(find.text('角度'), findsOneWidget);
      
      // 4. 保存测量结果
      await tester.tap(find.text('保存测量'));
      await tester.pumpAndSettle();
      
      // 5. 切换到历史页面
      await tester.tap(find.text('历史'));
      await tester.pumpAndSettle();
      
      // 6. 验证历史记录
      expect(find.byType(Card), findsAtLeastNWidgets(1));
      
      // 7. 测试设置功能
      await tester.tap(find.text('设置'));
      await tester.pumpAndSettle();
      
      // 8. 切换测量单位
      await tester.tap(find.text('毫米 (mm)'));
      await tester.pumpAndSettle();
      
      // 9. 验证单位切换成功
      await tester.tap(find.text('尺子'));
      await tester.pumpAndSettle();
      expect(find.text('mm'), findsOneWidget);
    });
  });
}

部署指南

1. Android部署

# 构建APK
flutter build apk --release

# 构建App Bundle(推荐用于Google Play)
flutter build appbundle --release

# 安装到设备
flutter install

2. iOS部署

# 构建iOS应用
flutter build ios --release

# 使用Xcode打开项目进行签名和发布
open ios/Runner.xcworkspace

3. 应用图标和启动页

# pubspec.yaml
dev_dependencies:
  flutter_launcher_icons: ^0.13.1
  flutter_native_splash: ^2.3.2

flutter_icons:
  android: true
  ios: true
  image_path: "assets/icon/ruler_icon.png"
  adaptive_icon_background: "#2196F3"
  adaptive_icon_foreground: "assets/icon/ruler_foreground.png"

flutter_native_splash:
  color: "#2196F3"
  image: "assets/splash/ruler_splash.png"
  android_12:
    image: "assets/splash/ruler_splash_android12.png"
    color: "#2196F3"

项目总结

这个屏幕尺子工具应用展示了Flutter在工具类应用开发中的强大能力。通过精确的测量算法、智能的校准系统和专业的UI设计,为用户提供了完整的屏幕测量解决方案。

技术亮点

  1. 精确测量算法:基于像素计算和设备校准的高精度测量
  2. 自定义绘制:使用CustomPainter实现专业的测量界面
  3. 智能校准系统:自动和手动校准相结合的精度保证
  4. 丰富的交互体验:手势识别、动画效果、触觉反馈
  5. 完整的功能闭环:测量、保存、历史、设置的完整流程

学习价值

  • CustomPainter的高级应用
  • 手势识别和处理技巧
  • 设备信息获取和利用
  • 动画控制器的使用
  • 数据持久化和管理
  • 专业工具应用的设计模式

这个项目为Flutter开发者提供了一个完整的工具类应用开发案例,涵盖了从基础UI到高级功能的各个方面,是学习Flutter应用开发的优秀参考。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐