鸿蒙元服务
转载改编自@author 坚果派·红目香薰
第一章 环境与示例
安装包地址:https://developer.huawei.com/consumer/cn/download/
创建元服务项目
安装好的DevEco Studio最新版本后,如图所示创建项目 之后根据指示使用华为账号登录 去创建一个【Register App ID】并在DevEco Studio中引用 创建完毕的初始状态
创建出来文件的作用
AppScope > app.json5:元服务的全局配置信息。
entry :HarmonyOS工程模块,编译构建生成一个HAP。
- src > main > ets:用于存放ArkTS源码。
-
- src > main > ets > entryability:元服务的入口。 -
- src > main > ets > pages:元服务包含的页面。 -
- src > main > resources:用于存放元服务所用到的资源文件,如图形、多媒体、字符串、布局文件等。关于资源文件, -
- src > main > module.json5:模块配置文件。主要包含HAP的配置信息、元服务在具体设备上的配置信息以及元服务的全局配置信息。
- build-profile.json5:当前的模块信息 、编译信息配置项,包括buildOption、targets配置等。
- hvigorfile.ts:模块级编译构建任务脚本,开发者可以自定义相关任务和代码实现。
- oh_modules:用于存放三方库依赖信息。
- build-profile.json5:元服务级配置信息,包括签名signingConfigs、产品配置products等。
- hvigorfile.ts:元服务级编译构建任务脚本。
生成元服务图标
如图所示添加Image Asset 图片要求1024*1024 配置后可以在目录src > main > resources > base > media路径下找到
构建页面
布局的方式有很多种:
- 线性布局 (Row/Column)
- 层叠布局 (Stack)
- 弹性布局 (Flex)
- 相对布局 (RelativeContainer)
- 栅格布局 (GridRow/GridCol)
- 媒体查询 (@ohos.mediaquery)
- 创建列表 (List)
- 创建网格 (Grid/GridItem)
- 创建轮播 (Swiper)
- 选项卡 (Tabs)。 默认为相对布局,今天我们主要以相对布局为主要的例子 RelativeContainer为采用相对布局的容器,支持容器内部的子元素设置相对位置关系,适用于界面复杂场景的情况,对多个子组件进行对齐和排列。子元素支持指定兄弟元素作为锚点,也支持指定父容器作为锚点,基于锚点做相对位置布局。
示例代码
请替换ets/pages/index.ts代码
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
/**
* @author 坚果派·红目香薰
* @date 2024年11月5日13:58:20
*/
@Entry
@Component
struct Index {
@State message: string = '时辰时刻';
@State base_time: Date = new Date(Date.now());
@State str_time: string =
this.formatTime(this.base_time.getHours().toString() + ":" + this.base_time.getMinutes() + ":" + this.base_time.getSeconds());
@State show_change_time: string = this.getShiChen(this.base_time.getHours(),this.base_time.getMinutes());
@State isf : boolean = true;
build() {
RelativeContainer() {
Row() {
Text(this.message)
.id('shiChenShiKe')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}.id('row1').margin({ top: 50 }).justifyContent(FlexAlign.Center).width('100%')
Row() {
Text(this.str_time).fontSize(35).fontColor(Color.Black).fontWeight(FontWeight.Bolder)
}
.backgroundColor('#66CCDD')
.id('row2')
.margin({ top: 150 })
.justifyContent(FlexAlign.Center)
.width('100%')
Row() {
Text(this.show_change_time).fontSize(40).fontColor(Color.Black).fontWeight(FontWeight.Bolder)
}
.backgroundColor('#66CCDD')
.id('row3')
.margin({ top: 250 })
.justifyContent(FlexAlign.Center)
.width('100%')
Row() {
Button('开启时时模式').size({ width: 250, height: 80 }).fontSize(30).onClick(() => {
if(this.isf){
this.isf = false;
setInterval(()=>{this.show_change_time = this.Time_Change_Time();},1000);
}
}).id("btn1").width('100%')
}.id('row4').margin({ top: 350 }).width('100%')
}
.height('100%')
.width('100%')
}
Time_Change_Time():string{
let timeInMs = Date.now();
let dateObj = new Date(timeInMs);
this.str_time = this.formatTime(dateObj.getHours() + ":" + dateObj.getMinutes() + ":" + dateObj.getSeconds());
return this.getShiChen(dateObj.getHours(), dateObj.getMinutes());
}
formatTime(timeStr: string): string {
let timeParts = timeStr.split(':');
let formattedParts = timeParts.map((part) => {
let num = parseInt(part);
return num < 10? `0${num}` : `${num}`;
});
return formattedParts.join(':');
}
aboutToAppear() {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
this.loginWithHuaweiID();
}
convertToChineseTimes(hour: number): string {
if (23 <= hour || hour < 1) {
return "子时";
} else if (1 <= hour && hour < 3) {
return "丑时";
} else if (3 <= hour && hour < 5) {
return "寅时";
} else if (5 <= hour && hour < 7) {
return "卯时";
} else if (7 <= hour && hour < 9) {
return "辰时";
} else if (9 <= hour && hour < 11) {
return "巳时";
} else if (11 <= hour && hour < 13) {
return "午时";
} else if (13 <= hour && hour < 15) {
return "未时";
} else if (15 <= hour && hour < 17) {
return "申时";
} else if (17 <= hour && hour < 19) {
return "酉时";
} else if (19 <= hour && hour < 21) {
return "戌时";
} else {
return "亥时";
}
}
convertToChineseTime(hour: number, minute: number): string {
let chineseHour = this.convertToChineseTimes(hour);
let minuteMark = hour % 2 !== 0 ? "上" : "下";
if (minute < 15) {
minuteMark += "一刻";
} else if (minute < 30) {
minuteMark += "二刻";
} else if (minute < 45) {
minuteMark += "三刻";
} else {
minuteMark += "四刻";
}
return `${chineseHour}${minuteMark}`;
}
getShiChen(hour: number, minute: number): string {
return this.convertToChineseTime(hour, minute);
}
/**
* Sample code for using HUAWEI ID to log in to atomic service.
* According to the Atomic Service Review Guide, when a atomic service has an account system,
* the option to log in with a HUAWEI ID must be provided.
* The following presets the atomic service to use the HUAWEI ID silent login function.
* To enable the atomic service to log in successfully using the HUAWEI ID, please refer
* to the HarmonyOS HUAWEI ID Access Guide to configure the client ID and fingerprint certificate.
*/
private loginWithHuaweiID() {
// Create a login request and set parameters
let loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
// Whether to forcibly launch the HUAWEI ID login page when the user is not logged in with the HUAWEI ID
loginRequest.forceLogin = false;
// Execute login request
let controller = new authentication.AuthenticationController();
controller.executeRequest(loginRequest).then((data) => {
let loginWithHuaweiIDResponse = data as authentication.LoginWithHuaweiIDResponse;
let authCode = loginWithHuaweiIDResponse.data?.authorizationCode;
// Send authCode to the backend in exchange for unionID, session
}).catch((error: BusinessError) => {
hilog.error(0x0000, 'testTag', 'error: %{public}s', JSON.stringify(error));
if (error.code == authentication.AuthenticationErrorCode.ACCOUNT_NOT_LOGGED_IN) {
// HUAWEI ID is not logged in, it is recommended to jump to the login guide page
}
});
}
}
结果如图所示
创建虚拟机
选择右上角的【虚拟机】下拉按钮,点击【Device Manager】,创建模拟器【New Emulator】,下载目标虚拟机的包,下载完毕后点击下一步即可。 启动虚拟机
新建元服务卡片
操作顺序:File>New > Service Widget > Dynamic Widget,如图所示,选择4*4即可 结果如图所示
第二章页面与布局
页面创建
根据上个章节创建的项目,初始完毕后可以看到只有一个page的index.ets页面,我们已经有了第一页面,我们要继续创建一个页面。操作步骤【pages(鼠标右键)>New>ArkTS File】 输入ArkTS File名称,并创建完毕 这里将【Index.ets】的内容赋值到【SecondPage.ets】中,改一下结构体名称与message的字符串。 这样我们就有两个页面了
router 路由
我们将页面的信息都写到【src>main>resources>base>main_pages.json】中。
示例代码
页面1:index.ets
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { router } from '@kit.ArkUI'
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
Button("跳转第二页").width('100%').height(80).onClick((event: ClickEvent) => {
router.pushUrl({ url: 'pages/SecondPage' })
}).margin({ top: 200 })
}
.height('100%')
.width('100%')
}
aboutToAppear() {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
this.loginWithHuaweiID();
}
/**
* Sample code for using HUAWEI ID to log in to atomic service.
* According to the Atomic Service Review Guide, when a atomic service has an account system,
* the option to log in with a HUAWEI ID must be provided.
* The following presets the atomic service to use the HUAWEI ID silent login function.
* To enable the atomic service to log in successfully using the HUAWEI ID, please refer
* to the HarmonyOS HUAWEI ID Access Guide to configure the client ID and fingerprint certificate.
*/
private loginWithHuaweiID() {
// Create a login request and set parameters
let loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
// Whether to forcibly launch the HUAWEI ID login page when the user is not logged in with the HUAWEI ID
loginRequest.forceLogin = false;
// Execute login request
let controller = new authentication.AuthenticationController();
controller.executeRequest(loginRequest).then((data) => {
let loginWithHuaweiIDResponse = data as authentication.LoginWithHuaweiIDResponse;
let authCode = loginWithHuaweiIDResponse.data?.authorizationCode;
// Send authCode to the backend in exchange for unionID, session
}).catch((error: BusinessError) => {
hilog.error(0x0000, 'testTag', 'error: %{public}s', JSON.stringify(error));
if (error.code == authentication.AuthenticationErrorCode.ACCOUNT_NOT_LOGGED_IN) {
// HUAWEI ID is not logged in, it is recommended to jump to the login guide page
}
});
}
}
页面2:SecondPage.ets
@Entry
@Component
struct SecondPage {
@State message: string = '第二个页面';
build() {
RelativeContainer() {
Text(this.message)
.id('SecondPage')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
Button("跳转第一页").width('100%').height(80).onClick((event: ClickEvent) => {
router.pushUrl({ url: 'pages/Index' })
}).margin({ top: 200 })
}
.height('100%')
.width('100%')
}
}
效果:
布局
布局结构
布局通常为分层结构,一个常见的页面结构如下所示:
选择布局
声明式UI提供了以下10种常见布局,开发者可根据实际应用场景选择合适的布局进行页面开发。 | 布局 | 应用场景 | | --------------------------------- | ------------------------------------------------------------ | | 线性布局(Row、Column) | 如果布局内子元素超过1个时,且能够以某种方式线性排列时优先考虑此布局。 | | 层叠布局(Stack) | 组件需要有堆叠效果时优先考虑此布局。层叠布局的堆叠效果不会占用或影响其他同容器内子组件的布局空间。例如Panel作为子组件弹出时将其他组件覆盖更为合理,则优先考虑在外层使用堆叠布局。 | | 弹性布局(Flex) | 弹性布局是与线性布局类似的布局方式。区别在于弹性布局默认能够使子组件压缩或拉伸。在子组件需要计算拉伸或压缩比例时优先使用此布局,可使得多个容器内子组件能有更好的视觉上的填充效果。 | | 相对布局(RelativeContainer) | 相对布局是在二维空间中的布局方式,不需要遵循线性布局的规则,布局方式更为自由。通过在子组件上设置锚点规则(AlignRules)使子组件能够将自己在横轴、纵轴中的位置与容器或容器内其他子组件的位置对齐。设置的锚点规则可以天然支持子元素压缩、拉伸、堆叠或形成多行效果。在页面元素分布复杂或通过线性布局会使容器嵌套层数过深时推荐使用。 | | 栅格布局(GridRow、GridCol) | 栅格是多设备场景下通用的辅助定位工具,可将空间分割为有规律的栅格。栅格不同于网格布局固定的空间划分,可以实现不同设备下不同的布局,空间划分更随心所欲,从而显著降低适配不同屏幕尺寸的设计及开发成本,使得整体设计和开发流程更有秩序和节奏感,同时也保证多设备上应用显示的协调性和一致性,提升用户体验。推荐内容相同但布局不同时使用。 | | 媒体查询(@ohos.mediaquery) | 媒体查询可根据不同设备类型或同设备不同状态修改应用的样式。例如根据设备和应用的不同属性信息设计不同的布局,以及屏幕发生动态改变时更新应用的页面布局。 | | 列表(List) | 使用列表可以高效地显示结构化、可滚动的信息。在ArkUI中,列表具有垂直和水平布局能力和自适应交叉轴方向上排列个数的布局能力,超出屏幕时可以滚动。列表适合用于呈现同类数据类型或数据类型集,例如图片和文本。 | | 网格(Grid) | 网格布局具有较强的页面均分能力、子元素占比控制能力。网格布局可以控制元素所占的网格数量、设置子元素横跨几行或者几列,当网格容器尺寸发生变化时,所有子元素以及间距等比例调整。推荐在需要按照固定比例或者均匀分配空间的布局场景下使用,例如计算器、相册、日历等。 | | 轮播(Swiper) | 轮播组件通常用于实现广告轮播、图片预览等。 | | 选项卡(Tabs) | 选项卡可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。 |
布局位置
position、offset等属性影响了布局容器相对于自身或其他组件的位置。 | 定位能力 | 使用场景 | 实现方式 | | -------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | 绝对定位 | 对于不同尺寸的设备,使用绝对定位的适应性会比较差,在屏幕的适配上有缺陷。 | 使用【position】实现绝对定位,设置元素左上角相对于父容器左上角偏移位置。在布局容器中,设置该属性不影响父容器布局,仅在绘制时进行位置调整。 | | 相对定位 | 相对定位不脱离文档流,即原位置依然保留,不影响元素本身的特性,仅相对于原位置进行偏移。 | 使用【offset】可以实现相对定位,设置元素相对于自身的偏移量。设置该属性,不影响父容器布局,仅在绘制时进行位置调整。 |
构建布局
线性布局
Column
示例代码
@Component
struct TestPage {
build() {
Column(){
Text("一帆風順").fontSize(15).width(65).height(50).backgroundColor(Color.Red)
Text("二龍騰飛").fontSize(15).width(65).height(50).backgroundColor(Color.Gray)
Text("三羊開泰").fontSize(15).width(65).height(50).backgroundColor(Color.Yellow)
Text("四季平安").fontSize(15).width(65).height(50).backgroundColor(Color.Blue)
Text("五福臨門").fontSize(15).width(65).height(50).backgroundColor(Color.White)
}.backgroundColor("#66CCFF").width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
效果:
ROW
示例代码
@Component
struct TestPage {
build() {
Row(){
Text("一帆風順").fontSize(15).width(65).height(50).backgroundColor(Color.Red)
Text("二龍騰飛").fontSize(15).width(65).height(50).backgroundColor(Color.Gray)
Text("三羊開泰").fontSize(15).width(65).height(50).backgroundColor(Color.Yellow)
Text("四季平安").fontSize(15).width(65).height(50).backgroundColor(Color.Blue)
Text("五福臨門").fontSize(15).width(65).height(50).backgroundColor(Color.White)
}.backgroundColor("#66CCFF").width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
效果图
排列布局
布局子元素在排列方向上的间距
在布局容器内,可以通过【space】属性设置排列方向上子元素的间距,使各子元素在排列方向上有等间距效果。 示例代码
@Component
struct TestPage {
build() {
Column({ space: 7 }){
Text("一帆風順").fontSize(15).width(65).height(50).backgroundColor(Color.Red)
Text("二龍騰飛").fontSize(15).width(65).height(50).backgroundColor(Color.Gray)
Text("三羊開泰").fontSize(15).width(65).height(50).backgroundColor(Color.Yellow)
Text("四季平安").fontSize(15).width(65).height(50).backgroundColor(Color.Blue)
Text("五福臨門").fontSize(15).width(65).height(50).backgroundColor(Color.White)
}.backgroundColor("#66CCFF").width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
效果图 示例代码
@Component
struct TestPage {
build() {
Row({ space: 7 }){
Text("一帆風順").fontSize(15).width(65).height(50).backgroundColor(Color.Red)
Text("二龍騰飛").fontSize(15).width(65).height(50).backgroundColor(Color.Gray)
Text("三羊開泰").fontSize(15).width(65).height(50).backgroundColor(Color.Yellow)
Text("四季平安").fontSize(15).width(65).height(50).backgroundColor(Color.Blue)
Text("五福臨門").fontSize(15).width(65).height(50).backgroundColor(Color.White)
}.backgroundColor("#66CCFF").width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
效果图
容器内子元素在水平方向上的排列
这里分为Column于Row两个方向的排序。 Column用到的对象是HorizontalAlign,其属性有3: HorizontalAlign.Start:子元素在水平方向左对齐。 HorizontalAlign.Center:默认为居中,子元素在水平方向居中对齐。 HorizontalAlign.End:子元素在水平方向右对齐。 Column示例 示例代码
@Component
struct TestPage {
build() {
Column({ space: 7 }){
Text("一帆風順").fontSize(15).width(65).height(50).backgroundColor(Color.Red)
Text("二龍騰飛").fontSize(15).width(65).height(50).backgroundColor(Color.Gray)
Text("三羊開泰").fontSize(15).width(65).height(50).backgroundColor(Color.Yellow)
Text("四季平安").fontSize(15).width(65).height(50).backgroundColor(Color.Blue)
Text("五福臨門").fontSize(15).width(65).height(50).backgroundColor(Color.White)
}.backgroundColor("#66CCFF").width('100%').height('100%').justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Start)
}
}
效果图
布局子元素在主轴上的排列方式:Column
在布局容器内,可以通过justifyContent属性设置子元素在容器主轴上的排列方式。可以从主轴起始位置开始排布,也可以从主轴结束位置开始排布,或者均匀分割主轴的空间。 justifyContent(FlexAlign.Start):元素在垂直方向方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。 justifyContent(FlexAlign.Center):元素在垂直方向方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。 justifyContent(FlexAlign.End):元素在垂直方向方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。 justifyContent(FlexAlign.SpaceBetween):垂直方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。 justifyContent(FlexAlign.SpaceAround):垂直方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。 justifyContent(FlexAlign.SpaceEvenly):垂直方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。 示例代码
@Component
struct TestPage {
build() {
Column({ space: 7 }){
Text("一帆風順").fontSize(15).width(65).height(50).backgroundColor(Color.Red)
Text("二龍騰飛").fontSize(15).width(65).height(50).backgroundColor(Color.Gray)
Text("三羊開泰").fontSize(15).width(65).height(50).backgroundColor(Color.Yellow)
Text("四季平安").fontSize(15).width(65).height(50).backgroundColor(Color.Blue)
Text("五福臨門").fontSize(15).width(65).height(50).backgroundColor(Color.White)
}.backgroundColor("#66CCFF").width('100%').height('100%').justifyContent(FlexAlign.Start)
}
}
示例图
布局子元素在主轴上的排列方式:Row
justifyContent(FlexAlign.Start):元素在水平方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。 justifyContent(FlexAlign.Center):元素在水平方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。 justifyContent(FlexAlign.End):元素在水平方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。 justifyContent(FlexAlign.SpaceBetween):水平方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。 justifyContent(FlexAlign.SpaceAround):水平方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。 justifyContent(FlexAlign.SpaceEvenly):水平方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。 示例代码
@Component
struct TestPage {
build() {
Row({ space: 7 }){
Text("一帆風順").fontSize(15).width(65).height(50).backgroundColor(Color.Red)
Text("二龍騰飛").fontSize(15).width(65).height(50).backgroundColor(Color.Gray)
Text("三羊開泰").fontSize(15).width(65).height(50).backgroundColor(Color.Yellow)
}.backgroundColor("#66CCFF").width('100%').height('100%').justifyContent(FlexAlign.Start)
}
}
示例图
自适应
自适应拉伸
在线性布局下,常用空白填充组件Blank,在容器主轴方向自动填充空白空间,达到自适应拉伸效果。Row和Column作为容器,只需要添加宽高为百分比,当屏幕宽高发生变化时,会产生自适应效果。
自适应缩放
自适应缩放是指子元素随容器尺寸的变化而按照预设的比例自动调整尺寸,适应各种不同大小的设备。在线性布局中,可以使用以下两种方法实现自适应缩放。 父容器尺寸确定时,使用layoutWeight属性设置子元素和兄弟元素在主轴上的权重,忽略元素本身尺寸设置,使它们在任意尺寸的设备下自适应占满剩余空间。
自适应延伸
自适应延伸是指在不同尺寸设备下,当页面的内容超出屏幕大小而无法完全显示时,可以通过滚动条进行拖动展示。这种方法适用于线性布局中内容无法一屏展示的场景。通常有以下两种实现方式。 在List中添加滚动条:当List子项过多一屏放不下时,可以将每一项子元素放置在不同的组件中,通过滚动条进行拖动展示。可以通过scrollBar属性设置滚动条的常驻状态,edgeEffect属性设置拖动到内容最末端的回弹效果。 使用Scroll组件:在线性布局中,开发者可以进行垂直方向或者水平方向的布局。当一屏无法完全显示时,可以在Column或Row组件的外层包裹一个可滚动的容器组件Scroll来实现可滑动的线性布局。