1. 问题现象与背景分析
最近在开发一个基于uni-app的直尺测量工具时,遇到了两个棘手的问题:canvas绘制的刻度线不显示,以及测量结果返回NaN值。这两个问题看似简单,但排查过程却让我踩了不少坑。
先说说这个直尺测量工具的应用场景。在移动端开发中,经常需要实现类似AR测量、图片标注等功能,而canvas绘图是实现这类功能的核心技术。uni-app作为跨端开发框架,虽然提供了canvas组件,但在实际使用中还是会遇到各种兼容性问题。
我遇到的具体表现是:
- 在iOS设备上,canvas绘制的刻度线完全不显示
- 在部分Android设备上,虽然能看到刻度线,但测量结果却显示NaN
- 开发工具上预览正常,但真机调试就出问题
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Canvas刻度不显示问题排查
2.1 基础canvas绘制代码检查
首先检查了最基础的canvas绘制代码。在uni-app中,我们通常这样初始化canvas:
javascript复制const ctx = uni.createCanvasContext('myCanvas', this);
ctx.setStrokeStyle('#000000');
ctx.setLineWidth(1);
ctx.beginPath();
// 绘制刻度线
for(let i=0; i<10; i++){
ctx.moveTo(20, 20+i*10);
ctx.lineTo(30, 20+i*10);
}
ctx.stroke();
ctx.draw();
这段代码在开发工具中运行正常,但在真机上却看不到任何绘制内容。
2.2 真机兼容性问题排查
经过多次测试发现,问题出在canvas的绘制时机上。uni-app的canvas绘制是异步的,需要确保在canvas组件完全加载后再执行绘制操作。修改后的代码如下:
javascript复制onReady() {
this.$nextTick(() => {
setTimeout(() => {
this.drawRuler();
}, 300);
});
}
这个延迟虽然看起来有点"hack",但确实是解决uni-app canvas绘制问题的有效方法。特别是在iOS设备上,必须确保组件完全渲染完成后再进行绘制。
2.3 Canvas尺寸适配问题
另一个常见问题是canvas的尺寸适配。在uni-app中,canvas的width和height需要使用px单位,并且建议通过JS动态计算:
javascript复制const systemInfo = uni.getSystemInfoSync();
this.canvasWidth = systemInfo.windowWidth;
this.canvasHeight = 200; // 固定高度
然后在模板中:
html复制<canvas
canvas-id="myCanvas"
:style="{
width: canvasWidth + 'px',
height: canvasHeight + 'px'
}"
></canvas>
重要提示:千万不要使用rpx单位设置canvas的宽高,这会导致绘制内容缩放异常。
3. 测量结果NaN问题解决
3.1 数据类型检查
测量结果返回NaN,通常是因为进行了非法的数学运算。检查测量计算代码:
javascript复制const distance = Math.sqrt(
Math.pow(endX - startX, 2) +
Math.pow(endY - startY, 2)
);
问题可能出在startX、startY等坐标值上。通过console.log输出发现,这些值有时会是字符串类型,导致计算异常。
3.2 坐标值类型转换
解决方案是对所有坐标值进行强制类型转换:
javascript复制const startX = Number(touchStart.x);
const startY = Number(touchStart.y);
const endX = Number(touchEnd.x);
const endY = Number(touchEnd.y);
3.3 触摸事件坐标转换
另一个常见问题是触摸事件的坐标系统。在uni-app中,canvas的触摸事件返回的坐标是相对于整个页面的,而canvas绘制的坐标是相对于canvas自身的。需要进行坐标转换:
javascript复制uni.createSelectorQuery()
.select('#myCanvas')
.boundingClientRect(rect => {
const canvasX = touchX - rect.left;
const canvasY = touchY - rect.top;
})
.exec();
4. 性能优化与进阶技巧
4.1 双Canvas绘制策略
为了提高绘制性能,可以采用双Canvas策略:一个Canvas用于静态内容(如刻度线),另一个Canvas用于动态内容(如测量线)。这样可以减少重绘区域,提升性能。
html复制<canvas canvas-id="staticCanvas"></canvas>
<canvas canvas-id="dynamicCanvas"></canvas>
4.2 使用离屏Canvas
对于复杂的刻度绘制,可以先用离屏Canvas绘制好,然后通过drawImage绘制到主Canvas上:
javascript复制// 创建离屏Canvas
const offScreenCanvas = uni.createOffscreenCanvas();
const offCtx = offScreenCanvas.getContext('2d');
// 在离屏Canvas上绘制刻度
// ...
// 绘制到主Canvas
ctx.drawImage(offScreenCanvas, 0, 0);
4.3 解决Canvas层级问题
在小程序中,canvas的z-index总是最高,无法被其他组件覆盖。解决方案是使用cover-view覆盖,或者使用uni-app的subNVue功能。
5. 完整代码示例
以下是经过优化的完整直尺测量工具实现:
javascript复制export default {
data() {
return {
canvasWidth: 0,
canvasHeight: 200,
startPoint: null,
endPoint: null
}
},
onReady() {
this.initCanvas();
},
methods: {
initCanvas() {
const systemInfo = uni.getSystemInfoSync();
this.canvasWidth = systemInfo.windowWidth;
this.$nextTick(() => {
setTimeout(() => {
this.drawStaticContent();
}, 300);
});
},
drawStaticContent() {
const ctx = uni.createCanvasContext('staticCanvas', this);
// 绘制刻度线
ctx.setStrokeStyle('#333333');
ctx.setLineWidth(1);
ctx.beginPath();
for(let i=0; i<this.canvasHeight/10; i++){
const y = 20 + i*10;
ctx.moveTo(20, y);
ctx.lineTo(i % 5 === 0 ? 35 : 30, y);
if(i % 5 === 0) {
ctx.setFontSize(10);
ctx.fillText(i/5+'cm', 40, y+3);
}
}
ctx.stroke();
ctx.draw();
},
handleTouchStart(e) {
uni.createSelectorQuery()
.select('#staticCanvas')
.boundingClientRect(rect => {
this.startPoint = {
x: Number(e.touches[0].clientX - rect.left),
y: Number(e.touches[0].clientY - rect.top)
};
})
.exec();
},
handleTouchMove(e) {
uni.createSelectorQuery()
.select('#staticCanvas')
.boundingClientRect(rect => {
this.endPoint = {
x: Number(e.touches[0].clientX - rect.left),
y: Number(e.touches[0].clientY - rect.top)
};
this.drawMeasurement();
})
.exec();
},
drawMeasurement() {
const ctx = uni.createCanvasContext('dynamicCanvas', this);
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
if(this.startPoint && this.endPoint) {
// 绘制测量线
ctx.setStrokeStyle('#FF0000');
ctx.setLineWidth(2);
ctx.beginPath();
ctx.moveTo(this.startPoint.x, this.startPoint.y);
ctx.lineTo(this.endPoint.x, this.endPoint.y);
ctx.stroke();
// 计算并显示距离
const distance = Math.sqrt(
Math.pow(this.endPoint.x - this.startPoint.x, 2) +
Math.pow(this.endPoint.y - this.startPoint.y, 2)
);
const cm = (distance / 37.7952755906).toFixed(1); // 像素转厘米
ctx.setFontSize(12);
ctx.fillText(`${cm} cm`,
(this.startPoint.x + this.endPoint.x)/2,
(this.startPoint.y + this.endPoint.y)/2 - 10);
}
ctx.draw();
}
}
}
6. 常见问题与解决方案
6.1 真机调试时canvas不显示
- 确保canvas组件在页面结构中正确声明
- 检查canvas的width和height是否设置正确
- 尝试增加绘制延迟,确保组件完全加载
6.2 测量结果不准确
- 检查坐标转换是否正确
- 确认设备像素比(DPI)是否考虑在内
- 测试不同设备上的表现,可能需要设备特定的校准
6.3 绘制内容闪烁
- 使用双缓冲技术:先在离屏Canvas绘制,再一次性绘制到主Canvas
- 减少不必要的重绘
- 对于静态内容,绘制一次后缓存起来
在实际项目中,我还发现不同厂商的Android设备对canvas的支持程度差异很大。特别是某些低端机型,canvas的绘制性能较差。针对这种情况,我有几个优化建议:
- 简化绘制内容,减少不必要的图形元素
- 对于复杂的刻度绘制,可以考虑使用图片替代
- 合理使用setTimeout分帧绘制,避免卡顿
经过这些优化后,我们的直尺测量工具在各种设备上都能稳定运行,测量结果也准确可靠。希望这些经验能帮助到遇到类似问题的开发者。
