基于HarmonyOS的分布式白板协作应用开发

一、项目概述

本项目基于HarmonyOS的分布式能力和手写笔API,开发一个支持多设备实时协同绘图的分布式白板应用。参考《鸿蒙跨端U同步:同一局游戏中多设备玩家昵称/头像显示》的技术方案,实现跨设备绘图数据同步、实时协作和手写笔优化等功能。

https://example.com/dist-whiteboard-arch.png
图1:分布式白板系统架构(包含绘图引擎层、数据同步层和设备管理层)

二、核心功能实现

1. 分布式数据管理(ArkTS)

// 分布式白板数据管理
class DistributedWhiteboard {
  private static instance: DistributedWhiteboard;
  private distObject: distributedDataObject.DataObject;
  private strokes: Stroke[] = [];
  private devices: DeviceInfo[] = [];
  
  static getInstance(): DistributedWhiteboard {
    if (!DistributedWhiteboard.instance) {
      DistributedWhiteboard.instance = new DistributedWhiteboard();
    }
    return DistributedWhiteboard.instance;
  }
  
  constructor() {
    this.initDistributedObject();
    this.registerDevice();
  }
  
  // 初始化分布式数据对象
  private initDistributedObject() {
    this.distObject = distributedDataObject.create({
      strokes: [],
      activeDevices: {},
      currentTool: 'pen',
      currentColor: '#000000'
    });
    
    // 监听数据变化
    this.distObject.on('change', (fields: string[]) => {
      if (fields.includes('strokes')) {
        this.handleStrokesUpdate();
      }
      if (fields.includes('activeDevices')) {
        this.handleDevicesUpdate();
      }
    });
  }
  
  // 注册当前设备
  private registerDevice() {
    this.distObject.activeDevices[deviceInfo.deviceId] = {
      name: deviceInfo.deviceName,
      lastActive: Date.now(),
      tool: 'pen',
      color: '#000000'
    };
  }
  
  // 添加笔画
  addStroke(stroke: Stroke): void {
    this.strokes.push(stroke);
    this.syncStrokes();
  }
  
  // 清除所有笔画
  clearAll(): void {
    this.strokes = [];
    this.syncStrokes();
  }
  
  // 同步笔画数据
  private syncStrokes(): void {
    this.distObject.strokes = this.strokes.map(s => ({
      ...s,
      syncTime: Date.now()
    }));
    
    this.syncToConnectedDevices();
  }
  
  // 处理笔画更新
  private handleStrokesUpdate(): void {
    const remoteStrokes = this.distObject.strokes;
    if (remoteStrokes && remoteStrokes.length > 0) {
      // 合并本地和远程笔画,保留最新版本
      const merged = this.mergeStrokes(this.strokes, remoteStrokes);
      this.strokes = merged;
      EventBus.emit('strokesUpdated', this.strokes);
    }
  }
  
  // 合并笔画数据
  private mergeStrokes(local: Stroke[], remote: Stroke[]): Stroke[] {
    const merged: Stroke[] = [];
    const allStrokes = [...local, ...remote];
    const strokeMap = new Map<string, Stroke>();
    
    allStrokes.forEach(stroke => {
      const key = stroke.id || `${stroke.deviceId}-${stroke.timestamp}`;
      const existing = strokeMap.get(key);
      
      if (!existing || (stroke.syncTime || 0) > (existing.syncTime || 0)) {
        strokeMap.set(key, stroke);
      }
    });
    
    return Array.from(strokeMap.values());
  }
  
  // 处理设备更新
  private handleDevicesUpdate(): void {
    this.devices = Object.entries(this.distObject.activeDevices)
      .map(([id, info]) => ({ deviceId: id, ...info }));
    
    EventBus.emit('devicesUpdated', this.devices);
  }
  
  // 更新当前工具状态
  updateTool(tool: string): void {
    this.distObject.currentTool = tool;
    this.distObject.activeDevices[deviceInfo.deviceId].tool = tool;
  }
  
  // 更新当前颜色状态
  updateColor(color: string): void {
    this.distObject.currentColor = color;
    this.distObject.activeDevices[deviceInfo.deviceId].color = color;
  }
  
  // 同步到已连接设备
  private syncToConnectedDevices(): void {
    const targetDevices = DeviceManager.getConnectedDevices()
      .map(d => d.deviceId)
      .filter(id => id !== deviceInfo.deviceId);
    
    if (targetDevices.length > 0) {
      this.distObject.setDistributed(targetDevices);
    }
  }
}

// 笔画数据类型定义
interface Stroke {
  id?: string;
  deviceId: string;
  points: Point[];
  color: string;
  width: number;
  tool: string;
  timestamp: number;
  syncTime?: number;
}

