鸿蒙元服务实战-笑笑五子棋(5)

来到最后一章了,这一章节讲两个部分。一是笑笑五子棋的卡片制作,二就是发布上架。

卡片介绍

Form Kit(卡片开发框架)提供了一种在桌面、锁屏等系统入口嵌入显示应用信息的开发框架和 API,可以将应用内用户关注的重要

信息或常用操作抽取到服务卡片(以下简称“卡片”)上,通过将卡片添加到桌面上,以达到信息展示、服务直达的便捷体验效果。

image-20250105154516700

新建卡片

卡片类型分为两种:

  1. 静态卡片 功能稍弱
  2. 动态卡片 功能强一些

image-20250105154650672

  1. 选择卡片的属性

    image-20250105155715639

  2. 然后你就得到了以下文件

    image-20250105154810895

卡片文件解释

EntryFormAbility.ets

entry/src/main/ets/entryformability/EntryFormAbility.ets

该文件可以定义卡片的生命周期,传递数据给卡片等

WidgetCard

entry/src/main/ets/widget/pages/WidgetCard.ets

该文件是卡片的主要展示和业务功能页面。 卡片外观、功能主要由它来提供

form_config.json

entry/src/main/resources/base/profile/form_config.json

该文件是卡片的配置文件,比如卡片的图标、卡片的名字、卡片的种类等等都可以在这配置

获取卡片宽度

卡片的 api 和元服务的 api 稍有区别,所以在开发的需要额外注意

这里在 entry/src/main/ets/entryformability/EntryFormAbility.ets 内,可以设置卡片创建的时获取卡片的宽度

因为卡片有不同的规格尺寸,所以可以动态来获取。

  onAddForm(want: Want) {
    let formData: Record<string, number> = {
      "canwidth": px2vp((want.parameters?.[formInfo.FormParam.WIDTH_KEY] as number) * 2),
    };
    return formBindingData.createFormBindingData(formData);
  }

卡片中是无法使用 AppStorage,所以需要使用 Localstorage 来代替,进行数据传递

卡片中接收

@Entry
@Component
struct WidgetCard {
  @LocalStorageProp("canwidth")
  canwidth: number = 0
  @LocalStorageProp("canwidth")
  canheight: number = 0

完成卡片下棋逻辑

因为卡片的下棋逻辑和宿主-元服务本身几乎一致。因此在实际开发中,可以将它们共同的逻辑抽离出来方便管理。这里就 cv 复用了。

@Entry
@Component
struct WidgetCard {
  @LocalStorageProp("canwidth")
  canwidth: number = 0
  @LocalStorageProp("canwidth")
  canheight: number = 0
  settings: RenderingContextSettings = new RenderingContextSettings(true);
  ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  // 棋盘参数
  gridSize: number = 15;
  cellSize: number = this.canwidth / this.gridSize;
  radius: number = this.cellSize / 2 - 5; // 棋子的半径
  // 棋盘数据
  board: number[][] = []
  currentPlayer: number = 1; // 当前玩家 (1: 黑子, 2: 白子)
  gameOver: boolean = false;
  @State
  textContent: string = ""
  // 处理玩家落子
  handleClick = async (event: ClickEvent) => {
    if (this.gameOver) {
      return;
    }

    const x = event.x;
    const y = event.y;

    const col = Math.floor(x / this.cellSize);
    const row = Math.floor(y / this.cellSize);

    if (this.board[row] && this.board[row][col] === 0) {
      this.board[row][col] = this.currentPlayer;
      this.drawBoard();

      if (this.checkWin(row, col)) {
        this.textContent = this.currentPlayer === 1 ? '黑子胜利!' : '白子胜利!';
        this.gameOver = true;
        // AlertDialog.show({ message: this.textContent })


      } else {
        this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
        this.textContent = this.currentPlayer === 1 ? '轮到黑子落子' : '轮到白子落子';
      }
    } else {
      // promptAction.showToast({ message: `请点击中棋盘对位位置` })
    }
  }

  aboutToAppear(): void {

  }

