HarmonyOS分布式协作开发实战:多人协同白板

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);
  
    // 其他实现与之前类似
    // ...
}

六、总结一下下

分布式协作白板让多人实时协作成为可能,核心要点:

  1. 数据模型:标准化的图元模型,支持多种类型
  2. 同步机制:增量同步、批量合并、冲突解决
  3. 实时交互:光标同步、选中状态同步
  4. 性能优化:分层渲染、视口裁剪
  5. HarmonyOS 6:高性能Canvas API、离屏渲染

分布式协作场景的实现,让团队协作不再受地理位置限制,真正实现了"天涯若比邻"的协作体验。