// 点数据类型定义
interface Point {
  x: number;
  y: number;
  pressure?: number;
  timestamp: number;
}

// 设备信息类型定义
interface DeviceInfo {
  deviceId: string;
  name: string;
  lastActive: number;
  tool: string;
  color: string;
}

2. 手写笔绘图引擎(ArkTS)

// 白板绘图组件
@Component
struct WhiteboardCanvas {
  @State strokes: Stroke[] = [];
  @State currentStroke: Stroke | null = null;
  @State currentTool: string = 'pen';
  @State currentColor: string = '#000000';
  @State currentWidth: number = 3;
  
  private whiteboard = DistributedWhiteboard.getInstance();
  private canvasRef: CanvasRenderingContext2D | null = null;
  
  aboutToAppear() {
    // 监听笔画更新
    EventBus.on('strokesUpdated', (strokes: Stroke[]) => {
      this.strokes = strokes;
      this.redrawCanvas();
    });
  }
  
  build() {
    Stack() {
      // 画布区域
      Canvas(this.canvasRef)
        .width('100%')
        .height('100%')
        .backgroundColor('#FFFFFF')
        .onReady(() => {
          this.canvasRef = this.$canvas;
          this.redrawCanvas();
        })
        .onTouch((event: TouchEvent) => {
          this.handleTouchEvent(event);
        })
      
      // 工具面板
      ToolPanel({
        tool: this.currentTool,
        color: this.currentColor,
        width: this.currentWidth,
        onToolChange: (tool) => {
          this.currentTool = tool;
          this.whiteboard.updateTool(tool);
        },
        onColorChange: (color) => {
          this.currentColor = color;
          this.whiteboard.updateColor(color);
        }
      })
      .position({ x: 0, y: 0 })
    }
  }
  
  // 处理触摸事件
  private handleTouchEvent(event: TouchEvent) {
    if (!this.canvasRef) return;
    
    const touches = event.touches;
    if (touches.length === 0) return;
    
    const touch = touches[0];
    const point = {
      x: touch.x,
      y: touch.y,
      pressure: touch.force || 1.0,
      timestamp: Date.now()
    };
    
    switch (event.type) {
      case 'touchstart':
        this.startNewStroke(point);
        break;
      case 'touchmove':
        this.addPointToStroke(point);
        break;
      case 'touchend':
        this.finishCurrentStroke();
        break;
    }
  }
  
  // 开始新笔画
  private startNewStroke(point: Point) {
    this.currentStroke = {
      deviceId: deviceInfo.deviceId,
      points: [point],
      color: this.currentColor,
      width: this.currentWidth,
      tool: this.currentTool,
      timestamp: Date.now()
    };
    
    this.drawPoint(point, this.currentColor, this.currentWidth);
  }
  
  // 添加点到当前笔画
  private addPointToStroke(point: Point) {
    if (!this.currentStroke) return;
    
    this.currentStroke.points.push(point);
    
    // 绘制线段
    const prevPoint = this.currentStroke.points[this.currentStroke.points.length - 2];
    this.drawLine(prevPoint, point, this.currentColor, this.currentWidth);
  }
  
  // 完成当前笔画
  private finishCurrentStroke() {
    if (!this.currentStroke) return;
    
    // 确保至少有两个点
    if (this.currentStroke.points.length < 2) {
      this.currentStroke.points.push({
        ...this.currentStroke.points[0],
        timestamp: Date.now()
      });
    }
    
    this.whiteboard.addStroke(this.currentStroke);
    this.currentStroke = null;
  }
  
  // 重绘画布
  private redrawCanvas() {
    if (!this.canvasRef) return;
    
    // 清空画布
    this.canvasRef.clearRect(0, 0, 
      this.canvasRef.width, this.canvasRef.height);
    
    // 重绘所有笔画
    this.strokes.forEach(stroke => {
      if (stroke.points.length < 2) return;
      
      let prevPoint = stroke.points[0];
      for (let i = 1; i < stroke.points.length; i++) {
        const point = stroke.points[i];
        this.drawLine(prevPoint, point, stroke.color, stroke.width);
        prevPoint = point;
      }
    });
    
    // 绘制当前未完成的笔画
    if (this.currentStroke) {
      let prevPoint = this.currentStroke.points[0];
      for (let i = 1; i < this.currentStroke.points.length; i++) {
        const point = this.currentStroke.points[i];
        this.drawLine(prevPoint, point, this.currentColor, this.currentWidth);
        prevPoint = point;
      }
    }
  }
  
