从零开始开发纯血鸿蒙应用
- 一、题引
- 二、UI 组成
- 三、UI 封装原则
- 四、实现 lib_comps
- 1、封装 UI 样式
- 1.1、attributeModifier 属性
- 1.2、自定义`AttributeModifier<T>`类
- 2、封装 UI 组件
- 五、总结
一、题引
在开始正文前,为了大家能够从本篇博文中,汲取到代码外的东西,即编程思想
,我想问问每一个屏幕前的读者,一个问题,那就是:所谓UI,在你看来,可以划分成哪些组成?
二、UI 组成
UI,即 User Interface
,译为用户界面,是一个应用提供给用户直接进行查看和操作的内容。
虽然,还不知道大家脑海中对于UI组成,都有什么样的看法?但我这里需要说的是,我认为一个合格的UI,当是由结构
、样式
和响应
有机组合成的,而合格的UI封装也应该就此进行。
UI 实际上就像大家日常生活中居住的房子,一个住起来感到惬意的房间,必然先后经历房壳子和装潢后的房间,这两个截然不同的阶段;在装潢阶段,房间的墙壁或被贴上墙纸、或被刷上墙漆,墙壁上的某些位置上出现的各种插座、开关面板,则是早在搭建房壳子的时候一并完成的,然而它们实际会控制什么样的电器,则是在装潢阶段进行的。
三、UI 封装原则
再问大家一个问题:进行UI公共组件的封装时,是习惯于将结构、样式和响应,杂糅在一起进行封装,还是采用粒度更细化的方式进行呢?
我进行UI封装时,会有一个很明确的行为准则:单一职能原则
,一个封装实现体(class或struct),应该只承担粒度从小到大顺序中的某一个职能,比如只负责确定结构、或只负责确定样式。
当然了,前提是所使用的UI开发框架支持相应的粒度抽取。
就鸿蒙应用SDK来说,最新版本的API里,样式已经允许与结构、响应独立,封装在不同的ets文件中,具体如何实现,后文细说;而对于结构和响应,就像造房子一样,墙壁上开关面板的走线管道,只能在墙壁完整造出来前布置好,UI结构封装时必须预留响应的载入通道,通常就是允许传入一个函数
。
四、实现 lib_comps
现在,开始对工程里面的 lib_comps
模块进行实现,先看一下该模块下的目录结构:
在 ets 源码目录下,我细分出 components 和 style两个子目录,前者负责UI结构和响应载入通道的封装,后者负责UI样式的封装。
1、封装 UI 样式
进行网页实现的时候,大家经常会用到类似如下的代码:
<div class="container"><p class="normal-text">文本</p>
</div>
html 标签的 class 属性,用于将提前封装在CSS文件中的各种样式进行载入的位置,只有这样,才可能进行样式的独立封装。现在,我可以明确地告诉大家,鸿蒙API 12 之后也能如此进行了:
1.1、attributeModifier 属性
在我看来,该属性是相当重要而有用的,它让鸿蒙 UI 实现的灵活度得到了进一步的增强。
虽然,官方将其解释为“动态设置组件的属性方法”,但我却更愿意将其看待成鸿蒙组件的class属性,因为,在鸿蒙UI实现中,组件样式也是通过属性方法进行设置的,比如下面:
Row(){Text(this.fileName.split(".")[0]).fontSize(25).fontWeight(FontWeight.Regular)Text(this.fileName.split(".")[1]).fontSize(20).fontWeight(FontWeight.Lighter)Text('操作').fontSize(25).fontWeight(FontWeight.Regular).bindMenu(this.OptionMenu())}.width("100%").height("10%").alignItems(VerticalAlign.Center).justifyContent(FlexAlign.SpaceBetween)
那么,传入 attributeModifier 属性里面的自定义AttributeModifier<T>类
里面,只设置样式相关的属性,那么,实际上对应的自定义AttributeModifier<T>类
就相当是一个css
了。
1.2、自定义AttributeModifier<T>
类
实现时,要与目标组件的类型相一致,也就是说,如果是要为 column 组件的 attributeModifier 属性赋值,那么泛类型 T 对应的具体类型就是 ColumnAttribute。
通常,应用里面使用的 column 样式不会只有一种,所以,不妨把所有适用于 column 的样式封装,都放在同一份 ets 文件中,从而就有了前面 lib_comps 工程目录结构图里面的 ColumnStyles.ets
文件。
下面,以 RootTopColumn 类为例,讲解如何进行自定义AttributeModifier<T>类
的封装:
export class RootTopColumn implements AttributeModifier<ColumnAttribute> {private columnColor: ResourceColor;constructor(columnColor: ResourceColor) {this.columnColor = columnColor;}applyNormalAttribute(instance: ColumnAttribute): void {instance.width("100%").height("100%").backgroundColor(this.columnColor).alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Start)}applyPressedAttribute(instance: ColumnAttribute): void {}applyFocusedAttribute(instance: ColumnAttribute): void {}applyDisabledAttribute(instance: ColumnAttribute): void {}applySelectedAttribute(instance: ColumnAttribute): void {}}
首先,自定义AttributeModifier<T>类
和普通类一样,可以定义字段并通过构造函数进行初始化或赋值;其次,可以一次性封装5种不同的样式
,用以针对组件的普通状态、按下状态、获焦状态、不可用状态和选择状态,不同的状态样式由各自对应的 applyXXXXAttribute 方法实现,这些方法统一接受一个 T 类型参数,对于 Column 组件来说,就是 ColumnAttribute;最后,在具体渲染时调用哪一个 applyXXXXAttribute 方法,由 UI 渲染引擎自行判断,开发者无需关心。
实现每个具体的 applyXXXXAttribute 方法时,可以从样式的如下几方面进行:
- 尺寸:width 属性和 height 属性
- 颜色:背景色 backgroundColor 属性,前景色foregroundColor属性
- 对齐方式:alignItem 属性和 justifyContent 属性
- 边距:外边距 margin 属性和内边距 padding 属性
- 边框:boder 属性
当然了,鸿蒙 UI 组件支持的样式属性绝不止这些,只不过上面这些是比较常用的,基本足够应付大多数UI实现所需的样式了。
当对应的 自定义AttributeModifier<T>类
,需要用在最外层的容器上,那么尺寸就应该是与屏幕尺寸相同,这里的 RootTopColumn 就是这样的,所以,我才会在 applyNormalAttribute 函数中,将尺寸都设置为百分百;同时,因为预期布局方式是水平居中、垂直局始,所以,只剩下背景色需要通过类构造函数进行传入。
2、封装 UI 组件
由于,鸿蒙API中,UI 结构和响应,没办法彻底解耦,所以,结构和响应就没有按照彻底独立的方式进行封装,而是直接上升到组件维度进行封装。
在组件中,按钮和对话框应该算得上是使用频率很高的组件了,所以,有必要为此封装相应的公共组件,以黑色背景的按钮来说,可以用类似如下的代码进行封装:
@Component
export struct BlackBtn {private buttonWidth: Length = "75%"private buttonHeight: Length = 55@Prop @Require text: ResourceStr = ""private textSize: string | number | Resource = 20private action?: () => void = () => {}private btnStyle: BlackButton = new BlackButton(this.buttonWidth, this.buttonHeight)build() {Button() {Text(this.text).WhiteText(this.textSize)}.attributeModifier(this.btnStyle).onClick(this.action)}
}
在鸿蒙UI中,所有的组件都是用注解了 Component 的 struct 体承担实现,并且该 struct 体中必须实现 build 方法,具体的 UI 内容实现放在 build 方法里,build 方法里不允许使用非UI表达式,除了少数允许的条件表达式外的一切逻辑表达式,都不允许直接放在 build 方法体里面。
由于注解了 Component 的 struct 体,跟类一样,允许从外部传入参数,所以,具体的内容和样式参数,都可以从外部传入;当然了,也可以为它们都设置初始化值;如果某个属性值必须要外部传入,可以在相应的字段前用 Require 注解。
对话框的封装,也是差不多的形式:
从上面的代码可以看出,封装UI组件的时候,可以根据内容的多寡,决定是否继续划分出更小的组件。
五、总结
受限于篇幅,我这里并没有将 lib_comps 模块的全部实现代码贴出来,大家不妨前往lib_comps仓库,查看具体的实现代码。
总而言之,在封装UI的时候,应当在遵循单一职能的基础上,按粒度从小到大的形式进行内容拆分和实现,一块砖一块砖地码,万里长城才能码得稳、码得好!