鸿蒙APP开发-通过单块酷APP了解效果链绘制功能

5 阅读4分钟

吉他效果器链怎么做?HarmonyOS Canvas绘制效果链

如果你对吉他效果器感兴趣,可以去鸿蒙应用市场搜一下**「单块酷」**,下载下来体验体验。在Canvas上绘制效果器链路图,拖拽排列顺序,连接信号流向。体验完了再回来看这篇文章,你会更清楚效果链绘制是怎么用Canvas实现的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,Canvas绘图和拖拽交互算是家常便饭。去年开始转战鸿蒙生态,用ArkTS开发App,发现Canvas这块虽然底层API相似,但组件模型和事件处理差异不小。

比如:

  • Canvas初始化:Web里document.getElementById('canvas').getContext('2d');鸿蒙里用new CanvasRenderingContext2D(new RenderingContextSettings(true))
  • 拖拽实现:Web里用dragstart/dragover/drop事件;鸿蒙里用onTouch事件手动计算。
  • 动画帧:Web里用requestAnimationFrame;鸿蒙里用requestAnimationFrame或定时器。

别担心,接下来这篇文章,我会用"单块酷"的效果链绘制,带你看看HarmonyOS的Canvas怎么绘制效果器链路图。


这篇文章聊什么

单块酷的效果链绘制功能,核心要解决:

  1. 效果器绘制:绘制效果器方块和图标
  2. 连线绘制:绘制信号流向的连线
  3. 拖拽排序:拖拽效果器调整顺序
  4. 信号流向:可视化信号从吉他到音箱的路径

第一步:设计效果链数据结构

// Web前端同学看这里:React里我们用state管理效果器列表
// 鸿蒙里用@State装饰器,数据结构设计思路一致

// 效果器类型
const PEDAL_TYPES = [
  { id: 'tuner', name: '调音表', color: '#22c55e', icon: '🎸' },
  { id: 'compressor', name: '压缩', color: '#3b82f6', icon: '📊' },
  { id: 'overdrive', name: '过载', color: '#f97316', icon: '🔥' },
  { id: 'distortion', name: '失真', color: '#ef4444', icon: '⚡' },
  { id: 'fuzz', name: '法兹', color: '#8b5cf6', icon: '🌀' },
  { id: 'delay', name: '延迟', color: '#06b6d4', icon: '⏱️' },
  { id: 'reverb', name: '混响', color: '#ec4899', icon: '🌊' },
  { id: 'chorus', name: '合唱', color: '#14b8a6', icon: '🎶' },
  { id: 'wah', name: '哇音', color: '#eab308', icon: '👄' },
  { id: 'eq', name: '均衡', color: '#6366f1', icon: '📊' },
];

// 效果器实例
interface PedalInstance {
  id: string;
  typeId: string;
  order: number;
  isOn: boolean;
  parameters: Record<string, number>;
}

// 效果链
interface PedalChain {
  id: string;
  name: string;
  pedals: PedalInstance[];
  createdAt: string;
}

第二步:实现Canvas绘制

// Web前端同学看这里:Canvas绘制逻辑和Web端几乎一样
// 主要区别是组件初始化和事件绑定方式