  // 绘制点
  private drawPoint(point: Point, color: string, width: number) {
    if (!this.canvasRef) return;
    
    this.canvasRef.beginPath();
    this.canvasRef.fillStyle = color;
    this.canvasRef.arc(point.x, point.y, width / 2, 0, Math.PI * 2);
    this.canvasRef.fill();
  }
  
  // 绘制线段
  private drawLine(p1: Point, p2: Point, color: string, width: number) {
    if (!this.canvasRef) return;
    
    this.canvasRef.beginPath();
    this.canvasRef.strokeStyle = color;
    this.canvasRef.lineWidth = width;
    this.canvasRef.lineCap = 'round';
    this.canvasRef.lineJoin = 'round';
    this.canvasRef.moveTo(p1.x, p1.y);
    this.canvasRef.lineTo(p2.x, p2.y);
    this.canvasRef.stroke();
  }
}

// 工具面板组件
@Component
struct ToolPanel {
  @Prop tool: string;
  @Prop color: string;
  @Prop width: number;
  @Link onToolChange: (tool: string) => void;
  @Link onColorChange: (color: string) => void;
  
  @State showColorPicker: boolean = false;
  @State showWidthPicker: boolean = false;
  
  build() {
    Row() {
      // 工具选择
      ToolButton({
        icon: 'pen',
        active: this.tool === 'pen',
        onClick: () => this.onToolChange('pen')
      })
      
      ToolButton({
        icon: 'eraser',
        active: this.tool === 'eraser',
        onClick: () => this.onToolChange('eraser')
      })
      
      // 颜色选择
      Button()
        .width(30)
        .height(30)
        .backgroundColor(this.color)
        .borderRadius(15)
        .onClick(() => this.showColorPicker = true)
      
      // 线宽选择
      Button(`${this.width}px`)
        .width(50)
        .height(30)
        .onClick(() => this.showWidthPicker = true)
      
      // 清空按钮
      Button('清空')
        .onClick(() => {
          DistributedWhiteboard.getInstance().clearAll();
        })
    }
    .padding(10)
    .backgroundColor('rgba(255,255,255,0.8)')
    .borderRadius(20)
    .margin(10)
    
    // 颜色选择器
    if (this.showColorPicker) {
      ColorPicker({
        selectedColor: this.color,
        onColorSelect: (color) => {
          this.onColorChange(color);
          this.showColorPicker = false;
        },
        onClose: () => this.showColorPicker = false
      })
    }
    
    // 线宽选择器
    if (this.showWidthPicker) {
      WidthPicker({
        selectedWidth: this.width,
        onWidthSelect: (width) => {
          this.width = width;
          this.showWidthPicker = false;
        },
        onClose: () => this.showWidthPicker = false
      })
    }
  }
}

// 工具按钮组件
@Component
struct ToolButton {
  @Prop icon: string;
  @Prop active: boolean;
  @Link onClick: () => void;
  
  build() {
    Button(this.icon)
      .width(40)
      .height(40)
      .backgroundColor(this.active ? '#4CAF50' : '#E0E0E0')
      .fontColor(this.active ? '#FFFFFF' : '#000000')
      .borderRadius(20)
      .margin({ right: 10 })
      .onClick(this.onClick)
  }
}

3. 手写笔优化处理(ArkTS)

// 手写笔事件处理扩展
class StylusHandler {
  private static instance: StylusHandler;
  private whiteboard: DistributedWhiteboard;
  
  static getInstance(): StylusHandler {
    if (!StylusHandler.instance) {
      StylusHandler.instance = new StylusHandler();
    }
    return StylusHandler.instance;
  }
  
  constructor() {
    this.whiteboard = DistributedWhiteboard.getInstance();
    this.registerStylusListener();
  }
  
  // 注册手写笔监听
  private registerStylusListener() {
    try {
      inputDevice.on('stylus', (event: inputDevice.StylusEvent) => {
        this.handleStylusEvent(event);
      });
      
      console.log('手写笔监听已注册');
    } catch (error) {
      console.error('注册手写笔监听失败:', error);
    }
  }
  
  // 处理手写笔事件
  private handleStylusEvent(event: inputDevice.StylusEvent) {
    const point = {
      x: event.x,
      y: event.y,
      pressure: event.pressure || 1.0,
      timestamp: Date.now()
    };
    
    switch (event.action) {
      case inputDevice.StylusAction.DOWN:
        this.whiteboard.updateTool('pen');
        EventBus.emit('stylusStart', point);
        break;
      case inputDevice.StylusAction.MOVE:
        EventBus.emit('stylusMove', point);
        break;
      case inputDevice.StylusAction.UP:
        EventBus.emit('stylusEnd', point);
        break;
      case inputDevice.StylusAction.HOVER:
        // 悬停处理
        break;
    }
  }
  