  // 绘制棋盘
  drawBoard = () => {
    this.ctx.clearRect(0, 0, this.canwidth, this.canwidth);

    // 绘制网格
    this.ctx.strokeStyle = "#000";
    this.ctx.lineWidth = 1;
    for (let i = 0; i < this.gridSize; i++) {
      this.ctx.beginPath();
      this.ctx.moveTo(this.cellSize * i, 0);
      this.ctx.lineTo(this.cellSize * i, this.canwidth);
      this.ctx.stroke();

      this.ctx.beginPath();
      this.ctx.moveTo(0, this.cellSize * i);
      this.ctx.lineTo(this.canwidth, this.cellSize * i);
      this.ctx.stroke();
    }

    // 绘制已落的棋子
    for (let row = 0; row < this.gridSize; row++) {
      for (let col = 0; col < this.gridSize; col++) {
        if (this.board[row][col] !== 0) {
          this.ctx.beginPath();
          this.ctx.arc(col * this.cellSize + this.cellSize / 2, row * this.cellSize + this.cellSize / 2, this.radius, 0,
            2 * Math.PI);
          this.ctx.fillStyle = this.board[row][col] === 1 ? 'black' : 'white';
          this.ctx.fill();
          this.ctx.stroke();
        }
      }
    }
  }
  // 判断是否有五子连珠
  checkWin = (row: number, col: number) => {
    interface abc {
      dr: number
      dc: number
    }

    const directions: abc[] = [
      { dr: 0, dc: 1 }, // 水平
      { dr: 1, dc: 0 }, // 垂直
      { dr: 1, dc: 1 }, // 主对角线
      { dr: 1, dc: -1 }// 副对角线
    ];

    for (let i = 0; i < directions.length; i++) {
      const dr = directions[i].dr
      const dc = directions[i].dc
      let count = 1;

      // 向一个方向检查
      for (let i = 1; i < 5; i++) {
        let r = row + dr * i;
        let c = col + dc * i;
        if (r >= 0 && r < this.gridSize && c >= 0 && c < this.gridSize && this.board[r][c] === this.currentPlayer) {
          count++;
        } else {
          break;
        }
      }

      // 向另一个方向检查
      for (let i = 1; i < 5; i++) {
        let r = row - dr * i;
        let c = col - dc * i;
        if (r >= 0 && r < this.gridSize && c >= 0 && c < this.gridSize && this.board[r][c] === this.currentPlayer) {
          count++;
        } else {
          break;
        }
      }

      // 如果连续五个相同的棋子,则胜利
      if (count >= 5) {
        return true;
      }
    }

    return false;
  }
  // 初始化游戏
  initGame = () => {
    this.board = []
    for (let index = 0; index < this.gridSize; index++) {
      const arr: number[] = []
      for (let index2 = 0; index2 < this.gridSize; index2++) {
        arr.push(0)

      }
      this.board.push(arr)

    }
    this.currentPlayer = 1;
    this.gameOver = false;
    this.textContent = '轮到黑子落子';
    this.drawBoard();
  }

  build() {
    Stack({ alignContent: Alignment.TopStart }) {
      Canvas(this.ctx)
        .width(this.canwidth)
        .height(this.canwidth)
        .backgroundColor(Color.Orange)
        .onReady(() => {
          this.cellSize = this.canwidth / this.gridSize;
          this.radius = this.cellSize / 2 - 5; // 棋子的半径
          this.initGame()
        })
        .onClick(
          this.handleClick
        )

      Text(this.textContent)
        .fontSize(14)
        .padding(5)
        .fontColor(Color.White)
        .fontWeight(700)

    }
    .width("100%")
    .height("100%")
  }
}

调整卡片的图标和名字

主要业务开发完毕了,可以调整卡片的展示信息

image-20250105160500387

这部分信息在 entry/src/main/resources/base/profile/form_config.json中配置:

  1. displayName 标题
  2. description 简介
{
  "forms": [
    {
      "name": "widget",
      "displayName": "$string:widget_display_name",
      "description": "$string:widget_desc",
      "src": "./ets/widget/pages/WidgetCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDynamic": true,
      "isDefault": true,
      "updateEnabled": false,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "4*4",
      "supportDimensions": [
        "2*2",
        "4*4"
      ]
    }
  ]
}

发布上架

最后,如果要将卡片发布上架,还需要做一些小处理

  1. 设置你的元服务的展示图标
  2. 配置证书
  3. 打包成 Hap
  4. 在 AGC 平台上发布上架等等
  5. 具体流程可以参考底部的文章

参考链接

  1. 卡片开发
  2. HarmonyOS Next 实战卡片开发 01
  3. HarmonyOS Next 实战卡片开发 02
  4. HarmonyOS Next 实战卡片开发 03
  5. HarmonyOS Next 最新 元服务新建到上架全流程

代码仓库

https://gitee.com/ukSir/laughing-at-gomoku

总结

至此,笑笑五子棋的开发上架流程已经完毕。

如果你兴趣想要了解更多的鸿蒙应用开发细节和最新资讯,欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。