@Entry
@Component
struct PedalChainEditor {
  @State chain: PedalChain | null = null
  @State selectedPedal: string = ''
  @State isDragging: boolean = false
  @State dragIndex: number = -1

  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  private drawChain() {
    if (!this.chain) return;

    const ctx = this.context;
    const width = 350;
    const height = 120;
    const pedalWidth = 60;
    const pedalHeight = 60;
    const spacing = 20;

    ctx.clearRect(0, 0, width, height);

    // 绘制输入端
    ctx.fillStyle = '#374151';
    ctx.fillRect(0, 30, 20, 60);
    ctx.fillStyle = '#ffffff';
    ctx.font = '10px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('IN', 10, 65);

    // 绘制效果器
    this.chain.pedals.forEach((pedal, index) => {
      const x = 30 + index * (pedalWidth + spacing);
      const y = 30;
      const typeInfo = PEDAL_TYPES.find(t => t.id === pedal.typeId);

      // 效果器方块
      ctx.fillStyle = typeInfo?.color || '#6b7280';
      ctx.fillRect(x, y, pedalWidth, pedalHeight);
      ctx.strokeStyle = this.selectedPedal === pedal.id ? '#000000' : 'transparent';
      ctx.lineWidth = 2;
      ctx.strokeRect(x, y, pedalWidth, pedalHeight);

      // 关闭状态变暗
      if (!pedal.isOn) {
        ctx.fillStyle = 'rgba(0,0,0,0.3)';
        ctx.fillRect(x, y, pedalWidth, pedalHeight);
      }

      // 效果器名称
      ctx.fillStyle = '#ffffff';
      ctx.font = '10px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText(typeInfo?.name || '', x + pedalWidth / 2, y + pedalHeight / 2 + 4);

      // 连线(非第一个效果器)
      if (index > 0) {
        const prevX = 30 + (index - 1) * (pedalWidth + spacing) + pedalWidth;
        ctx.beginPath();
        ctx.moveTo(prevX, y + pedalHeight / 2);
        ctx.lineTo(x, y + pedalHeight / 2);
        ctx.strokeStyle = '#9ca3af';
        ctx.lineWidth = 2;
        ctx.stroke();

        // 箭头
        ctx.beginPath();
        ctx.moveTo(x - 5, y + pedalHeight / 2 - 3);
        ctx.lineTo(x, y + pedalHeight / 2);
        ctx.lineTo(x - 5, y + pedalHeight / 2 + 3);
        ctx.fillStyle = '#9ca3af';
        ctx.fill();
      }
    });

    // 绘制输出端
    const lastX = 30 + this.chain.pedals.length * (pedalWidth + spacing);
    ctx.fillStyle = '#374151';
    ctx.fillRect(lastX, 30, 20, 60);
    ctx.fillStyle = '#ffffff';
    ctx.font = '10px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText('OUT', lastX + 10, 65);
  }

  build() {
    Column() {
      Text('效果链编辑')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      Canvas(this.context)
        .width('100%')
        .height(120)
        .backgroundColor('#f9fafb')
        .borderRadius(12)
        .onReady(() => this.drawChain())
        .onTouch((event: TouchEvent) => {
          if (event.type === TouchType.Down) {
            this.handleTouchDown(event.touches[0].x, event.touches[0].y);
          } else if (event.type === TouchType.Move && this.isDragging) {
            this.handleTouchMove(event.touches[0].x);
          } else if (event.type === TouchType.Up) {
            this.handleTouchUp();
          }
        })

      // 效果器选择
      Text('添加效果器')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .margin({ top: 20, bottom: 12 })

      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(PEDAL_TYPES, (pedalType) => {
          Button(`${pedalType.icon} ${pedalType.name}`)
            .fontSize(12)
            .height(36)
            .backgroundColor(pedalType.color)
            .borderRadius(8)
            .margin({ right: 8, bottom: 8 })
            .onClick(() => this.addPedal(pedalType.id))
        })
      }
    }
    .padding(16)
  }

  private handleTouchDown(x: number, y: number) {
    if (!this.chain) return;

    const pedalWidth = 60;
    const spacing = 20;

    this.chain.pedals.forEach((pedal, index) => {
      const px = 30 + index * (pedalWidth + spacing);
      if (x >= px && x <= px + pedalWidth && y >= 30 && y <= 90) {
        this.selectedPedal = pedal.id;
        this.isDragging = true;
        this.dragIndex = index;
      }
    });
  }

  private handleTouchMove(x: number) {
    // 拖拽中的视觉反馈(可以添加)
  }

  private handleTouchUp() {
    this.isDragging = false;
    this.dragIndex = -1;
    this.drawChain();
  }

  private addPedal(typeId: string) {
    if (!this.chain) return;

    const newPedal: PedalInstance = {
      id: `pedal_${Date.now()}`,
      typeId,
      order: this.chain.pedals.length,
      isOn: true,
      parameters: {}
    };

    this.chain.pedals.push(newPedal);
    this.drawChain();
  }
}

第三步:常见问题

3.1 效果器数量限制

问题:屏幕空间有限,放不下太多效果器。

解决:支持横向滚动,或者提供折叠/展开功能。

3.2 拖拽排序精度

问题:手指拖拽时定位不准确。

解决:增加触摸区域的容错范围,或者使用吸附效果。


总结

这篇文章围绕"单块酷"的效果链绘制,讲解了:

Canvas绘制

  • 效果器方块绘制
  • 连线和箭头绘制
  • 选中状态和关闭状态的视觉表现

交互实现

  • 触摸事件处理
  • 拖拽排序逻辑
  • 添加效果器

如果你对"单块酷"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。