HarmonyOS分布式协作开发实战:多人协同白板
会议室里,每个人都在自己的设备上实时编辑同一份白板------有人用平板画图,有人用手机标注,有人用电脑写文字,所有人的操作实时同步,这就是鸿蒙分布式协作的魅力。
一、背景与动机
1.1 传统协作的痛点
传统会议协作真的很低效:
- 设备孤岛:白板内容只能拍照分享,清晰度差、无法编辑
- 版本混乱:你改一版、我改一版,最后不知道哪个是最终版
- 同步延迟:在线白板工具延迟大,画一笔等半天才能看到
- 离线无助:网络一断,所有协作内容丢失
- 权限失控:谁都能改,谁都能删,内容安全没保障
1.2 鸿蒙分布式协作的愿景
鸿蒙让协作体验焕然一新:
实时同步:毫秒级延迟,所见即所得。
离线可用:断网也能编辑,联网自动同步。
多端协同:手机、平板、电脑同时参与,各展所长。
安全可控:细粒度权限,内容全程加密。
#mermaid-svg-SxvRBH7NxCDrDqdV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SxvRBH7NxCDrDqdV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SxvRBH7NxCDrDqdV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SxvRBH7NxCDrDqdV .error-icon{fill:#552222;}#mermaid-svg-SxvRBH7NxCDrDqdV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SxvRBH7NxCDrDqdV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SxvRBH7NxCDrDqdV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SxvRBH7NxCDrDqdV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SxvRBH7NxCDrDqdV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SxvRBH7NxCDrDqdV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SxvRBH7NxCDrDqdV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SxvRBH7NxCDrDqdV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SxvRBH7NxCDrDqdV .marker.cross{stroke:#333333;}#mermaid-svg-SxvRBH7NxCDrDqdV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SxvRBH7NxCDrDqdV p{margin:0;}#mermaid-svg-SxvRBH7NxCDrDqdV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SxvRBH7NxCDrDqdV .cluster-label text{fill:#333;}#mermaid-svg-SxvRBH7NxCDrDqdV .cluster-label span{color:#333;}#mermaid-svg-SxvRBH7NxCDrDqdV .cluster-label span p{background-color:transparent;}#mermaid-svg-SxvRBH7NxCDrDqdV .label text,#mermaid-svg-SxvRBH7NxCDrDqdV span{fill:#333;color:#333;}#mermaid-svg-SxvRBH7NxCDrDqdV .node rect,#mermaid-svg-SxvRBH7NxCDrDqdV .node circle,#mermaid-svg-SxvRBH7NxCDrDqdV .node ellipse,#mermaid-svg-SxvRBH7NxCDrDqdV .node polygon,#mermaid-svg-SxvRBH7NxCDrDqdV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SxvRBH7NxCDrDqdV .rough-node .label text,#mermaid-svg-SxvRBH7NxCDrDqdV .node .label text,#mermaid-svg-SxvRBH7NxCDrDqdV .image-shape .label,#mermaid-svg-SxvRBH7NxCDrDqdV .icon-shape .label{text-anchor:middle;}#mermaid-svg-SxvRBH7NxCDrDqdV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SxvRBH7NxCDrDqdV .rough-node .label,#mermaid-svg-SxvRBH7NxCDrDqdV .node .label,#mermaid-svg-SxvRBH7NxCDrDqdV .image-shape .label,#mermaid-svg-SxvRBH7NxCDrDqdV .icon-shape .label{text-align:center;}#mermaid-svg-SxvRBH7NxCDrDqdV .node.clickable{cursor:pointer;}#mermaid-svg-SxvRBH7NxCDrDqdV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SxvRBH7NxCDrDqdV .arrowheadPath{fill:#333333;}#mermaid-svg-SxvRBH7NxCDrDqdV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SxvRBH7NxCDrDqdV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SxvRBH7NxCDrDqdV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SxvRBH7NxCDrDqdV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SxvRBH7NxCDrDqdV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SxvRBH7NxCDrDqdV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SxvRBH7NxCDrDqdV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SxvRBH7NxCDrDqdV .cluster text{fill:#333;}#mermaid-svg-SxvRBH7NxCDrDqdV .cluster span{color:#333;}#mermaid-svg-SxvRBH7NxCDrDqdV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SxvRBH7NxCDrDqdV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SxvRBH7NxCDrDqdV rect.text{fill:none;stroke-width:0;}#mermaid-svg-SxvRBH7NxCDrDqdV .icon-shape,#mermaid-svg-SxvRBH7NxCDrDqdV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SxvRBH7NxCDrDqdV .icon-shape p,#mermaid-svg-SxvRBH7NxCDrDqdV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SxvRBH7NxCDrDqdV .icon-shape .label rect,#mermaid-svg-SxvRBH7NxCDrDqdV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SxvRBH7NxCDrDqdV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SxvRBH7NxCDrDqdV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SxvRBH7NxCDrDqdV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-SxvRBH7NxCDrDqdV .primary>*{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;}#mermaid-svg-SxvRBH7NxCDrDqdV .primary span{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;}#mermaid-svg-SxvRBH7NxCDrDqdV .info>*{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:2px!important;}#mermaid-svg-SxvRBH7NxCDrDqdV .info span{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:2px!important;} 协作流程
绘制图形
添加标注
编辑文字
实时同步
实时同步
实时同步
用户A-平板
分布式白板
用户B-手机
用户C-电脑
二、核心原理
2.1 协同编辑数据模型
白板内容由多个图元组成:
typescript
// 白板图元
interface WhiteboardElement {
id: string; // 图元ID
type: ElementType; // 图元类型
x: number; // X坐标
y: number; // Y坐标
width: number; // 宽度
height: number; // 高度
rotation: number; // 旋转角度
zIndex: number; // 层级
properties: Record<string, any>; // 类型特定属性
createdBy: string; // 创建者
createdAt: number; // 创建时间
updatedBy: string; // 最后修改者
updatedAt: number; // 最后修改时间
version: number; // 版本号
}
// 图元类型
enum ElementType {
PATH = 'path', // 路径(手绘线条)
TEXT = 'text', // 文本
IMAGE = 'image', // 图片
SHAPE = 'shape', // 形状(矩形、圆形等)
STICKY = 'sticky', // 便签
LINE = 'line', // 直线/箭头
ERASER = 'eraser' // 橡皮擦痕迹
}
2.2 操作转换算法
多人同时编辑需要解决冲突:
渲染错误: Mermaid 渲染失败: Parse error on line 14: ...lassDef primary fill:#e1f5fe,stroke:#015 -----------------------^ Expecting '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT'
2.3 同步策略
增量同步:只同步变化部分,减少数据传输。
批量合并:短时间内多个操作合并发送。
冲突解决:基于时间戳和版本号的自动解决。
三、代码实战
3.1 分布式白板核心实现
typescript
// DistributedWhiteboard.ets
import { distributedData } from '@kit.ArkData';
import canvas from '@ohos.graphics.canvas2d';
// 白板状态
interface WhiteboardState {
whiteboardId: string;
elements: Map<string, WhiteboardElement>;
selectedElement: string | null;
viewport: {
x: number;
y: number;
scale: number;
};
collaborators: Collaborator[];
}
// 协作者
interface Collaborator {
userId: string;
userName: string;
deviceId: string;
cursor: { x: number; y: number };
selection: string | null;
color: string;
}
@Entry
@Component
struct DistributedWhiteboard {
// 白板状态
@State elements: WhiteboardElement[] = [];
@State selectedElement: string | null = null;
@State viewport = { x: 0, y: 0, scale: 1 };
// 协作者
@State collaborators: Collaborator[] = [];
// 工具状态
@State currentTool: ToolType = ToolType.SELECT;
@State currentColor: string = '#000000';
@State strokeWidth: number = 2;
// 绘制状态
private currentPath: Point[] = [];
private isDrawing: boolean = false;
// 分布式数据
private kvStore: distributedData.KvStore | null = null;
// 画布上下文
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: canvas.CanvasRenderingContext2D = new canvas.CanvasRenderingContext2D(this.settings);
aboutToAppear() {
this.initDistributedData();
this.loadWhiteboard();
}
// 初始化分布式数据
async initDistributedData() {
try {
// 创建分布式数据存储
// const kvManager = distributedData.createKvManager({...});
// this.kvStore = await kvManager.createKvStore('whiteboard', {...});
// 监听数据变化
// this.kvStore.on('dataChange', (data) => {
// this.handleRemoteChange(data);
// });
console.info('分布式数据初始化成功');
} catch (err) {
console.error(`初始化失败: ${JSON.stringify(err)}`);
}
}
// 加载白板
async loadWhiteboard() {
// 从分布式数据库加载图元
// 模拟数据
this.elements = [
{
id: 'elem_001',
type: ElementType.TEXT,
x: 100,
y: 100,
width: 200,
height: 50,
rotation: 0,
zIndex: 1,
properties: {
text: '会议主题',
fontSize: 24,
color: '#333333'
},
createdBy: 'user_001',
createdAt: Date.now(),
updatedBy: 'user_001',
updatedAt: Date.now(),
version: 1
}
];
}
// 添加图元
async addElement(element: WhiteboardElement) {
this.elements.push(element);
await this.syncElement(element);
}
// 更新图元
async updateElement(elementId: string, updates: Partial<WhiteboardElement>) {
const index = this.elements.findIndex(e => e.id === elementId);
if (index >= 0) {
const element = this.elements[index];
Object.assign(element, updates);
element.updatedAt = Date.now();
element.version++;
await this.syncElement(element);
}
}
// 删除图元
async deleteElement(elementId: string) {
const index = this.elements.findIndex(e => e.id === elementId);
if (index >= 0) {
this.elements.splice(index, 1);
await this.syncDelete(elementId);
}
}
// 同步图元到其他设备
async syncElement(element: WhiteboardElement) {
if (!this.kvStore) {
return;
}
// await this.kvStore.put(element.id, JSON.stringify(element));
}
// 同步删除操作
async syncDelete(elementId: string) {
if (!this.kvStore) {
return;
}
// await this.kvStore.delete(elementId);
}
// 处理远程变化
handleRemoteChange(data: any) {
// 解析变化数据
// 更新本地状态
}
// 处理触摸开始
handleTouchStart(x: number, y: number) {
const transformedX = (x - this.viewport.x) / this.viewport.scale;
const transformedY = (y - this.viewport.y) / this.viewport.scale;
switch (this.currentTool) {
case ToolType.SELECT:
// 选择图元
this.selectElementAt(transformedX, transformedY);
break;
case ToolType.PEN:
// 开始绘制
this.isDrawing = true;
this.currentPath = [{ x: transformedX, y: transformedY }];
break;
case ToolType.ERASER:
// 擦除
this.eraseAt(transformedX, transformedY);
break;
}
}
// 处理触摸移动
handleTouchMove(x: number, y: number) {
const transformedX = (x - this.viewport.x) / this.viewport.scale;
const transformedY = (y - this.viewport.y) / this.viewport.scale;
switch (this.currentTool) {
case ToolType.SELECT:
// 移动选中图元
if (this.selectedElement) {
this.moveElement(this.selectedElement, transformedX, transformedY);
}
break;
case ToolType.PEN:
// 继续绘制
if (this.isDrawing) {
this.currentPath.push({ x: transformedX, y: transformedY });
this.redraw();
}
break;
case ToolType.ERASER:
// 继续擦除
this.eraseAt(transformedX, transformedY);
break;
}
}
// 处理触摸结束
handleTouchEnd() {
switch (this.currentTool) {
case ToolType.PEN:
// 完成绘制,创建路径图元
if (this.isDrawing && this.currentPath.length > 1) {
const element: WhiteboardElement = {
id: `path_${Date.now()}`,
type: ElementType.PATH,
x: 0,
y: 0,
width: 0,
height: 0,
rotation: 0,
zIndex: this.elements.length + 1,
properties: {
points: this.currentPath,
color: this.currentColor,
width: this.strokeWidth
},
createdBy: 'local',
createdAt: Date.now(),
updatedBy: 'local',
updatedAt: Date.now(),
version: 1
};
this.addElement(element);
}
this.isDrawing = false;
this.currentPath = [];
break;
}
}
// 选择图元
selectElementAt(x: number, y: number) {
// 从后往前查找(后绘制的在上面)
for (let i = this.elements.length - 1; i >= 0; i--) {
const element = this.elements[i];
if (this.isPointInElement(x, y, element)) {
this.selectedElement = element.id;
return;
}
}
this.selectedElement = null;
}
// 判断点是否在图元内
isPointInElement(x: number, y: number, element: WhiteboardElement): boolean {
return x >= element.x && x <= element.x + element.width &&
y >= element.y && y <= element.y + element.height;
}
// 移动图元
async moveElement(elementId: string, x: number, y: number) {
await this.updateElement(elementId, { x, y });
}
// 擦除
eraseAt(x: number, y: number) {
// 查找并删除擦除位置的图元
for (let i = this.elements.length - 1; i >= 0; i--) {
const element = this.elements[i];
if (this.isPointInElement(x, y, element)) {
this.deleteElement(element.id);
break;
}
}
}
// 重绘
redraw() {
// 清空画布
this.context.clearRect(0, 0, 1920, 1080);
// 应用视口变换
this.context.save();
this.context.translate(this.viewport.x, this.viewport.y);
this.context.scale(this.viewport.scale, this.viewport.scale);
// 绘制所有图元
for (const element of this.elements) {
this.drawElement(element);
}
// 绘制当前路径
if (this.currentPath.length > 1) {
this.drawPath(this.currentPath);
}
// 绘制协作者光标
for (const collaborator of this.collaborators) {
this.drawCursor(collaborator);
}
this.context.restore();
}
// 绘制图元
drawElement(element: WhiteboardElement) {
switch (element.type) {
case ElementType.PATH:
this.drawPath(element.properties.points);
break;
case ElementType.TEXT:
this.drawText(element);
break;
case ElementType.IMAGE:
this.drawImage(element);
break;
case ElementType.SHAPE:
this.drawShape(element);
break;
}
}
// 绘制路径
drawPath(points: Point[]) {
if (points.length < 2) {
return;
}
this.context.beginPath();
this.context.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
this.context.lineTo(points[i].x, points[i].y);
}
this.context.strokeStyle = this.currentColor;
this.context.lineWidth = this.strokeWidth;
this.context.lineCap = 'round';
this.context.lineJoin = 'round';
this.context.stroke();
}
// 绘制文本
drawText(element: WhiteboardElement) {
this.context.font = `${element.properties.fontSize}px sans-serif`;
this.context.fillStyle = element.properties.color;
this.context.fillText(element.properties.text, element.x, element.y);
}
// 绘制图片
drawImage(element: WhiteboardElement) {
// 实际实现需要加载图片
}
// 绘制形状
drawShape(element: WhiteboardElement) {
// 根据形状类型绘制
}
// 绘制协作者光标
drawCursor(collaborator: Collaborator) {
this.context.beginPath();
this.context.arc(collaborator.cursor.x, collaborator.cursor.y, 10, 0, 2 * Math.PI);
this.context.fillStyle = collaborator.color;
this.context.fill();
this.context.font = '12px sans-serif';
this.context.fillStyle = collaborator.color;
this.context.fillText(collaborator.userName, collaborator.cursor.x + 15, collaborator.cursor.y);
}
build() {
Column() {
// 工具栏
this.buildToolbar();
// 画布
Stack() {
Canvas(this.context)
.width('100%')
.height('100%')
.onReady(() => {
this.redraw();
})
.onTouch((event: TouchEvent) => {
for (const touch of event.touches) {
if (event.type === TouchType.Down) {
this.handleTouchStart(touch.x, touch.y);
} else if (event.type === TouchType.Move) {
this.handleTouchMove(touch.x, touch.y);
} else if (event.type === TouchType.Up) {
this.handleTouchEnd();
}
}
});
// 协作者指示器
this.buildCollaboratorIndicators();
}
.layoutWeight(1);
// 底部工具栏
this.buildBottomToolbar();
}
.width('100%')
.height('100%');
}
@Builder
buildToolbar() {
Row() {
// 返回
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_back'))
.width(24)
.height(24);
}
.width(40)
.height(40)
.backgroundColor(Color.Transparent);
Blank();
// 协作者
Row() {
ForEach(this.collaborators, (c: Collaborator) => {
Circle()
.width(24)
.height(24)
.fill(c.color)
.margin({ left: 4 });
});
};
Blank();
// 导出
Button('导出')
.onClick(() => {
this.exportWhiteboard();
});
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White);
}
@Builder
buildBottomToolbar() {
Row() {
// 选择工具
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_select'))
.width(24)
.height(24);
}
.width(48)
.height(48)
.backgroundColor(this.currentTool === ToolType.SELECT ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.currentTool = ToolType.SELECT;
});
// 画笔
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_pen'))
.width(24)
.height(24);
}
.width(48)
.height(48)
.backgroundColor(this.currentTool === ToolType.PEN ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.currentTool = ToolType.PEN;
});
// 橡皮擦
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_eraser'))
.width(24)
.height(24);
}
.width(48)
.height(48)
.backgroundColor(this.currentTool === ToolType.ERASER ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.currentTool = ToolType.ERASER;
});
// 文字
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_text'))
.width(24)
.height(24);
}
.width(48)
.height(48)
.backgroundColor(this.currentTool === ToolType.TEXT ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.currentTool = ToolType.TEXT;
});
// 形状
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_shape'))
.width(24)
.height(24);
}
.width(48)
.height(48)
.backgroundColor(this.currentTool === ToolType.SHAPE ? '#E3F2FD' : Color.Transparent)
.onClick(() => {
this.currentTool = ToolType.SHAPE;
});
}
.width('100%')
.height(64)
.justifyContent(FlexAlign.SpaceEvenly)
.backgroundColor(Color.White);
}
@Builder
buildCollaboratorIndicators() {
// 显示协作者光标和选中状态
ForEach(this.collaborators, (c: Collaborator) => {
Column() {
Circle()
.width(20)
.height(20)
.fill(c.color);
Text(c.userName)
.fontSize(10)
.fontColor(c.color);
}
.position({ x: c.cursor.x, y: c.cursor.y });
});
}
// 导出白板
exportWhiteboard() {
// 导出为图片或PDF
console.info('导出白板');
}
}
// 工具类型
enum ToolType {
SELECT = 'select',
PEN = 'pen',
ERASER = 'eraser',
TEXT = 'text',
SHAPE = 'shape'
}
// 点
interface Point {
x: number;
y: number;
}
3.2 实时光标同步
typescript
// CursorSync.ets
// 光标同步管理器
class CursorSyncManager {
private cursors: Map<string, CursorState> = new Map();
private syncInterval: number = 50; // 50ms同步一次
// 更新本地光标
updateLocalCursor(x: number, y: number) {
// 立即更新UI
// 定时同步到其他设备
}
// 接收远程光标
receiveRemoteCursor(userId: string, x: number, y: number) {
const cursor = this.cursors.get(userId) || {
userId: userId,
x: 0,
y: 0,
lastUpdate: 0
};
cursor.x = x;
cursor.y = y;
cursor.lastUpdate = Date.now();
this.cursors.set(userId, cursor);
}
// 获取所有光标
getAllCursors(): CursorState[] {
return Array.from(this.cursors.values());
}
}
interface CursorState {
userId: string;
x: number;
y: number;
lastUpdate: number;
}
3.3 操作历史与撤销重做
typescript
// HistoryManager.ets
// 操作历史管理器
class HistoryManager {
private undoStack: Operation[] = [];
private redoStack: Operation[] = [];
private maxHistory: number = 100;
// 记录操作
recordOperation(operation: Operation) {
this.undoStack.push(operation);
this.redoStack = []; // 清空重做栈
// 限制历史数量
if (this.undoStack.length > this.maxHistory) {
this.undoStack.shift();
}
}
// 撤销
async undo(): Promise<Operation | null> {
if (this.undoStack.length === 0) {
return null;
}
const operation = this.undoStack.pop()!;
this.redoStack.push(operation);
// 执行逆操作
await this.executeInverse(operation);
return operation;
}
// 重做
async redo(): Promise<Operation | null> {
if (this.redoStack.length === 0) {
return null;
}
const operation = this.redoStack.pop()!;
this.undoStack.push(operation);
// 执行操作
await this.executeOperation(operation);
return operation;
}
// 执行逆操作
private async executeInverse(operation: Operation) {
switch (operation.type) {
case 'add':
// 逆操作:删除
break;
case 'delete':
// 逆操作:添加
break;
case 'update':
// 逆操作:恢复旧值
break;
}
}
// 执行操作
private async executeOperation(operation: Operation) {
// 执行操作
}
}
interface Operation {
type: 'add' | 'delete' | 'update';
elementId: string;
data: any;
inverseData: any;
timestamp: number;
userId: string;
}
四、踩坑与注意事项
4.1 性能优化
问题:图元过多时,绘制卡顿。
解决方案:
typescript
// 分层渲染
class LayeredRenderer {
private layers: CanvasLayer[] = [];
// 添加图层
addLayer(layer: CanvasLayer) {
this.layers.push(layer);
}
// 渲染可见图层
renderVisible(viewport: Viewport) {
for (const layer of this.layers) {
if (this.isLayerVisible(layer, viewport)) {
layer.render();
}
}
}
// 判断图层是否可见
isLayerVisible(layer: CanvasLayer, viewport: Viewport): boolean {
// 视口裁剪
return true;
}
}
interface CanvasLayer {
elements: WhiteboardElement[];
render(): void;
}
interface Viewport {
x: number;
y: number;
width: number;
height: number;
scale: number;
}
4.2 冲突处理
问题:多人同时修改同一图元。
解决方案:
typescript
// 基于版本号的冲突检测
class ConflictResolver {
// 检测冲突
detectConflict(local: WhiteboardElement, remote: WhiteboardElement): boolean {
// 版本号相同但内容不同,说明有冲突
if (local.version === remote.version &&
JSON.stringify(local) !== JSON.stringify(remote)) {
return true;
}
return false;
}
// 解决冲突
resolve(local: WhiteboardElement, remote: WhiteboardElement): WhiteboardElement {
// 策略1:最后修改者胜
if (remote.updatedAt > local.updatedAt) {
return remote;
}
// 策略2:合并
// ...
return local;
}
}
五、HarmonyOS 6适配
5.1 API差异
typescript
// HarmonyOS 6 Canvas API
import { graphics } from '@kit.GraphicsKit';
// 新增:高性能渲染上下文
const context = graphics.createCanvasRenderingContext2D({
antialias: true,
alpha: true,
willReadFrequently: false // 性能提示
});
// 新增:离屏渲染
const offscreen = graphics.createOffscreenCanvas(1920, 1080);
5.2 适配代码
typescript
// DistributedWhiteboardV6.ets
import { graphics } from '@kit.GraphicsKit';
import { distributedData } from '@kit.ArkData';
@Entry
@Component
struct DistributedWhiteboardV6 {
// HarmonyOS 6高性能渲染
private settings: graphics.CanvasRenderingContext2DSettings = {
antialias: true,
alpha: true
};
private context: graphics.CanvasRenderingContext2D =
graphics.createCanvasRenderingContext2D(this.settings);
// 其他实现与之前类似
// ...
}
六、总结一下下
分布式协作白板让多人实时协作成为可能,核心要点:
- 数据模型:标准化的图元模型,支持多种类型
- 同步机制:增量同步、批量合并、冲突解决
- 实时交互:光标同步、选中状态同步
- 性能优化:分层渲染、视口裁剪
- HarmonyOS 6:高性能Canvas API、离屏渲染
分布式协作场景的实现,让团队协作不再受地理位置限制,真正实现了"天涯若比邻"的协作体验。