antd-mobile-icon for harmony

366 阅读3分钟

为了能够直接从低代码 dsl 生成鸿蒙代码,所以在对标 antd-mobile 上会很严格,因此实现上可能会很怪,但它可能不是一个真正好的鸿蒙组件库

思路

antd-mobile 的 Icon 组件在新版本中使用 svgr 渲染,因此每一个图标都是一个独立的组件,这在减少尺寸包大小方面作用很大。

import React from 'react'
import { AntOutline, ArrowDownCircleOutline } from 'antd-mobile-icons'
import { DemoBlock } from 'demos'
import { Space } from 'antd-mobile'

export default () => {
  return (
    <>
      <DemoBlock title='基础用法'>
        <Space wrap style={{ fontSize: 36 }}>
          <AntOutline />
          <ArrowDownCircleOutline />
        </Space>
      </DemoBlock>
    </>
  )
}

但在鸿蒙上 Icon 其实就是 Image 组件加载了图片资源,只要将图片放在 icons/src/main/resources/base/media 路径下,就可以在页面中使用 $r('app.media.AaOutline') 取到图片资源,返回类型为 Resource。因此在鸿蒙上的 Icon 的实现又回到了 antd-mobile@2 的老路上了,使用类似 Icon.type 的方式来指定图标类型

import React from 'react'
import { Icon } from 'antd-mobile'

export default () => {
  return (
    <>
      <Icon type="search" size={40}>
    </>
  )
}

获取需要的资源文件

所以需求就变的简单了,看起来都是一些无脑操作,将 svg 文件放到 icons/src/main/resources/base/media 然后在 Index.ets 导出即可。

然而事情并不简单,antd-mobile-icons 的开源仓库中并没有提供 svg 源文件,只有通过脚本生成后的 React 组件。

import * as React from "react";

function AaOutline(props) {
  return /*#__PURE__*/React.createElement("svg", Object.assign({
    width: "1em",
    height: "1em",
    viewBox: "0 0 48 48",
    xmlns: "http://www.w3.org/2000/svg",
    xmlnsXlink: "http://www.w3.org/1999/xlink"
  }, props, {
    style: Object.assign({
      verticalAlign: '-0.125em'
    }, props.style),
    className: ['antd-mobile-icon', props.className].filter(Boolean).join(' ')
  }), /*#__PURE__*/React.createElement("g", {
  }, /*#__PURE__*/React.createElement("g", {
    id: "AaOutline-\u7F16\u7EC4"
  }, /*#__PURE__*/React.createElement("rect", {
    id: "AaOutline-\u77E9\u5F62",
    fill: "#FFFFFF",
    opacity: 0,
    x: 0,
    y: 0,
    width: 48,
    height: 48
  }), /*#__PURE__*/React.createElement("path", {
    d: "",
    id: "AaOutline-\u5F62\u72B6\u7ED3\u5408",
    fill: "currentColor",
    fillRule: "nonzero"
  }))));
}

export default AaOutline;

因此我又写了一个脚本把上面的 React 组件反向跑成 svg 文件(为了阅读体验,只有部分代码,不可使用)

<svg width="1em" height="1em" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink" class="antd-mobile-icon" style="vertical-align: -0.125em;">
    <g id="AaOutline-AAOutline" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="AaOutline-编组">
            <rect id="AaOutline-矩形" fill="#FFFFFF" opacity="0" x="0" y="0" width="48" height="48">
            </rect>
        </g>
    </g>
</svg>

然而有 151 个组件,还要识别文件名,脚本写起来不畅快。因此我又直接从官网把渲染后的 svg 拷贝下来。

代码如:

<div class="adm-grid" style="--columns: 7;">
  <div class="adm-grid-item adm-icon-doc-item" style="--item-span: 1;">
    <div class="adm-icon-doc-icon">
      <svg width="1em" height="1em" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="antd-mobile-icon" style="vertical-align: -0.125em;">
        <g id="AaOutline-AAOutline" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
          <g id="AaOutline-编组">
            <rect id="AaOutline-矩形" fill="#FFFFFF" opacity="0" x="0" y="0" width="48" height="48"></rect>
          </g>
        </g>
      </svg>
    </div>
  <div class="adm-icon-doc-label">AaOutline</div>
</div>

写一个简单的脚本就能获得,我要的所有的文件

import fs from 'fs';
import path from 'path';
import cheerio from 'cheerio';

