画布组件
画布组件是 uni-app 提供的用于绘制图形的组件,可用于实现各种自定义绘图、图表、签名等功能。
canvas 画布
canvas
组件提供了一个画布,开发者可以使用 JavaScript 在上面绘制各种图形。
属性说明
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
canvas-id | String | canvas 组件的唯一标识符,必填 | |
disable-scroll | Boolean | false | 当在 canvas 中移动时,是否禁止页面滚动 |
type | String | 2d | 指定 canvas 类型,支持 2d 和 webgl |
width | Number | canvas 宽度,单位为 px | |
height | Number | canvas 高度,单位为 px |
事件说明
事件名 | 说明 | 返回值 |
---|---|---|
@touchstart | 手指触摸动作开始时触发 | event |
@touchmove | 手指触摸后移动时触发 | event |
@touchend | 手指触摸动作结束时触发 | event |
@touchcancel | 手指触摸动作被打断时触发 | event |
@longtap | 手指长按 500ms 之后触发 | event |
@error | 发生错误时触发 | event |
示例代码
基础绘图
vue
<template>
<view class="container">
<canvas
canvas-id="myCanvas"
:width="canvasWidth"
:height="canvasHeight"
:disable-scroll="true"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
style="width: 100%; height: 300px; background-color: #f1f1f1;"
></canvas>
<view class="canvas-tools">
<view class="tool-item" v-for="(color, index) in colors" :key="index">
<view
class="color-block"
:style="{ backgroundColor: color }"
:class="{ active: currentColor === color }"
@click="selectColor(color)"
></view>
</view>
<view class="tool-item">
<slider
:value="lineWidth"
:min="1"
:max="20"
:step="1"
show-value
@change="changeLineWidth"
></slider>
</view>
<view class="tool-actions">
<button type="default" @click="clearCanvas">清空画布</button>
<button type="primary" @click="saveCanvas">保存图片</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
canvasWidth: 300,
canvasHeight: 200,
canvasContext: null,
lastX: 0,
lastY: 0,
isTouching: false,
lineWidth: 5,
currentColor: '#000000',
colors: ['#000000', '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff']
}
},
onReady() {
// 获取设备信息以设置 canvas 大小
const sysInfo = uni.getSystemInfoSync();
this.canvasWidth = sysInfo.windowWidth;
this.canvasHeight = 300;
// 获取 canvas 上下文
this.canvasContext = uni.createCanvasContext('myCanvas', this);
// 设置默认样式
this.canvasContext.setStrokeStyle(this.currentColor);
this.canvasContext.setLineWidth(this.lineWidth);
this.canvasContext.setLineCap('round');
this.canvasContext.setLineJoin('round');
},
methods: {
// 触摸开始事件
touchStart(e) {
const touch = e.touches[0];
this.lastX = touch.x;
this.lastY = touch.y;
this.isTouching = true;
},
// 触摸移动事件
touchMove(e) {
if (!this.isTouching) return;
const touch = e.touches[0];
const x = touch.x;
const y = touch.y;
// 绘制线条
this.canvasContext.beginPath();
this.canvasContext.moveTo(this.lastX, this.lastY);
this.canvasContext.lineTo(x, y);
this.canvasContext.stroke();
this.canvasContext.draw(true);
// 更新最后的坐标
this.lastX = x;
this.lastY = y;
},
// 触摸结束事件
touchEnd() {
this.isTouching = false;
},
// 选择颜色
selectColor(color) {
this.currentColor = color;
this.canvasContext.setStrokeStyle(color);
},
// 改变线宽
changeLineWidth(e) {
this.lineWidth = e.detail.value;
this.canvasContext.setLineWidth(this.lineWidth);
},
// 清空画布
clearCanvas() {
this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.canvasContext.draw();
},
// 保存画布为图片
saveCanvas() {
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: (res) => {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({
title: '保存成功',
icon: 'success'
});
},
fail: (err) => {
console.error('保存失败:', err);
uni.showToast({
title: '保存失败',
icon: 'none'
});
}
});
},
fail: (err) => {
console.error('导出失败:', err);
uni.showToast({
title: '导出失败',
icon: 'none'
});
}
}, this);
}
}
}
</script>
<style>
.container {
padding: 15px;
}
.canvas-tools {
margin-top: 20px;
}
.tool-item {
margin-bottom: 15px;
}
.color-block {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 10px;
display: inline-block;
border: 2px solid #ddd;
}
.color-block.active {
border: 2px solid #007AFF;
}
.tool-actions {
display: flex;
justify-content: space-between;
}
.tool-actions button {
width: 48%;
}
</style>
绘制图表
vue
<template>
<view class="container">
<canvas
canvas-id="chartCanvas"
:width="canvasWidth"
:height="canvasHeight"
style="width: 100%; height: 300px; background-color: #ffffff;"
></canvas>
<view class="chart-controls">
<button type="primary" @click="drawBarChart">柱状图</button>
<button type="primary" @click="drawLineChart">折线图</button>
<button type="primary" @click="drawPieChart">饼图</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
canvasWidth: 300,
canvasHeight: 300,
canvasContext: null,
chartData: [
{ name: '一月', value: 55 },
{ name: '二月', value: 66 },
{ name: '三月', value: 78 },
{ name: '四月', value: 95 },
{ name: '五月', value: 110 },
{ name: '六月', value: 130 }
],
colors: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272']
}
},
onReady() {
// 获取设备信息以设置 canvas 大小
const sysInfo = uni.getSystemInfoSync();
this.canvasWidth = sysInfo.windowWidth - 30; // 减去 padding
this.canvasHeight = 300;
// 获取 canvas 上下文
this.canvasContext = uni.createCanvasContext('chartCanvas', this);
// 默认绘制柱状图
this.$nextTick(() => {
this.drawBarChart();
});
},
methods: {
// 绘制柱状图
drawBarChart() {
const ctx = this.canvasContext;
const data = this.chartData;
const width = this.canvasWidth;
const height = this.canvasHeight;
const padding = 40;
const barWidth = (width - padding * 2) / data.length - 10;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 找出最大值
const maxValue = Math.max(...data.map(item => item.value));
// 绘制坐标轴
ctx.beginPath();
ctx.setLineWidth(2);
ctx.setStrokeStyle('#333333');
ctx.moveTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding); // x轴
ctx.moveTo(padding, height - padding);
ctx.lineTo(padding, padding); // y轴
ctx.stroke();
// 绘制柱状图
data.forEach((item, index) => {
const x = padding + index * ((width - padding * 2) / data.length) + 5;
const barHeight = ((height - padding * 2) * item.value) / maxValue;
const y = height - padding - barHeight;
// 绘制柱子
ctx.beginPath();
ctx.setFillStyle(this.colors[index % this.colors.length]);
ctx.fillRect(x, y, barWidth, barHeight);
// 绘制数值
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(item.value.toString(), x + barWidth / 2 - 10, y - 5);
// 绘制标签
ctx.fillText(item.name, x + barWidth / 2 - 10, height - padding + 20);
});
// 绘制 y 轴刻度
for (let i = 0; i <= 5; i++) {
const y = height - padding - (i * (height - padding * 2)) / 5;
const value = Math.round((i * maxValue) / 5);
ctx.beginPath();
ctx.setLineWidth(1);
ctx.setStrokeStyle('#cccccc');
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(value.toString(), padding - 25, y + 5);
}
// 绘制标题
ctx.setFontSize(16);
ctx.setFillStyle('#333333');
ctx.fillText('月度销售数据', width / 2 - 50, 20);
ctx.draw();
},
// 绘制折线图
drawLineChart() {
const ctx = this.canvasContext;
const data = this.chartData;
const width = this.canvasWidth;
const height = this.canvasHeight;
const padding = 40;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 找出最大值
const maxValue = Math.max(...data.map(item => item.value));
// 绘制坐标轴
ctx.beginPath();
ctx.setLineWidth(2);
ctx.setStrokeStyle('#333333');
ctx.moveTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding); // x轴
ctx.moveTo(padding, height - padding);
ctx.lineTo(padding, padding); // y轴
ctx.stroke();
// 绘制折线
ctx.beginPath();
ctx.setLineWidth(2);
ctx.setStrokeStyle('#5470c6');
data.forEach((item, index) => {
const x = padding + index * ((width - padding * 2) / (data.length - 1));
const y = height - padding - ((height - padding * 2) * item.value) / maxValue;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// 绘制数据点
ctx.setFillStyle('#5470c6');
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fill();
// 绘制数值
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(item.value.toString(), x - 10, y - 10);
// 绘制标签
ctx.fillText(item.name, x - 10, height - padding + 20);
});
ctx.stroke();
// 绘制 y 轴刻度
for (let i = 0; i <= 5; i++) {
const y = height - padding - (i * (height - padding * 2)) / 5;
const value = Math.round((i * maxValue) / 5);
ctx.beginPath();
ctx.setLineWidth(1);
ctx.setStrokeStyle('#cccccc');
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(value.toString(), padding - 25, y + 5);
}
// 绘制标题
ctx.setFontSize(16);
ctx.setFillStyle('#333333');
ctx.fillText('月度销售趋势', width / 2 - 50, 20);
ctx.draw();
},
// 绘制饼图
drawPieChart() {
const ctx = this.canvasContext;
const data = this.chartData;
const width = this.canvasWidth;
const height = this.canvasHeight;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 2 - 60;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 计算总和
const total = data.reduce((sum, item) => sum + item.value, 0);
// 绘制饼图
let startAngle = 0;
data.forEach((item, index) => {
const portion = item.value / total;
const endAngle = startAngle + portion * 2 * Math.PI;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.setFillStyle(this.colors[index % this.colors.length]);
ctx.fill();
// 绘制标签线和文字
const midAngle = startAngle + (endAngle - startAngle) / 2;
const labelRadius = radius * 1.2;
const labelX = centerX + Math.cos(midAngle) * labelRadius;
const labelY = centerY + Math.sin(midAngle) * labelRadius;
ctx.beginPath();
ctx.moveTo(centerX + Math.cos(midAngle) * radius, centerY + Math.sin(midAngle) * radius);
ctx.lineTo(labelX, labelY);
ctx.setStrokeStyle('#333333');
ctx.setLineWidth(1);
ctx.stroke();
// 绘制百分比
const percentage = Math.round(portion * 100) + '%';
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(`${item.name}: ${percentage}`, labelX - 20, labelY);
startAngle = endAngle;
});
// 绘制标题
ctx.setFontSize(16);
ctx.setFillStyle('#333333');
ctx.fillText('销售占比分析', width / 2 - 50, 20);
ctx.draw();
}
}
}
</script>
<style>
.container {
padding: 15px;
}
.chart-controls {
display: flex;
justify-content: space-around;
margin-top: 20px;
}
.chart-controls button {
width: 30%;
}
</style>
注意事项
Canvas 的坐标系以左上角为原点 (0, 0),x 轴向右,y 轴向下。
在使用 Canvas 时,需要注意不同平台的兼容性问题,某些 API 可能在特定平台上不可用。
Canvas 绘图是一次性的,如果需要更新画布内容,需要重新绘制整个画布。
使用
draw()
方法时,第一个参数为true
表示保留上一次绘制的结果,为false
或不传表示清空画布再绘制。在小程序中,Canvas 的大小单位是 px,不是 rpx。如果需要适配不同屏幕,可以通过
uni.getSystemInfoSync()
获取设备信息来动态设置 Canvas 的大小。对于复杂的图表需求,建议使用专业的图表库,如 ECharts、F2 等,它们提供了更丰富的功能和更好的性能。
在进行 Canvas 绘图时,应当注意性能问题,避免在一个页面中同时绘制多个复杂的 Canvas。
使用
canvasToTempFilePath
方法可以将 Canvas 导出为图片,但需要注意该方法是异步的,应当在draw()
方法的回调函数中调用。