  // 获取手写笔压力曲线
  getPressureCurve(pressure: number): number {
    // 自定义压力曲线,增强手写体验
    return Math.pow(pressure, 1.5);
  }
}

三、关键功能说明

1. 分布式数据同步机制

sequenceDiagram
    participant 设备A
    participant 设备B
    participant 设备C
    
    设备A->>设备A: 用户绘制笔画
    设备A->>设备B: 同步笔画数据
    设备A->>设备C: 同步笔画数据
    设备B->>设备B: 更新本地画布
    设备C->>设备C: 更新本地画布
    设备B->>设备B: 用户添加笔画
    设备B->>设备A: 同步新笔画
    设备B->>设备C: 同步新笔画

2. 手写笔优化技术

技术点 实现方式 效果
压力感应 根据pressure参数动态调整线宽 实现自然笔触效果
悬停预览 处理StylusAction.HOVER事件 显示光标位置
低延迟 本地优先渲染+异步同步 减少绘制延迟

3. 冲突解决策略

// 基于时间戳的冲突解决
private mergeStrokes(local: Stroke[], remote: Stroke[]): Stroke[] {
  const merged: Stroke[] = [];
  const allStrokes = [...local, ...remote];
  const strokeMap = new Map<string, Stroke>();
  
  allStrokes.forEach(stroke => {
    const key = stroke.id || `${stroke.deviceId}-${stroke.timestamp}`;
    const existing = strokeMap.get(key);
    
    // 保留最新版本
    if (!existing || (stroke.syncTime || 0) > (existing.syncTime || 0)) {
      strokeMap.set(key, stroke);
    }
  });
  
  return Array.from(strokeMap.values());
}

四、项目扩展与优化

1. 功能扩展建议

  1. ​多人光标显示​​:

    // 显示其他用户的光标位置
    showRemoteCursors(devices: DeviceInfo[]): void {
      devices.forEach(device => {
        if (device.deviceId !== this.deviceId && device.cursorPosition) {
          this.drawCursor(device.cursorPosition, device.color);
        }
      });
    }
  2. ​白板历史记录​​:

    // 保存白板状态
    saveWhiteboardState(): void {
      const state = {
        strokes: this.strokes,
        timestamp: Date.now()
      };
      distributedDataObject.save('whiteboard_history', state);
    }
  3. ​图像插入功能​​:

    // 插入图片到白板
    insertImage(imageUri: string, position: Point): void {
      this.whiteboard.addStroke({
        type: 'image',
        uri: imageUri,
        position,
        deviceId: this.deviceId,
        timestamp: Date.now()
      });
    }

2. 性能优化建议

  1. ​笔画数据压缩​​:

    // 压缩笔画点数据
    compressStrokePoints(points: Point[]): Point[] {
      if (points.length <= 10) return points;
      
      const compressed = [points[0]];
      for (let i = 1; i < points.length - 1; i++) {
        if (i % 3 === 0) compressed.push(points[i]);
      }
      compressed.push(points[points.length - 1]);
      
      return compressed;
    }
  2. ​增量同步策略​​:

    // 只同步新增笔画
    syncNewStrokes(lastSyncTime: number): Stroke[] {
      return this.strokes.filter(s => 
        !s.syncTime || s.syncTime > lastSyncTime
      );
    }
  3. ​本地缓存优化​​:

    // 使用本地缓存减少网络传输
    cacheStrokesLocally(): void {
      localStorage.set('cached_strokes', JSON.stringify(this.strokes));
    }

五、总结

本项目基于HarmonyOS实现了具有以下特点的分布式白板协作应用:

  1. ​实时多端协作​​:支持多设备同时绘制,实时同步绘图数据
  2. ​自然书写体验​​:优化手写笔支持,提供压力感应和低延迟绘制
  3. ​智能冲突解决​​:基于时间戳的合并策略保证数据一致性
  4. ​灵活的工具集​​:提供多种绘图工具和自定义选项

通过参考《鸿蒙跨端U同步:同一局游戏中多设备玩家昵称/头像显示》的技术方案,我们验证了HarmonyOS在实时协作场景下的强大能力,为开发者提供了构建分布式协作应用的实践参考。

注意事项:
1. 实际开发需要申请分布式权限和设备输入权限
2. 生产环境需要考虑数据加密和安全性
3. 可根据具体需求扩展更多协作功能
4. 建议在真机上测试手写笔功能以获得最佳体验
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