function gAntIcons(str: string, dir: string) {
    const $ = cheerio.load(str);
    $('.adm-grid-item').each((index, element) => {
        const label = $(element).find('.adm-icon-doc-label').text().trim();
        const svg = $(element).find('svg').parent().html();
        const filePath = path.join(__dirname, dir, `${label}.svg`);
        fs.writeFileSync(filePath, svg || '<svg></svg>');
    });
}

gAntIcons(html, 'svg');
gAntIcons(html2, 'fill');

生成鸿蒙代码

把文件区分在两个文件夹中是因为图标分成线框风格和实底风格,下一步可以获取文件夹下的文件清单,就可以跑出鸿蒙组件的代码,也可以在上面 gAntIcons 中做记录,直接生成代码,分开成两个步骤的原因是后续如果需要扩展图标库,只要把新的 svg 文件放到相应目录下,跑一下脚本就行。因为 antd-mobile-icons 几乎不会更新,上面的相当于一次性代码了。

使用 DevEco Studio 新建一个鸿蒙项目,然后在 文件 - 新建 - 模块 里面添加一个 Static LibraryModule name 改成 icons。

生成 icons/src/main/ets/index.ets 文件

export const AaOutline=$r('app.media.AaOutline');
export const AddCircleOutline=$r('app.media.AddCircleOutline');
...
export const VideoOutline=$r('app.media.VideoOutline');

生成 icons/src/main/ets/fill.ets 文件

export const AddressBookFill=$r('app.media.AddressBookFill');
export const AlipayCircleFill=$r('app.media.AlipayCircleFill');`ets
...
export const TeamFill=$r('app.media.TeamFill');

将生成的 svg 图片放到 icons/src/main/resources/base/media 目录下。

然后在主入口文件中 icons/Index.ets 将所有的资源导出。

主入口文件可以在 oh-package.json5 中配置

测试使用

在 entry 中测试使用,新建一个组件

Icon 组件

这个组件也是 antd-mobile for harmony 的 Icon 组件实现

@Component
export struct ADM_Icon {
  /**
   *字体大小
   */
  @Prop fontSize: number = 30
  /**
   *字体颜色
   */
  @Prop color?: ResourceColor = Color.Black;

  @Require @Prop type: Resource

  build() {
    Stack({ alignContent: Alignment.Center }) {
      Image(this.type)
        .width(this.fontSize).height(this.fontSize).fillColor(this.color)
        .interpolation(ImageInterpolation.High)
    }
  }
}

export default ADM_Icon

在 entry 中引入模块

{
  "name": "entry",
  "dependencies": {
    "antd-mobile-icons": "file:../icons"
  }
}

依旧用脚本生成代码 entry/src/main/ets/pages/Index.ets (不然手写151个图标引用有点累)

import { ADM_Icon, } from '../components/Icon'
import {
  AaOutline,
  AddCircleOutline,
  ...
  AddressBookFill,
  AlipayCircleFill,
  ...
} from 'antd-mobile-icons'

@Entry
@Component
struct Index {
  build() {
    Scroll() {
      Flex({
        direction: FlexDirection.Row,
        alignItems: ItemAlign.Start,
        justifyContent: FlexAlign.SpaceBetween,
        wrap: FlexWrap.Wrap
      }) {
        Row() {
          Text("线框风格")
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .margin(10)
        }.width("100%")

        Column() {
          ADM_Icon({ type: AaOutline })
          Text('AaOutline').margin(5)
        }.alignItems(HorizontalAlign.Center).width('50%')

        Column() {
          ADM_Icon({ type: AddCircleOutline })
          Text('AddCircleOutline').margin(5)
        }.alignItems(HorizontalAlign.Center).width('50%')

        Row() {
          Text("实底风格")
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .margin(10)
        }.width("100%")

        Column() {
          ADM_Icon({ type: AddressBookFill })
          Text('AddressBookFill').margin(5)
        }.alignItems(HorizontalAlign.Center).width('50%')

        Column() {
          ADM_Icon({ type: AlipayCircleFill })
          Text('AlipayCircleFill').margin(5)
        }.alignItems(HorizontalAlign.Center).width('50%')
      }.padding({ left: 35, right: 35, top: 35 })
    }.scrollBar(BarState.Auto)

  }
}

渲染图

Screenshot_2025-01-22T165429.png

发布第三方库 antd-mobile-icon

然后根据官方文档将 antd-mobile-icon 发布到 ohpm 上

 ohpm i antd-mobile-icons
import { AaOutline, } from 'antd-mobile-icons'

@Entry
@Component
struct Index {
  build() {
    Column() {
        Image(AaOutline)
        Text('AaOutline').margin(5)
      }.alignItems(HorizontalAlign.Center).width('50%')
  }
}

开源仓库

antd-mobile-icons-harmony