目录
一、概述
声明式UI
应用模型
二、常用组件
1、Image:图片展示组件
示例
配置控制授权申请
2、Text:文本显示组件
示例
3、TextInput:文本输入组件
示例
4、Button:按钮组件
5、Slider:滑动条组件
三、页面布局
1、线性布局组件
主轴对齐
交叉轴对齐
示例
四、循环控制与List
1、forEach
2、List
五、自定义组件
组件构建函数
通用样式封装
方舟开发框架(简称ArkUI)为HarmonyOS应用的UI开发提供了完整的基础设施,包括简洁的UI语法、丰富的UI功能(组件、布局、动画以及交互事件),以及实时界面预览工具等,可以支持开发者进行可视化界面开发。
ArkUI提供了非常多的组件供开发者使用,我们在使用时直接去查看官方文档即可。按钮(Button)-添加常用组件-添加组件-基于ArkTS的声明式开发范式-UI开发-开发-HarmonyOS应用开发
或者我们使用DevEco Studio的时候,将鼠标在一个API上悬停一段时间,就会出现show in Api Reference选项,此时点击就可以查看IDE自带的官方API文档,这个文档与官方文档是一摸一样的,非常好用。
我们接下来使用的是声明式开发范式,当然官方也推荐这种开发方式
那什么是声明式开发?
与声明式开发相对的就是命令式开发,我们先来看一段代码
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 命令式编程方式:计算所有偶数的平方和
int evenSquaredSumImperative = 0;
for (int num : numbers) {
if (num % 2 == 0) {
evenSquaredSumImperative += num * num;
}
}
System.out.println("Imperative: " + evenSquaredSumImperative);
在这段Python代码中,它的目的是计算所有偶数的平方和,最终打印输出。我们要实现这个功能就要思考怎么去实现,并最终使用代码逻辑去完成这个功能。
但是我们现在使用声明式开发
int evenSquaredSumDeclarative = numbers.stream()
.filter(num -> num % 2 == 0)
.mapToInt(num -> num * num)
.sum();
System.out.println("Declarative: " + evenSquaredSumDeclarative);
我们直接调用了Java的流式API来进行实现,不用你去实现这个功能的底层逻辑,你只需要思考怎么去使用这些API来完成你需要的功能即可。
我们再来看我们以后使用的ArkUI开发代码
Text('hello')
.fontSize(this.size)
Image('test.jpg')
.width(this.count % 2 === 0 ? 100 : 200)
.height(this.offset + 100)
在这样的代码中,我们几乎看不到任何操作UI更新的代码,而这正是声明式UI的特点,它侧重于描述做什么,而不是怎么做,开发者只需要关注UI应该如何呈现,而不需要关心UI的具体实现过程。
开发者要做的,就只是提供不同UI与不同状态之间的映射关系,而无需编写如何在不同UI之间进行切换的代码。
随着系统的演进发展,HarmonyOS先后提供了两种应用模型:
FA(Feature Ability)模型:HarmonyOS早期版本开始支持的模型,已经不再主推。
Stage模型:HarmonyOS 3.1 Developer Preview版本开始新增的模型,是目前主推且会长期演进的模型。在该模型中,由于提供了AbilityStage、WindowStage等类作为应用组件和Window窗口的“舞台”,因此称这种应用模型为Stage模型。
Stage模型与FA模型最大的区别在于:Stage模型中,多个应用组件共享同一个ArkTS引擎实例;而FA模型中,每个应用组件独享一个ArkTS引擎实例。因此在Stage模型中,应用组件之间可以方便的共享对象和状态,同时减少复杂应用运行对内存的占用。Stage模型作为主推的应用模型,开发者通过它能够更加便利地开发出分布式场景下的复杂应用。
总之,我们以后的开发发使用也会基于Stage模型进行开发
(1)、声明Image组件并设置图片源
Image(src: string | PixelMap | Resource)
Image('https://xxx.png')
Image(pixelMapObject)
有两个目录可以用来存放本地图片,media文件夹和rawfile文件夹
//读取media文件夹下的图片,图片后缀可省略
Image($r{'app.media.mate60'})
//读取rawfile文件夹下的图片,图片后缀不可省略
Image($rawfile('mate60.png'))
(2)、添加图片属性
属性 | 说明 |
---|---|
width(100) | 宽度 |
height(120) | 高度 |
borderRadius(10) | 边框圆角 |
interpolation(ImageInterpolation.High) | 图片插值 |
图片插值使用用来消除图片锯齿的,就是能让一个模糊的图片变得平滑清晰
注意:如width()赋值可以是整型100,也可以是字符串'100%'
如果是数字整型100的话就是100vx(虚拟像素),如果是字符串'100%'就是按照图片本身占设备屏幕的比例
你可能会疑问,不是说网络图片需要申请网络访问权限吗,为什么直接就出来了?
这是因为这个是预览图,并不算是真正运行的程序,我们需要启动虚拟机才算是启动程序,这时我们就需要配置授权了
应用的APL(Ability Privilege Level)等级分为normal、system_basic和system_core三个等级,默认情况下,应用的APL等级都为normal等级。权限类型分为system_grant和user_grant两种类型。应用可申请的权限项参见应用权限列表。
需要在module.json5配置文件中声明一下权限即可
其中name是必须要填的,我们这里只使用name查看一下效果
此时启动了虚拟机网络图片也成功加载出来了。
注意:忘记了组件的使用方法和API记得直接查看IDE自带的官方文档
(1)、声明Text组件并设置文本内容
Text(content?: string | Resource)
Text('图片宽度')
Text($r('app.string.width_label'))
解释一下这个是什么意思
同样的$r 符号读取本地文件,app是默认前缀。
我们的resources目录下存放着我们的本地资源,在element目录下有一个叫string的json格式文件,里面存放着一些字符常量。
因此这行代码的意思就是寻找本地资源目录下string.json文件中的名为width_label的json对象
这里有一个点需要注意的是,我们在base外面能看到两个目录,en_US和zh_CN,里面也有element文件夹
这是为了国际化,系统会检测你的使用语言版本来使用不同限定词目录下的element,en_US就是英文系统,zh_CH就是中文系统
但是需要注意的一点是,它们里面存在的属性,在base目录下也要存在才能使用,否则会报错。因为程序会先去base里面找,然后再去限定词目录里面更换值,如果没有该限定词目录,才会使用base里面的默认值。
(2)、添加文本属性
属性 | 说明 |
---|---|
lineHeight(32) | 行高 |
fontSize(20) | 字体大小 |
fontColor('#36D') | 字体颜色 |
fontWeight(FontWeight.Medium) | 字体粗细 |
做一个国际化双语言的文本展示
base
{
"name": "wal",
"value": "An1ong"
}
zh_CN
{
"name": "wal",
"value": "优米"
}
en_US
{
"name": "wal",
"value": "yummy"
}
代码
@Entry
@Component
struct Index {
build() {
Row() {
Column() {
Text($r("app.string.wal"))
.fontColor('#36D')
.fontSize(38)
}
}
}
}
效果
(1)、声明TextInput组件
TextInput({ placeholder?: ResourceStr,text?:ResourceStr})
TextInput({placeholder: '请输入无输入时的提示文本'})
TextInput({text: '默认文本'})
(2)、添加属性和事件
属性 | 作用 |
---|---|
width(150) | 宽 |
height(30) | 高 |
backgroundColor('#FFF') | 背景色 |
type(InputType.Password) | 输入框类型 |
onChange( value => { //value是文本框的内容 }) |
当输入框中的内容发生改变时执行事件 |
这里type输入框类型有好多,有现成的Email格式、Phone格式、Password格式等会对输入的内容进行约束,这样就不用自己写正则表达式来检验了。
@Entry
@Component
struct Index {
@State imgSize: number = 100
build() {
Row() {
Column() {
Image($r('app.media.icon'))
.width(this.imgSize)
.interpolation(ImageInterpolation.High)
Text('此时图标的大小为: ' + this.imgSize)
.fontSize(30)
TextInput({placeholder: '请输入图片的大小'})
.type(InputType.Number)
.width(250)
.height(50)
.onChange(value => {
this.imgSize = parseInt(value)
})
}
}
}
}
(1)、声明Button组件,label是按钮文字
Button(lable?:ResourceStr )
Button("点我")
Button(){
Image($r('app.media.search')).width(20).margin(10)
}
(2)、添加属性和响应
属性 | 说明 |
---|---|
width(100) | 宽 |
height(30) | 高 |
type(ButtonType.Normal) | 按钮类型 |
onClick(()=> { //处理点击事件 }) |
点击事件 |
type属性有三种类型
Capsule | 胶囊型按钮(圆角默认为高度的一半) |
Circle | 圆形按钮 |
Normal | 普通按钮(默认不带圆角) |
(1)、声明一个滑动条组件
Slider(options?:SliderOptions)
参数 | 作用 |
---|---|
min(0) | 最小值 |
max(100) | 最大值 |
value(30) | 当前值 |
step(10) | 滑动步长,默认是1 |
style:SliderStyle.OutSet | InSet就是滑块在滑动条里 |
direction:Axis.Horizontal | 水平滑动,Vertical是垂直滑动 |
reverse:false | 是否反向滑动,就是大小互换一下 |
(2)、添加属性和响应
属性 | 作用 |
---|---|
width('90%') | 大小 |
trackThickness(7) | 滑动条粗细 |
blockColor() | 滑块的颜色 |
trackColor() | 滑轨的颜色 |
showTips(true) | 是否展示value百分比提示 |
onChange(value => { //value就是当前值 }) |
滑动触发事件 |
我们之前的例子中用了很多次Row()与Column(),它们就是线性布局组件
Column()是列容器,Row()是行容器
仅仅这样使用是不够的,我们在组装一个页面的时候为了美观都会让各个组件对齐或是居中等等
属性 | 作用 | 参数 |
---|---|---|
justifyContent | 设置子元素在主轴方向的对齐格式 | FlexAlign枚举 |
alignItems | 设置子元素在交叉轴方向的对齐格式 | Row容器使用VerrticalAlign枚举 Column容器使用HorizontalAlign枚举 |
FlexAlign的对齐方式也有很多
Column容器
Row容器
Column容器
Row容器
这里有一个点需要注意,因为布局容器的大小是默认自适应组件的大小的,所以如果你不修改width和height属性就直接使用布局对齐的话是没有效果的,比如这里我这里明明使用了布局对齐,但是按钮的格式并没有改变
我们添加一个背景色看看是咋回事
可以看到Row的大小默认就是这两个按钮所占的大小,所以首先要调整一下Row容器的大小
现在我们来综合以上学过的组件和布局来写一个案例
@Entry
@Component
struct Index {
@State imgSize: number = 100
build() {
Column() {
Row(){
Image($r("app.media.app_icon"))
.width(this.imgSize)
}
.height(350)
Row(){
Text($r("app.string.image_width"))
.fontSize(25)
.fontWeight(FontWeight.Bold)
TextInput({text: this.imgSize.toFixed(0)})
.width(200)
.type(InputType.Number)
.onChange(value => {
this.imgSize = parseInt(value)
})
}
.width('100%')
.padding({left: 20,right: 20})
Divider()
.width("91%")
Row(){
Button("缩小")
.type(ButtonType.Capsule)
.onClick(() => {
if (this.imgSize > 0) {
this.imgSize -= 10
}
})
Button("扩大")
.type(ButtonType.Normal)
.onClick(() => {
if(this.imgSize < 300){
this.imgSize += 10
}
})
}
.width("100%")
.justifyContent(FlexAlign.SpaceEvenly)
.margin({top: 35,bottom:35})
Row(){
Slider({min: 0,max: 300, style: SliderStyle.InSet,step: 10,value: this.imgSize})
.width("90%")
.showTips(true)
.trackThickness(20)
.onChange(value => {
this.imgSize = value
})
}
}
.width("100%")
.height("100%")
}
}
比如我们要写一个商品列表,但是有100条商品要展示,每一个里面都要写布局和重复的组件。一个一个手动写的话未免也太麻烦了,于是我们可以直接使用forEach函数来循环生成
一般键生成函数不需要我们重写,都用默认的就行
刚才使用forEach的时候循环打印了所有的数据,你会一个Column容器根本装不下所有的数据。因此为了实现滚动查看多条信息的功能,我们使用List容器
没错,他就是来替代Column容器的。
不过要注意Listitem不是容器,List才是,所以ListItem中只能写一个根组件,如果要使用多个组件的话要用别的容器将它们包裹起来
就像函数一样,我们将组件封装起来,这样就可以复用了
我们的index.ets组件上有一个@Entry注解,这代表它是一个入口组件,一开始就会展示这个组件。
其实将它去掉就是一个自定义组件了
当然我们不会像这样一样将所有的Component写在一个文件里,我们会分开为个个文件来使用
记得export导出
引入
当然,也可以直接将这些组件封装成一个函数来使用
这两个都是写在@Component外面的,如果要写在里面就把function关键字去掉就行了
如果是只是Text或是别的组件生效的话,就这样写,不过这样的就不能写在@Component里面了,只能写在外面