一、引言:一起来啃源码,解锁HarmonyOS NEXT的"组件密码"!
嘿,小伙伴们!今天想和大家聊一个超实用的开源项目------IBest-UI,一个专为鸿蒙生态打造的轻量级UI组件库。如果你正在开发HarmonyOS NEXT应用,一定遇到过这些痛点:重复造轮子、适配多端界面费时费力、深浅模式切换麻烦......别急,IBest-UI就是来"救场"的!
它有多香?
- 轻量到飞起:核心代码精简,引入即用,绝不给你添负担。
- 主题随心换:深色模式?浅色模式?一行代码切换,适配鸿蒙元服务毫无压力。
- 功能小而美:从按钮到弹窗,从徽章到导航栏,覆盖高频场景,样式参考vant,使用过vant的,都知道vant样式有多好看!
但今天咱们不光是"用组件",而是要打开引擎盖,看看里面的"黑科技" !我们发起一个源码共读计划,目标很简单:
- 拆解设计思想:比如Badge徽标组件,它是怎么实现动态更新、如何优雅适配不同设备?
- 偷师HarmonyOS NEXT:在源码中捕捉ArkTS的高阶用法,学习如何用声明式UI开发"丝滑"应用。
- 边学边玩:欢迎随时抛出问题、提交PR,咱们一起让IBest-UI变得更强大!
无论你是想提升源码阅读能力,还是想摸透鸿蒙开发的门道,这个系列都会是你的"实战指南"。准备好和我一起挖宝了吗?Let's go! 🚀
二、准备工作
看一个开源项目,第一步应该是先看 README.md 再看贡献文档 github/CONTRIBUTING.md。
- 克隆源码
bash
# 克隆gitalb仓库
git clone [email protected]:ibestservices/ibest-ui.git
# 或者克隆gitee仓库
git clone [email protected]:ibestservices/ibest-ui.git
# 进入项目
cd ./ibest-ui
# 安装依赖
ohpm install
- 查看目录结构
根据贡献文档,可以了解到目录结构
csharp
├── entry # 例子hap包
│ └── src
│ ├── main
│ │ ├── ets
│ │ │ ├── assets
│ │ │ │ └── styles # 例子页面样式
│ │ │ ├── components # 例子组件
│ │ │ ├── entryability
│ │ │ └── pages # 例子页面
│ │ └── resources
│ │ ├── base
# ...
├── hvigor
├── library # 组件库
│ └── src
│ └── main
│ ├── ets
│ │ ├── assets
│ │ │ └── ets # 工具方法
│ │ ├── components # 组件目录
│ │ │ ├── button
│ │ │ ├── cell
│ │ │ └── ...
│ │ └── theme-chalk # 样式变量
│ │ └── src
│ └── resources # 组件库资源
│ ├── base
# ...
根据目录,可以了解到,开发的组件、修复bug,主要是在library里进行开发,entry/main/ets/pages里做组件例子页面。
全局样式变量在 library/src/theme-chalk/...
里定义
三、快速找到源代码位置
可以通过快捷方式 Ctrl+Shift+N
转到文件,输入自己所想找到的文件
根据前面的目录结构,我们已经知道了entry是样例文件,library是组件库文件,那么我们要找的文件,就是在 library\src\main\ets\components\badge
里
四、源码解析
进到文件里,我们可以看到有三个文件
color.est文件定义了相关样式,可以看到,在 library\src\main\resources\base\element\color.json
读取样式,好处理全局样式
css
interface IBestBadgeColorType {
badgeBgColor: ResourceColor
textColor: ResourceColor
}
export const IBestBadgeColor: IBestBadgeColorType = {
badgeBgColor: $r("app.color.ibest_badge_background"),
textColor: $r("app.color.ibest_badge_text_color")
}
index.type.ets文件定义了IBestBadgePosition的类型
css
/**
* 徽标位置
*/
export type IBestBadgePosition = "top-left" | "top-right" | "bottom-left" | "bottom-right"
接下来就看下主文件index.est
less
// 获取全局样式
import { getDefaultBaseStyle, IBEST_UI_NAMESPACE } from "../../theme-chalk/src"
import { IBestUIBaseStyleObjType } from "../../theme-chalk/src/index.type"
// 根据单位转换尺寸 用于框架固定尺寸格式化 带单位
import { convertDimensionsWidthUnit } from "../../utils/utils"
// 获取样式
import { IBestBadgeColor } from "./color"
import { IBestBadgePosition } from "./index.type"
/**
* IBestBadge组件用于展示徽标,可以显示徽标内容、背景色、位置等
*/
@Component
export struct IBestBadge {
/**
* 全局公共样式
*/
@StorageLink(IBEST_UI_NAMESPACE) baseStyle: IBestUIBaseStyleObjType = getDefaultBaseStyle()
/**
* 徽标内容
*/
@Prop content: string | number = ''
/**
* 徽标背景色
*/
@Prop color: ResourceColor = IBestBadgeColor.badgeBgColor
/**
* 是否展示为小红点
*/
@Prop dot: boolean = false
/**
* 最大值,超过最大值会显示 {max}+,仅当 content 为数字时有效
*/
@Prop max: number = -1
/**
* 值为0时是否显示徽标
*/
@Prop showZero: boolean = true
/**
* 徽标位置
*/
@Prop badgePosition: IBestBadgePosition = 'top-right'
/**
* 自定义内容
*/
@BuilderParam defaultBuilder?: CustomBuilder
/**
* 判断徽标是否显示
* @returns boolean 表示徽标是否应该显示
*/
isShow(){
return !(typeof this.content == "number" && this.content == 0 && !this.showZero)
}
/**
* 获取徽标内容
* @returns string 表示要显示的徽标内容
*/
getContent(){
if(typeof this.content == 'number' && this.max > 0 && this.content > this.max){
return this.max + '+'
}
return this.content.toString()
}
/**
* 根据徽标位置获取边缘位置
* @returns Edges 表示徽标的边缘位置
*/
getPosition(): Edges{
switch (this.badgePosition){
case 'top-left':
return {
left: 0,
top: 0
}
case 'top-right':
return {
right: 0,
top: 0
}
case 'bottom-left':
return {
left: 0,
bottom: 0
}
case 'bottom-right':
return {
right: 0,
bottom: 0
}
}
}
/**
* 根据徽标位置获取平移选项
* @returns TranslateOptions 表示徽标的平移选项
*/
getTranslate(): TranslateOptions{
switch (this.badgePosition){
case 'top-left':
return {
x: "-50%",
y: "-50%"
}
case 'top-right':
return {
x: "50%",
y: "-50%"
}
case 'bottom-left':
return {
x: "-50%",
y: "50%"
}
case 'bottom-right':
return {
x: "50%",
y: "50%"
}
}
}
/**
* 构建徽标组件
*/
build() {
Row() {
if (this.defaultBuilder) {
this.defaultBuilder()
}
if (this.dot) {
Text()
.width(convertDimensionsWidthUnit(8))
.aspectRatio(1)
.borderRadius(this.baseStyle.borderRadiusMax)
.backgroundColor(this.color)
.position(this.getPosition())
.translate(this.getTranslate())
} else if(this.isShow()) {
Text(this.getContent())
.constraintSize({ minWidth: convertDimensionsWidthUnit(16) })
.fontColor(IBestBadgeColor.textColor)
.fontSize(convertDimensionsWidthUnit(12, true))
.padding({ left: convertDimensionsWidthUnit(3), right: convertDimensionsWidthUnit(3) })
.backgroundColor(this.color)
.borderRadius(this.baseStyle.borderRadiusMax)
.position(this.getPosition())
.translate(this.getTranslate())
.textAlign(TextAlign.Center)
}
}
}
}
代码解释
这段代码实现了一个名为 IBestBadge
的徽标组件,主要用于展示带有内容、背景色和位置的徽标。以下是详细功能分解:
- 属性定义:
-
baseStyle
:全局公共样式,通过@StorageLink
绑定。cotnten
:徽标内容,可以是字符串或数字。color
:徽标背景色,默认为IBestBadgeColor.badgeBgColor
。dot
:是否显示为小红点。max
:徽标内容的最大值,超过时显示{max}+
。showZero
:当content
为 0 时是否显示徽标。badgePosition
:徽标位置,支持top-left
、top-right
、bottom-left
和bottom-right
。defaultBuilder
:自定义内容的构建器。
- 定义了一系列
props
,包括徽标内容,徽标背景色、是否展示为小红点、徽标位置等。可直接参见文档中的API
属性。
- 方法逻辑:
-
isShow
:判断徽标是否需要显示,当content
为 0 且showZero
为false
时不显示。getContent
:根据max
值限制返回徽标内容,若超出最大值则显示{max}+
。getPosition
:根据badgePosition
返回徽标的边缘位置(如top-left
对应{left: 0, top: 0}
)。getTranslate
:根据badgePosition
返回徽标的平移选项(如top-left
对应{x: "-50%", y: "-50%"}
)。build
:构建徽标组件,优先渲染defaultBuilder
内容;若为小红点模式,则创建小红点;否则根据isShow
和getContent
渲染普通徽标。
- 渲染逻辑:
-
- 若
dot
为true
,渲染一个小红点。 - 若
isShow
返回true
,渲染普通徽标,并应用内容、样式和位置。
- 若
控制流图
这个Badge组件,源码很简单,不复杂(相关代码解释已在源码文件里写好了),为什么解析这么简单的组件,主要是想着由简入深,先熟悉下HarmonyOS NEXT的ArkTs的写法。
五、 自定义主题样式
其中,我们看组件的第一个属性baseStyle
:全局公共样式,通过 @StorageLink
绑定。
@StorageLink,从API version 11开始,该装饰器支持在元服务中使用。在harmonyOS NEXT中,可以适用。
让我们看下官方文档的说明
@StorageLink(key)是和AppStorage中key对应的属性建立双向数据同步:
- 本地修改发生,该修改会被写回AppStorage中。
- AppStorage中的修改发生后,该修改会被同步到所有绑定AppStorage对应key的属性上,包括单向(@StorageProp和通过Prop创建的单向绑定变量)、双向(@StorageLink和通过Link创建的双向绑定变量)变量和其他实例(比如PersistentStorage)。
@StorageLink
要配合着 AppStorage
使用,让我们看看AppStorage
怎么进行初始化
已知初始化方法为AppStorage.setOrCreate(propName, newValue)
,那我们找找看,该方法在哪初始化,可以通过快捷方式 Ctrl+Shift+N
,通过Text,来快速找到初始化的文件
进到文件library\src\main\ets\theme-chalk\src\index.ets
,我们可以找到样式初始化的方法setIBestUIBaseStyle
通过setIBestUIBaseStyle方法,设置全局样式。
ini
/**
* AppStorage命名空间
*/
export const IBEST_UI_NAMESPACE = '__IBEST-UI_BASE_STYLE'
/**
* 设置全局样式
* @param styleData
*/
export function setIBestUIBaseStyle(styleData?: Partial<IBestUIBaseStyleType>) {
const newStyleData = getDefaultBaseStyle();
if (typeof styleData === 'object' && styleData !== null) {
Object.keys(styleData).forEach(item => {
if (newStyleData[item]) {
newStyleData[item] = (styleData as IBestUIBaseStyleObjType)[item] ?? newStyleData[item]
}
})
}
AppStorage.setOrCreate(IBEST_UI_NAMESPACE, newStyleData)
}
IBEST_UI_NAMESPACE
常量
-
- 定义了一个全局命名空间常量,用于标识存储的全局样式数据。
setIBestUIBaseStyle
方法
-
- 该方法用于设置全局样式,接收一个可选参数
styleData
,类型为Partial<IBestUIBaseStyleType>
- 如果传入了有效的
styleData
对象,则将其与默认样式数据合并,覆盖默认值。 - 最终将合并后的样式数据存储到
AppStorage
中,使用命名空间__IBEST-UI_BASE_STYLE
。
- 该方法用于设置全局样式,接收一个可选参数
getDefaultBaseStyle
方法
-
- 该方法用于生成框架默认的主题样式数据,返回一个
IBestUIBaseStyleObjType
类型的对象。 - 数据包括颜色(如主题色、透明度)、间距(如
spaceMini
、spaceBase
)、字体大小、边框半径、行高等多种样式属性。 - 部分值通过
convertDimensionsWidthUnit
方法进行单位转换。
- 该方法用于生成框架默认的主题样式数据,返回一个
yaml
/**
* 框架默认主题
*/
export function getDefaultBaseStyle(): IBestUIBaseStyleObjType {
const data: IBestUIBaseStyleType = {
default: THEME_COLOR.DEFAULT,
primary: THEME_COLOR.PRIMARY,
success: THEME_COLOR.SUCCESS,
warning: THEME_COLOR.WARNING,
danger: THEME_COLOR.DANGER,
primaryOpacity: COLOR_OPACITY.PRIMARY,
successOpacity: COLOR_OPACITY.SUCCESS,
warningOpacity: COLOR_OPACITY.WARNING,
dangerOpacity: COLOR_OPACITY.DANGER,
spaceMini: convertDimensionsWidthUnit(SPACE.MINI),
spaceBase: convertDimensionsWidthUnit(SPACE.BASE),
spaceXs: convertDimensionsWidthUnit(SPACE.XS),
spaceSm: convertDimensionsWidthUnit(SPACE.SM),
spaceMd: convertDimensionsWidthUnit(SPACE.MD),
spaceLg: convertDimensionsWidthUnit(SPACE.LG),
spaceXl: convertDimensionsWidthUnit(SPACE.XL),
fontSizeXs: convertDimensionsWidthUnit(FONT_SIZE.XS, true),
fontSizeSm: convertDimensionsWidthUnit(FONT_SIZE.SM, true),
fontSizeMd: convertDimensionsWidthUnit(FONT_SIZE.MD, true),
fontSizeLg: convertDimensionsWidthUnit(FONT_SIZE.LG, true),
fontSizeXl: convertDimensionsWidthUnit(FONT_SIZE.XL, true),
borderRadiusSm: convertDimensionsWidthUnit(BORDER_RADIUS.SM),
borderRadiusMd: convertDimensionsWidthUnit(BORDER_RADIUS.MD),
borderRadiusLg: convertDimensionsWidthUnit(BORDER_RADIUS.LG),
borderRadiusMax: convertDimensionsWidthUnit(BORDER_RADIUS.MAX),
lineHeightXs: convertDimensionsWidthUnit(LINE_HEIGHT.XS),
lineHeightSm: convertDimensionsWidthUnit(LINE_HEIGHT.SM),
lineHeightMd: convertDimensionsWidthUnit(LINE_HEIGHT.MD),
lineHeightLg: convertDimensionsWidthUnit(LINE_HEIGHT.LG),
// 滚动效果
scrollEdgeEffect: EdgeEffect.Fade,
// 滚动条颜色
scrollBarColor: '#dbdfe6',
animationDuration: 200
}
return data;
}
再让我们找找setIBestUIBaseStyle在那边调用,然后可以发现在library\Index.ets
中将 setIBestUIBaseStyle 方法重命名为 IBestSetUIBaseStyle,并将此方法暴露出去。
在IBest-UI@HarmonyOS-自定义主题样式文档中,可以看到暴露出来的IBestSetUIBaseStyle方法
由小见大,我们知道了,IBest-UI@HarmonyOS组件库,是怎么实现自定义主题样式
六、结语
咱们一起盘盘这个HarmonyOS组件库!
这篇文章带大家拆解了IBest-UI组件库里的Badge徽标组件,顺便摸清了鸿蒙主题定制的套路。简单说就是:组件虽小,五脏俱全 ,特别适合新手练手!
干了啥?
- 组件怎么用?
-
- 能显示数字/文字、变红点、自动截断超长内容(比如"99+")
- 支持四个方位贴牌(左上右上随便钉!)
- 重点 :通过
@StorageLink
同步全局主题,换皮肤只需改配置文件,不用挨个改组件
- 源码里藏着啥宝贝?
-
- 属性全家桶 :
content
(内容)、dot
(小红点开关)、max
(最大值限制)... 想改啥直接传参 - 位置 :用
getPosition()
和getTranslate()
控制徽标位置,自动计算偏移量(再也不用硬编码坐标啦!) - 条件渲染 :
isShow()
判断要不要显示徽标,0值显示开关超贴心
- 属性全家桶 :
- 主题定制怎么玩?
-
- 通过
setIBestUIBaseStyle
统一管理样式变量(颜色/圆角/间距...) - 源码里直接
AppStorage
存全局配置,改一次全组件生效(妈妈再也不用担心我改漏文件!)
- 通过
吐槽&彩蛋
- ArkTS骚操作 :
convertDimensionsWidthUnit
这个工具函数,自动适配不同设备的尺寸单位,鸿蒙生态的"响应式"精髓就在这! - 隐藏关卡 :组件库里还有按钮、导航栏等一堆组件,Badge只是开胃菜,后面可以继续挖坑!
一句话总结 :这组件库把鸿蒙的声明式UI玩明白了,跟着抄作业能少写不少代码,建议收藏!