声明式UI
- 声明式描述
- 状态驱动视图更新
状态驱动视图更新
应用界面是动态的,需要根据状态不同显示不同内容
- 状态:数据源
- 视图:与状态相关联的UI内容
//定义状态变量
@State isComplete: boolean = false
...
//建立状态与视图间的关系
Row(){
if (this.isComplete) {
Image($r('app.media.ic_ok'))
...
} else {
Image($r('app.media.ic_default'))
...
}
Text('学习ArkTS')
...
}
.onClick(() => {
//改变状态变量
this.isComplete = !this.isComplete
})
页面和组件的生命周期
// Index.ets
import router from '@ohos.router';
@Entry
@Component
struct MyComponent {
@State showChild: boolean = true;
@State btnColor:string = "#FF007DFF"
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageShow() {
console.info('Index onPageShow');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageHide() {
console.info('Index onPageHide');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onBackPress() {
console.info('Index onBackPress');
this.btnColor ="#FFEE0606"
return true // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理
}
// 组件生命周期
aboutToAppear() {
console.info('MyComponent aboutToAppear');
}
// 组件生命周期
aboutToDisappear() {
console.info('MyComponent aboutToDisappear');
}
build() {
Column() {
// this.showChild为true,创建Child子组件,执行Child aboutToAppear
if (this.showChild) {
Child()
}
// this.showChild为false,删除Child子组件,执行Child aboutToDisappear
Button('delete Child')
.margin(20)
.backgroundColor(this.btnColor)
.onClick(() => {
this.showChild = false;
})
// push到page页面,执行onPageHide
Button('push to next page')
.onClick(() => {
router.pushUrl({ url: 'pages/page' });
})
}
}
}
@Component
struct Child {
@State title: string = 'Hello World';
// 组件生命周期
aboutToDisappear() {
console.info('[lifeCycle] Child aboutToDisappear')
}
// 组件生命周期
aboutToAppear() {
console.info('[lifeCycle] Child aboutToAppear')
}
build() {
Text(this.title).fontSize(50).margin(20).onClick(() => {
this.title = 'Hello ArkUI';
})
}
}
// page.ets
@Entry
@Component
struct page {
@State textColor: Color = Color.Black;
@State num: number = 0
onPageShow() {
this.num = 5
}
onPageHide() {
console.log("page onPageHide");
}
onBackPress() { // 不设置返回值按照false处理
this.textColor = Color.Grey
this.num = 0
}
aboutToAppear() {
this.textColor = Color.Blue
}
build() {
Column() {
Text(`num 的值为:${this.num}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(this.textColor)
.margin(20)
.onClick(() => {
this.num += 5
})
}
.width('100%')
}
}
以上示例中,Index页面包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有@Entry装饰的节点才可以使页面级别的生命周期方法生效,所以MyComponent中声明了当前Index页面的页面生命周期函数。MyComponent和其子组件Child也同时声明了组件的生命周期函数。
-
应用冷启动的初始化流程为:
MyComponent aboutToAppear --> MyComponent build -->
Child aboutToAppear --> Child build --> Child build执行完毕 -->
MyComponent build执行完毕 -->
Index onPageShow。 -
如果调用的是router.replaceUrl,则当前Index页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisappear,然后执行初始化新页面的生命周期流程。
-
点击返回按钮,触发页面生命周期Index onBackPress,且触发返回一个页面后会导致当前Index页面被销毁。
-
最小化应用或者应用进入后台,触发Index onPageHide。当前Index页面没有被销毁,所以并不会执行组件的aboutToDisappear。应用回到前台,执行Index onPageShow。
-
退出应用,执行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。
Builder装饰器
按引用传递参数
传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。
如果在@Builder方法内调用自定义组件,ArkUI提供$$作为按引用传递参数的范式。
class Tmp {
paramA1: string = ''
}
@Builder function overBuilder($$: Tmp) {
Row() {
Column() {
Text(`overBuilder===${$$.paramA1}`)
HelloComponent({message: $$.paramA1})
}
}
}
@Component
struct HelloComponent {
@Link message: string;
build() {
Row() {
Text(`HelloComponent===${this.message}`)
}
}
}
@Entry
@Component
struct Parent {
@State label: string = 'Hello';
build() {
Column() {
// Pass the this.label reference to the overBuilder component when the overBuilder component is called in the Parent component.
overBuilder({paramA1: this.label})
Button('Click me').onClick(() => {
// After Click me is clicked, the UI text changes from Hello to ArkUI.
this.label = 'ArkUI';
})
}
}
}
按值传递参数
调用@Builder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起@Builder方法内的UI刷新。所以当使用状态变量的时候,推荐使用按引用传递。
@Builder function overBuilder(paramA1: string) {
Row() {
Text(`UseStateVarByValue: ${paramA1} `)
}
}
@Entry
@Component
struct Parent {
@State label: string = 'Hello';
build() {
Column() {
overBuilder(this.label)
}
}
}
ForEach循环渲染列表
ForEach(this.totalTasks, (item: string) => {
ToDoItem({ content: item, haha : "hahahaha" })
}, (item: string) => JSON.stringify(item))
UIAbility
- 是系统调度的单元,提供窗口用于界面绘制
- 包含用户界面的应用组件,用于和用户进行交互
- 监听前后台切换、销毁
我理解鸿蒙中开发页面类似于Android中的单Activity多Fragment架构。UIAbility像Activity,module.json5中配置UIAbility的launchType相当于Android的task Affinity。
启动模式:
影响的是‘最近任务列表’展示样式
- singleton(单实例模式)
- multiton(多实例模式)
- specified(指定实例模式)
应用的UIAbility实例已创建,该UIAbility配置为单实例模式,再次调用startAbility()方法启动该UIAbility实例。由于启动的还是原来的UIAbility实例,并未重新创建一个新的UIAbility实例,此时只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调。
这不就是对应的Android的SingleTask启动模式吗
UIAbility生命周期:
- Create
- Foreground
- Background
- Destroy
WindowStage生命周期:
- WindowStageCreate
- WindowStageDestroy
页面跳转和参数接收
方式一:router.pushUrl
router.pushUrl({
url: 'pages/Second',
params: {
src: 'Index页面传来的数据',
}
}, router.RouterMode.Single)
-
router.RouterMode.Single 单实例模式
如果目标页面的url在页面栈中已经存在同url页面,离栈顶最近同url页面会被移动到栈顶,移动后的页面为新建页
-
router.RouterMode.Standard 多实例模式
如果目标页面的url在页面栈中不存在同url页面,按多实例模式跳转,页面栈的元素数量会加1
方式二:router.replaceUrl
router.replaceUrl({
url: 'pages/Second',
params: {
src: 'Index页面传来的数据',
}
}, router.RouterMode.Single)
- 单实例模式下:如果目标页面的url在页面栈中已经存在同url页面,离栈顶最近同url页面会被移动到栈顶,替换当前页面,并销毁被替换的当前页面,移动后的页面为新建页,页面栈的元素数量会减1
- 如果目标页面的url在页面栈中不存在同url页面,按多实例模式跳转,页面栈的元素数量不变。
注:哈?这不就是类似Android的Fragment replace吗?互相替换
接收参数
import router from '@ohos.router';
@Entry
@Component
struct Second {
@State name: string = router.getParams()?.['name'] ?? "待办"
// 页面刷新展示
// ...
}
页面返回
返回上一个页面
router.back();
返回到指定页面
router.back({ url: 'pages/Index' });
在Second页面中,调用router.back()方法返回上一个页面或者返回指定页面时,根据需要继续增加自定义参数,例如在返回时增加一个自定义参数src。 在Index页面通过调用router.getParams()方法,获取Second页面传递过来的自定义参数
router.back({
url: 'pages/Index',
params: {
src: 'Second页面传来的数据',
}
})
使用资源引用类型
可以将硬编码写到entry/src/main/resources下的资源文件
string.json中定义Button显示的文本
{
"string": [
{
"name": "login_text",
"value": "登录"
}
]
}
在float.json中定义Button的宽高和字体大小
{
"float": [
{
"name": "button_width",
"value": "300vp"
},
{
"name": "button_height",
"value": "40vp"
},
{
"name": "login_fontSize",
"value": "18fp"
}
]
}
在color.json中定义Button的背景颜色
{
"color": [
{
"name": "button_color",
"value": "#1890ff"
}
]
}
然后在Button组件通过“$r('app.type.name')”的形式引用应用资源。app代表应用内resources目录中定义的资源;type代表资源类型(或资源的存放位置),可以取“color”、“float”、“string”、“plural”、“media”;name代表资源命名,由开发者定义资源时确定。
Button('登录', { type: ButtonType.Capsule, stateEffect: true })
.width(300)
.height(40)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor('#007DFF')
修改为
Button($r('app.string.login_text'), { type: ButtonType.Capsule })
.width($r('app.float.button_width'))
.height($r('app.float.button_height'))
.fontSize($r('app.float.login_fontSize'))
.backgroundColor($r('app.color.button_color'))
容器组件:Column & Row
- justifyContent:设置子组件在主轴方向上的对齐格式
- alignItems:设置子组件在交叉轴方向上的对齐格式
容器组件:List & Grid
List:
@Entry
@Component
struct ListDemo {
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
build() {
Column() {
List({ space: 10 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Text(`${item}`)
.width('100%')
.height(100)
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0x007DFF)
}
}, item => item)
}
}
.padding(12)
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
这种方式会导致所有item都加载
可以采用懒加载的方式:
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0
}
public getData(index: number): any {
return undefined
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener')
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener')
this.listeners.splice(pos, 1)
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index)
})
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to)
})
}
}
class MyDataSource extends BasicDataSource {
private dataArray: Array<string> = new Array(100).fill('test')
public totalCount(): number {
return this.dataArray.length
}
public getData(index: number): any {
return this.dataArray[index]
}
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data)
this.notifyDataAdd(index)
}
public pushData(data: string): void {
this.dataArray.push(data)
this.notifyDataAdd(this.dataArray.length - 1)
}
}
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource()
build() {
Scroll() {
List() {
LazyForEach(this.data, (item: string, index: number) => {
ListItem() {
Text('item value: ' + item + (index + 1)).fontSize(20).margin(10)
}.width('100%')
})
}.width('100%').height(500)
}.backgroundColor(Color.Pink)
}
}
Gird:
@Entry
@Component
struct GridExample {
// 定义一个长度为16的数组
private arr: string[] = new Array(16).fill('').map((_, index) => `item ${index}`);
build() {
Column() {
Grid() {
ForEach(this.arr, (item: string) => {
GridItem() {
Text(item)
.fontSize(16)
.fontColor(Color.White)
.backgroundColor(0x007DFF)
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
}
}, item => item)
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.height(300)
}
.width('100%')
.padding(12)
.backgroundColor(0xF1F3F5)
}
}
示例代码中创建了16个GridItem列表项,但是不可滚动。
columnsTemplate的值为'1fr 1fr 1fr 1fr',表示这个网格为4列,将Grid允许的宽分为4等分,每列占1份;
rowsTemplate的值为'1fr 1fr 1fr 1fr',表示这个网格为4行,将Grid允许的高分为4等分,每行占1份。
将示例代码中GridItem的高度设置为固定值,例如100;仅设置columnsTemplate属性,不设置rowsTemplate属性,就可以实现Grid列表的滚动:
容器组件:Tabs
自定义TabBar样式(底部tab效果):
@Entry
@Component
struct TabsExample {
@State currentIndex: number = 0;
private tabsController: TabsController = new TabsController();
@Builder TabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) {
Column() {
Image(this.currentIndex === targetIndex ? selectedImg : normalImg)
.size({ width: 25, height: 25 })
Text(title)
.fontColor(this.currentIndex === targetIndex ? '#1698CE' : '#6B6B6B')
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = targetIndex;
this.tabsController.changeIndex(this.currentIndex);
})
}
build() {
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
TabContent() {
Column().width('100%').height('100%').backgroundColor('#00CB87')
}
.tabBar(this.TabBuilder('首页', 0, $r('app.media.home_selected'), $r('app.media.home_normal')))
TabContent() {
Column().width('100%').height('100%').backgroundColor('#007DFF')
}
.tabBar(this.TabBuilder('我的', 1, $r('app.media.mine_selected'), $r('app.media.mine_normal')))
}
.barWidth('100%')
.barHeight(50)
.onChange((index: number) => {
this.currentIndex = index;
})
}
}
装饰器:@State、@Prop、@Link、@Provide & @Consume
- @State:当前组件中使用
- @Prop:是父传子单向同步,当父改变子能监听到并刷新UI,但是子改变了但是父不会感知。写法上,父组件把
@State变量传给子组件,子组件对接收的这个变量要用@Prop修饰 - @Link:是父子双向同步,各自的修改对方都能监听到。写法上,父组件把
@State变量传给子组件,子组件对接收的这个变量要用@Link修饰 - @Provide & @Consume:可以跨组件的双向同步,不局限于父子关系。写法上,当前组件中的变量用
@Provide修饰,另一个组件对接收的这个变量要用@Consume修饰
以上涉及跨组件的,需要传递的,都需要变量名相同。
监听值改变的回调:@Watch
@Link @Watch('hahahaha') targetData: Array<TaskItemBean>;
hahahaha() {
hilog.error(0, "zktzkt", "hahahaha");
}
一旦targetData变化了,hahahaha()函数就会被回调
显式动画 animateTo
通用属性变化时,可以通过属性动画实现渐变过渡效果。
支持的属性包括:width、height、backgroundColor、opacity、scale、rotate、translate等。
只要该属性是在animateTo的闭包函数中修改的,那么由其引起的所有变化都会按照animateTo的动画参数执行动画过渡到终点值
animateTo({ duration: CommonConstants.DURATION }, () => {
this.isExpanded = !this.isExpanded;
})
属性动画 animation
想要组件随某个属性值的变化而产生动画,此属性需要加在animation属性之前。
有的属性变化不希望通过animation产生属性动画,可以放在animation之后。
产生属性动画的属性本身需满足一定的要求,并非任何属性都可以产生属性动画。目前支持的属性包括width、height、position、opacity、backgroundColor、scale、rotate、translate等
@Entry
@Component
struct LayoutChange2 {
@State myWidth: number = 100;
@State myHeight: number = 50;
@State flag: boolean = false;
@State myColor: Color = Color.Blue;
build() {
Column({ space: 10 }) {
Button("text")
.type(ButtonType.Normal)
.width(this.myWidth)
.height(this.myHeight)
// animation只对其上面的type、width、height属性生效,时长为1000ms,曲线为Ease
.animation({ duration: 1000, curve: Curve.Ease })
// animation对下面的backgroundColor、margin属性不生效
.backgroundColor(this.myColor)
.margin(20)
Button("area: click me")
.fontSize(12)
.onClick(() => {
// 改变属性值,配置了属性动画的属性会进行动画过渡
if (this.flag) {
this.myWidth = 100;
this.myHeight = 50;
this.myColor = Color.Blue;
} else {
this.myWidth = 200;
this.myHeight = 100;
this.myColor = Color.Pink;
}
this.flag = !this.flag;
})
}
}
}
安装三方库
执行命令
ohpm i @ohos/axios
成功后会在oh-package.json5中的dependencies节点下会自动生成"@ohos/axios": "^2.2.0"