Flutter 框架跨平台鸿蒙开发 - 屏幕尺子工具应用开发教程
使用CustomPainter实现测量线、标注和网格的绘制。@override// 绘制测量线// 绘制端点圆圈// 绘制距离标签// 绘制距离文本text: '${settingsunitprecision// 绘制测量线 canvas . drawLine(startPoint , endPoint , paint);
Flutter屏幕尺子工具应用开发教程
项目简介
这是一款功能完整的屏幕尺子工具应用,为用户提供精确的屏幕测量功能。应用采用Material Design 3设计风格,支持多种测量单位、智能校准、测量历史记录等功能,界面简洁专业,操作直观便捷。
运行效果图



核心特性
- 精确测量:支持厘米、毫米、英寸、像素、点等多种单位
- 智能校准:基于设备信息自动校准,支持手动精确校准
- 实时显示:实时显示距离、角度、像素等测量信息
- 测量历史:保存测量记录,支持查看、复制、删除操作
- 网格背景:可选显示测量网格和刻度数字
- 个性化设置:自定义尺子颜色、粗细、反馈方式
- 触觉反馈:支持震动和声音反馈增强体验
- 设备信息:显示屏幕尺寸、像素密度等设备参数
- 专业界面:渐变设计和动画效果提升用户体验
技术栈
- Flutter 3.x
- Material Design 3
- CustomPainter 自定义绘制
- 手势识别与处理
- 动画控制器
- 设备信息获取
项目架构
数据模型设计
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设计,为用户提供了完整的屏幕测量解决方案。
技术亮点
- 精确测量算法:基于像素计算和设备校准的高精度测量
- 自定义绘制:使用CustomPainter实现专业的测量界面
- 智能校准系统:自动和手动校准相结合的精度保证
- 丰富的交互体验:手势识别、动画效果、触觉反馈
- 完整的功能闭环:测量、保存、历史、设置的完整流程
学习价值
- CustomPainter的高级应用
- 手势识别和处理技巧
- 设备信息获取和利用
- 动画控制器的使用
- 数据持久化和管理
- 专业工具应用的设计模式
这个项目为Flutter开发者提供了一个完整的工具类应用开发案例,涵盖了从基础UI到高级功能的各个方面,是学习Flutter应用开发的优秀参考。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)