ArkTs 知识点
UI泛式
@Builder装饰器
- 全局
//全局定义
@Builder function globalBuild() { ... }
//调用
globalBuild()
- 组件内定义
//组件内定义
@Builder innerBuild() { ... }
//调用
this.innerBuild()
@BuilderParam装饰器:引用@Builder函数
//全局 builder
@Builder function globalBuild() {}
//组件内 builder
@Builder innerBuild() { ... }
//使用全局
@BuilderParam globalBuildParam: () => void = globalBuild
//使用内部
@BuilderParam innerBuildParam: () => void = this.innerBuild;
//调用
this.globalBuildParam()
this.innerBuildParam()
wrapBuilder:封装全局@Builder
let builderVar: WrappedBuilder<[string, number]> = wrapBuilder(MyBuilder)
let builderArr: WrappedBuilder<[string, number]>[] = [wrapBuilder(MyBuilder)] //可以放入数组
wrapBuilder方法只能传入全局@Builder方法。
//使用
@Builder function globalBuild(value: string, size: number) {}
let globalBuilder: WrappedBuilder<[string, number]> = wrapBuilder(globalBuild);
//调用
globalBuilder.builder("", 1)
@Styles装饰器:定义组件重用样式
@Styles方法不支持参数
//全局定义
@Styles function globalStyle() { ... }
//组件内定义
@Styles innerStyle() {}
如果要实现跨文件操作的功能,可以参考使用动态属性设置。
//调用
Text('FancyA')
.globalStyle()
Text('FancyA')
.innerStyle()
@Extend装饰器:定义扩展组件样式
//@Extend仅支持在全局定义
@Extend(Text) function fancy () {
.fontColor(Color.Red)
}
//@Extend装饰的方法支持参数,开发者可以在调用时传递参数
@Extend(Text) function fancy (fontSize: number) {
.fontColor(Color.Red)
.fontSize(fontSize)
}
//调用
Text('Fancy')
.fancy(16)
stateStyles:多态样式
focused:获焦态。
normal:正常态。
pressed:按压态。
disabled:不可用态。
selected10+:选中态。
Button('Button1')
.stateStyles({
focused: {
.backgroundColor(Color.Pink)
},
pressed: {
.backgroundColor(Color.Black)
},
normal: {
.backgroundColor(Color.Red)
}
})
@AnimatableExtend装饰器:定义可动画属性
//以下示例实现字体大小的动画效果。
@AnimatableExtend(Text) function animatableFontSize(size: number) {
.fontSize(size)
}
@State fontSize: number = 20
Text("AnimatableProperty")
.animatableFontSize(this.fontSize)
.animation({duration: 1000, curve: "ease"})
Button("Play")
.onClick(() => {
this.fontSize = this.fontSize == 20 ? 36 : 20
})
@Require装饰器:校验构造传参
//@Require是校验@Prop和@BuilderParam是否需要构造传参的一个装饰器, @Require装饰器不能单独使用。
//@Require装饰器仅用于装饰struct内的@Prop和@BuilderParam成员状态变量。
@Require @Prop message: string;
//构建组件时,必须传参
状态管理
状态管理概述
- @State:@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。
- @Prop:@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。
- @Link:@Link装饰的变量可以和父组件建立双向同步关系,子组件中@Link装饰变量的修改会同步给父组件中建立双向数据绑定的数据源,父组件的更新也会同步给@Link装饰的变量。
- @Provide/@Consume:@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。
- @Observed:@Observed装饰class,需要观察多层嵌套场景的class需要被@Observed装饰。单独使用@Observed没有任何作用,需要和@ObjectLink、@Prop连用。
- @ObjectLink:@ObjectLink装饰的变量接收@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。
- 仅@Observed/@ObjectLink可以观察嵌套场景,其他的状态变量仅能观察第一层,详情见各个装饰器章节的“观察变化和行为表现”小节。
- @Watch用于监听状态变量的变化。
@State装饰器:组件内状态
@Prop装饰器:父子单向同步
当数据源更改时,@Prop装饰的变量都会更新,并且会覆盖本地所有更改。因此,数值的同步是父组件到子组件(所属组件),子组件数值的变化不会同步到父组件。
@Prop装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。例如PixelMap等通过NAPI提供的复杂类型,由于有部分实现在Native侧,因此无法在ArkTS侧通过深拷贝获得完整的数据。
@Prop装饰器不能在@Entry装饰的自定义组件中使用。只能在子类中使用
//父组件@State到子组件@Prop简单数据类型同步
@Prop count: number = 0;
// @Prop装饰的变量不会同步给父组件
Button(`Try again`).onClick(() => {
this.count -= this.costOfOneAttempt;
})
// 父组件的修改会同步给子组件
@State countDownStartValue: number = 10;
CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
@Link装饰器:父子双向同步
@Link greenButtonState: GreenButtonState;
// 更新class的属性,变化可以被观察到同步回父组件
this.greenButtonState.width += 60;
//父类
@State greenButtonState: GreenButtonState = new GreenButtonState(180);
@Provide装饰器和@Consume装饰器:与后代组件双向同步
@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景
@Provide装饰的变量是在祖先组件中
@Consume装饰的变量是在后代组件中
// 通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;
// 通过相同的变量别名绑定
@Provide('a') b: number = 0;
@Consume('a') c: number;
//后代组件
@Consume selectedDate: Date;
//祖先(entry)组件
@Provide selectedDate: Date = new Date('2021-08-08')
allowOverride:@Provide重写选项。
//后代组件
@Consume("reviewVotes") reviewVotes: number;
//祖先(entry)组件
@Provide({ allowOverride: "reviewVotes" }) reviewVotes: number = 10;
@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化
@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步
被@Observed装饰的类,可以被观察到属性的变化
子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例和父组件中对应的状态变量建立双向数据绑定
使用@Observed装饰class会改变class原始的原型链,@Observed和其他类装饰器装饰同一个class可能会带来问题。
@ObjectLink装饰器不能在@Entry装饰的自定义组件中使用。
ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用@Prop。
@Observed
class Bag
@ObjectLink bag: Bag;
管理应用拥有的状态概述
LocalStorage:页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。
AppStorage:特殊的单例LocalStorage对象,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。
PersistentStorage:持久化存储UI状态,通常和AppStorage配合使用,选择AppStorage存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。
Environment:应用程序运行的设备的环境参数,环境参数会同步到AppStorage中,可以和AppStorage搭配使用。
LocalStorage:页面级UI状态存储
LocalStorage支持UIAbility实例内多个页面间状态共享。
应用程序可以创建多个LocalStorage实例,LocalStorage实例可以在页面内共享,也可以通过GetShared接口,实现跨页面、UIAbility实例内共享。
@LocalStorageProp:@LocalStorageProp装饰的变量与LocalStorage中给定属性建立单向同步关系。
@LocalStorageLink:@LocalStorageLink装饰的变量与LocalStorage中给定属性建立双向同步关系。
// 创建新实例并使用给定对象初始化
let para: Record<string, number> = { 'PropA': 47 };
let storage: LocalStorage = new LocalStorage(para);
storage.setOrCreate('PropB', new PropB(50));
// @LocalStorageLink变量装饰器与LocalStorage中的'PropA'属性建立双向绑定
@LocalStorageLink('PropA') childLinkNumber: number = 1;
// @LocalStorageLink变量装饰器与LocalStorage中的'PropB'属性建立双向绑定
@LocalStorageLink('PropB') childLinkObject: PropB = new PropB(0);
AppStorage:应用全局的UI状态存储
@StorageProp
在上文中已经提到,如果要建立AppStorage和自定义组件的联系,需要使用@StorageProp和@StorageLink装饰器。使用@StorageProp(key)/@StorageLink(key)装饰组件内的变量,key标识了AppStorage的属性。
当自定义组件初始化的时候,会使用AppStorage中对应key的属性值将@StorageProp(key)/@StorageLink(key)装饰的变量初始化。由于应用逻辑的差异,无法确认是否在组件初始化之前向AppStorage实例中存入了对应的属性,所以AppStorage不一定存在key对应的属性,因此@StorageProp(key)/@StorageLink(key)装饰的变量进行本地初始化是必要的。
@StorageProp(key)是和AppStorage中key对应的属性建立单向数据同步,允许本地改变,但是对于@StorageProp,本地的修改永远不会同步回AppStorage中,相反,如果AppStorage给定key的属性发生改变,改变会被同步给@StorageProp,并覆盖掉本地的修改。
@StorageLink(key)是和AppStorage中key对应的属性建立双向数据同步:
不建议借助@StorageLink的双向同步机制实现事件通知
PersistentStorage:持久化存储UI状态
前两个小节介绍的LocalStorage和AppStorage都是运行时的内存,但是在应用退出再次启动后,依然能保存选定的结果,是应用开发中十分常见的现象,这就需要用到PersistentStorage。
Environment:设备环境查询
accessibilityEnabled boolean 获取无障碍屏幕读取是否启用。
colorMode ColorMode 色彩模型类型:选项为ColorMode.LIGHT: 浅色,ColorMode.DARK: 深色。
fontScale number 字体大小比例,范围: [0.85, 1.45]。
fontWeightScale number 字体粗细程度,范围: [0.6, 1.6]。
layoutDirection LayoutDirection 布局方向类型:包括LayoutDirection.LTR: 从左到右,LayoutDirection.RTL: 从右到左。
languageCode string 当前系统语言值,取值必须为小写字母, 例如zh。
// 将设备的语言code存入AppStorage,默认值为en
Environment.envProp('languageCode', 'en');
@Watch:用于监听状态变量的变化。
@Watch用于监听状态变量的变化,当状态变量变化时,@Watch的回调方法将被调用。@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为false的情况下,就会触发@Watch的回调。
@Watch和自定义组件更新
@Prop @Watch('onCountUpdated') count: number = 0;
// @Watch 回调
onCountUpdated(propName: string): void {
this.total += this.count;
}
MVVM模式
ArkUI采取MVVM = Model + View + ViewModel模式,其中状态管理模块起到的就是ViewModel的作用,将数据与视图绑定在一起,更新数据的时候直接更新视图
Model层:存储数据和相关逻辑的模型。它表示组件或其他相关业务逻辑之间传输的数据。Model是对原始数据的进一步处理。 View层:在ArkUI中通常是@Component装饰组件渲染的UI。 ViewModel层:在ArkUI中,ViewModel是存储在自定义组件的状态变量、LocalStorage和AppStorage中的数据。 自定义组件通过执行其build()方法或者@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。 View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了@Watch回调方法用于监听状态数据的改变。 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。 ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。
状态管理优秀实践
使用@ObjectLink代替@Prop减少不必要的深拷贝
不使用状态变量强行更新非状态变量关联组件
精准控制状态变量关联的组件数
合理控制对象类型状态变量关联的组件数量
查询状态变量关联的组件数
避免在for、while等循环逻辑中频繁读取状态变量
渲染控制概述
if/else:条件渲染
使用if进行条件渲染
ForEach:循环渲染
ForEach接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。例如,ListItem组件要求ForEach的父容器组件必须为List组件。
ForEach(
arr: Array,
itemGenerator: (item: any, index: number) => void,
keyGenerator?: (item: any, index: number) => string
)
键值生成规则
在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
ForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: any, index: number) => { return index + '__' + JSON.stringify(item); }。
ArkUI框架对于ForEach的键值生成有一套特定的判断规则,这主要与itemGenerator函数的第二个参数index以及keyGenerator函数的第二个参数index有关,具体的键值生成规则判断逻辑如下图所示。
首次渲染
@State simpleList: Array<string> = ['one', 'two', 'three'];
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
}, (item: string) => item)
非首次渲染
在ForEach组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。
@State simpleList: Array<string> = ['one', 'two', 'three'];
this.simpleList[2] = 'new three';
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
.margin({ top: 20 })
}, (item: string) => item)
LazyForEach:数据懒加载
LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
LazyForEach(
dataSource: IDataSource, // 需要进行数据迭代的数据源
itemGenerator: (item: any, index: number) => void, // 子组件生成函数
keyGenerator?: (item: any, index: number) => string // 键值生成函数
): void
interface IDataSource {
totalCount(): number; // 获得数据总数
getData(index: number): Object; // 获取索引值对应的数据
registerDataChangeListener(listener: DataChangeListener): void; // 注册数据改变的监听器
unregisterDataChangeListener(listener: DataChangeListener): void; // 注销数据改变的监听器
}
interface DataChangeListener {
onDataReloaded(): void; // 重新加载数据完成后调用
onDataAdded(index: number): void; // 添加数据完成后调用
onDataMoved(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换完成后调用
onDataDeleted(index: number): void; // 删除数据完成后调用
onDataChanged(index: number): void; // 改变数据完成后调用
onDataAdd(index: number): void; // 添加数据完成后调用
onDataMove(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换完成后调用
onDataDelete(index: number): void; // 删除数据完成后调用
onDataChange(index: number): void; // 改变数据完成后调用
}
LazyForEach必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
数组操作
1. 创建
private data: string[] = []
2. 添加
data.push
3. 添加新元素 并删除旧元素
data.splice(2, 0, "") //在第三个位置插入元素
data.splice(3, 1) //删除第 4 个元素
Stage模型开发概述
Stage模型提供UIAbility和ExtensionAbility两种类型的组件
UIAbility组件是一种包含UI的应用组件,主要用于和用户交互。
ExtensionAbility组件是一种面向特定场景的应用组件。
ExtensionAbility组件有用于卡片场景的FormExtensionAbility,用于输入法场景的InputMethodExtensionAbility,用于闲时任务场景的WorkSchedulerExtensionAbility等多种派生类,这些派生类都是基于特定场景提供的。
WindowStage
每个UIAbility实例都会与一个WindowStage类实例绑定,该类起到了应用进程内窗口管理器的作用。它包含一个主窗口。也就是说UIAbility实例通过WindowStage持有了一个主窗口,该主窗口为ArkUI提供了绘制区域。
Context
在Stage模型上,Context及其派生类向开发者提供在运行期可以调用的各种资源和能力。UIAbility组件和各种ExtensionAbility组件的派生类都有各自不同的Context类,他们都继承自基类Context,但是各自又根据所属组件,提供不同的能力。
AbilityStage
每个Entry类型或者Feature类型的HAP在运行期都有一个AbilityStage类实例,当HAP中的代码首次被加载到进程中的时候,系统会先创建AbilityStage实例。
UIAbility的生命周期包括Create、Foreground、Background、Destroy四个状态,如下图所示。
WindowStageCreate和WindowStageDestroy状态
UIAbility实例创建完成之后,在进入Foreground之前,系统会创建一个WindowStage。WindowStage创建完成后会进入onWindowStageCreate()回调,可以在该回调中设置UI加载、
在onWindowStageCreate()回调中通过loadContent()方法设置应用要加载的页面,并根据需要调用on('windowStageEvent')方法订阅WindowStage的事件(获焦/失焦、可见/不可见)。
当应用的UIAbility实例已创建,且UIAbility配置为singleton启动模式时,再次调用startAbility()方法启动该UIAbility实例时,只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调。应用可以在该回调中更新要加载的资源和数据等,用于后续的UI展示。
UIAbility组件启动模式
singleton启动模式
multiton启动模式
specified启动模式 specified启动模式为指定实例模式,针对一些特殊场景使用(例如文档应用中每次新建文档希望都能新建一个文档实例,重复打开一个已保存的文档希望打开的都是同一个文档实例)
UIAbility组件基本用法
UIAbility组件的基本用法包括:指定UIAbility的启动页面以及获取UIAbility的上下文UIAbilityContext。
windowStage.loadContent('pages/Index', (err, data) => {
// ...
});
private context = getContext(this) as common.UIAbilityContext;
UIAbility组件与UI的数据同步
使用EventHub进行数据通信
使用AppStorage/LocalStorage进行数据同步
UIAbility组件间交互(设备内)
启动应用内的UIAbility
// context为Ability对象的成员,在非Ability对象内部调用需要
// 将Context对象传递过去
let wantInfo: Want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.samples.myapplication',
moduleName: 'entry', // moduleName非必选
abilityName: 'FuncAbilityA',
parameters: { // 自定义信息
info: '来自EntryAbility Page_UIAbilityComponentsInteractive页面'
},
}
// context为调用方UIAbility的UIAbilityContext
this.context.startAbility(wantInfo).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'startAbility success.');
}).catch((error: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, 'startAbility failed.');
});
// 接收调用方UIAbility传过来的参数
let funcAbilityWant = want;
let info = funcAbilityWant?.parameters?.info;
// ...
启动应用内的UIAbility并获取返回结果
let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
let want: Want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.samples.stagemodelabilitydevelop',
moduleName: 'entry', // moduleName非必选
abilityName: 'FuncAbilityA',
parameters: { // 自定义信息
info: '来自EntryAbility UIAbilityComponentsInteractive页面'
}
};
context.startAbilityForResult(want).then((data) => {
// ...
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to start ability for result. Code is ${err.code}, message is ${err.message}`);
});
let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
const RESULT_CODE: number = 1001;
let abilityResult: common.AbilityResult = {
resultCode: RESULT_CODE,
want: {
bundleName: 'com.samples.stagemodelabilitydevelop',
moduleName: 'entry', // moduleName非必选
abilityName: 'FuncAbilityB',
parameters: {
info: '来自FuncAbility Index页面'
},
},
};
context.terminateSelfWithResult(abilityResult, (err) => {
if (err.code) {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to terminate self with result. Code is ${err.code}, message is ${err.message}`);
return;
}
});
启动其他应用的UIAbility
使用隐式Want启动。系统会根据调用方的want参数来识别和启动匹配到的应用UIAbility。
通过 action 匹配
"actions": [
...
"ohos.want.action.viewData"
]
let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
let want: Want = {
deviceId: '', // deviceId为空表示本设备
// uncomment line below if wish to implicitly query only in the specific bundle.
// bundleName: 'com.samples.stagemodelabilityinteraction',
action: 'ohos.want.action.viewData',
// entities can be omitted.
entities: ['entity.system.default']
};
// context为调用方UIAbility的UIAbilityContext
context.startAbility(want).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in starting FuncAbility.');
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to start FuncAbility. Code is ${err.code}, message is ${err.message}`);
});
//关闭 ability
terminateSelf()
启动其他应用的UIAbility并获取返回结果
let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
let want: Want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.samples.stagemodelabilitydevelop',
moduleName: 'entry', // moduleName非必选
abilityName: 'FuncAbilityA',
parameters: { // 自定义信息
info: '来自EntryAbility UIAbilityComponentsInteractive页面'
}
};
context.startAbilityForResult(want).then((data) => {
// ...
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to start ability for result. Code is ${err.code}, message is ${err.message}`);
});
//需要调用terminateSelfWithResult()方法实现停止自身,并将abilityResult参数信息返回给调用方
启动UIAbility的指定页面
UIAbility冷启动:指的是UIAbility实例处于完全关闭状态下被启动,这需要完整地加载和初始化UIAbility实例的代码、资源等。
onWindowStageCreate(windowStage: window.WindowStage) {
// Main window is created, set main page for this ability
let url = 'pages/Index';
if (this.funcAbilityWant?.parameters?.router && this.funcAbilityWant.parameters.router === 'funcA') {
url = 'pages/Page_ColdStartUp';
}
windowStage.loadContent(url, (err, data) => {
// ...
});
UIAbility热启动:指的是UIAbility实例已经启动并在前台运行过,由于某些原因切换到后台,再次启动该UIAbility实例,这种情况下可以快速恢复UIAbility实例的状态。
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want?.parameters?.router && want.parameters.router === 'funcB') {
let funcAUrl = 'pages/Page_HotStartUp';
if (this.uiContext) {
let router: Router = this.uiContext.getRouter();
router.pushUrl({
url: funcAUrl
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to push url. Code is ${err.code}, message is ${err.message}`);
});
}
}
}
AbilityStage组件容器
AbilityStage是一个Module级别的组件容器,应用的HAP在首次加载时会创建一个AbilityStage实例,可以对该Module进行初始化等操作。
一个Module拥有一个AbilityStage。
export default class MyAbilityStage extends AbilityStage {
onCreate(): void {
// 应用的HAP在首次加载的时,为该Module初始化操作
}
onAcceptWant(want: Want): string {
// 仅specified模式下触发
return 'MyAbilityStage';
}
}
"srcEntry": "./ets/myabilitystage/MyAbilityStage.ets",
应用上下文Context
UIAbilityContext AbilityStageContext ApplicationContext this.context.getApplicationContext();
订阅进程内UIAbility生命周期变化
// 定义生命周期回调对象
let abilityLifecycleCallback: AbilityLifecycleCallback = {
// 当UIAbility创建时被调用
onAbilityCreate(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityCreate uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
},
// 当窗口创建时被调用
onWindowStageCreate(uiAbility, windowStage: window.WindowStage) {
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageCreate uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageCreate windowStage: ${JSON.stringify(windowStage)}`);
},
// 当窗口处于活动状态时被调用
onWindowStageActive(uiAbility, windowStage: window.WindowStage) {
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageActive uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageActive windowStage: ${JSON.stringify(windowStage)}`);
},
// 当窗口处于非活动状态时被调用
onWindowStageInactive(uiAbility, windowStage: window.WindowStage) {
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageInactive uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageInactive windowStage: ${JSON.stringify(windowStage)}`);
},
// 当窗口被销毁时被调用
onWindowStageDestroy(uiAbility, windowStage: window.WindowStage) {
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageDestroy uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageDestroy windowStage: ${JSON.stringify(windowStage)}`);
},
// 当UIAbility被销毁时被调用
onAbilityDestroy(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityDestroy uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
},
// 当UIAbility从后台转到前台时触发回调
onAbilityForeground(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityForeground uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
},
// 当UIAbility从前台转到后台时触发回调
onAbilityBackground(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityBackground uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
},
// 当UIAbility迁移时被调用
onAbilityContinue(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityContinue uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
}
};
// 获取应用上下文
let applicationContext = this.context.getApplicationContext();
try {
// 注册应用内生命周期回调
this.lifecycleId = applicationContext.on('abilityLifecycle', abilityLifecycleCallback);
} catch (err) {
let code = (err as BusinessError).code;
let message = (err as BusinessError).message;
hilog.error(DOMAIN_NUMBER, TAG, `Failed to register applicationContext. Code is ${code}, message is ${message}`);
};
hilog.info(DOMAIN_NUMBER, TAG, `register callback number: ${this.lifecycleId}`);
}
信息传递载体Want
Want是一种对象,用于在应用组件之间传递信息。
显式Want:在启动目标应用组件时,调用方传入的want参数中指定了abilityName和bundleName,称为显式Want。
let wantInfo: Want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.example.myapplication',
abilityName: 'FuncAbility',
}
隐式Want:在启动目标应用组件时,调用方传入的want参数中未指定abilityName,称为隐式Want。
let wantInfo: Want = {
// uncomment line below if wish to implicitly query only in the specific bundle.
// bundleName: 'com.example.myapplication',
action: 'ohos.want.action.search',
// entities can be omitted
entities: [ 'entity.system.browsable' ],
uri: 'https://www.test.com:8080/query/student',
type: 'text/plain',
};
线程模型概述
主线程
TaskPool Worker线程,用于执行耗时操作,支持设置调度优先级、负载均衡等功能,推荐使用。
Worker线程
TaskPool自行管理线程数量,其生命周期由TaskPool统一管理。Worker线程最多创建8个,其生命周期由开发者自行维护。
同一线程中存在多个组件,例如UIAbility组件和UI组件都存在于主线程中。在Stage模型中目前主要使用EventHub进行数据通信。
执行hdc shell命令,进入设备的shell命令行。在shell命令行中,执行ps -p <pid> -T命令,可以查看指定应用进程的线程信息。其中,<pid>为需要指定的应用进程的进程ID。
程序访问控制
申请应用权限
normal
user_grant
应用需要在module.json5配置文件的requestPermissions标签中声明权限。
"requestPermissions":[
{
"name" : "ohos.permission.PERMISSION1",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when":"inuse"
}
},
向用户申请授权
如需检查用户是否已向您的应用授予特定权限,可以使用checkAccessToken()函数,此方法会返回PERMISSION_GRANTED或PERMISSION_DENIED
需要使用requestPermissionsFromUser()接口请求相应的权限。
user_grant权限授权要基于用户可知可控的原则,需要应用在运行时主动调用系统动态申请权限的接口,系统弹框由用户授权,用户结合应用运行场景的上下文,识别出应用申请相应敏感权限的合理性,从而做出正确的选择。
//配置 MICROPHONE权限
申请ohos.permission.MICROPHONE权限
//检查是否有权限
async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
// 获取应用程序的accessTokenID
let tokenId: number = 0;
try {
let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
tokenId = appInfo.accessTokenId;
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
}
// 校验应用是否被授予权限
try {
grantStatus = await atManager.checkAccessToken(tokenId, permission);
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
}
return grantStatus;
}
动态向用户申请授权。
在UIAbility中向用户申请授权。
function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
// requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
atManager.requestPermissionsFromUser(context, permissions).then((data) => {
let grantStatus: Array<number> = data.authResults;
let length: number = grantStatus.length;
for (let i = 0; i < length; i++) {
if (grantStatus[i] === 0) {
// 用户授权,可以继续访问目标操作
} else {
// 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
return;
}
}
// 授权成功
}).catch((err: BusinessError) => {
console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
})
}
在UI中向用户申请授权。
function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
// requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
atManager.requestPermissionsFromUser(context, permissions).then((data) => {
let grantStatus: Array<number> = data.authResults;
let length: number = grantStatus.length;
for (let i = 0; i < length; i++) {
if (grantStatus[i] === 0) {
// 用户授权,可以继续访问目标操作
} else {
// 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
return;
}
}
// 授权成功
}).catch((err: BusinessError) => {
console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
})
}
//跳转到系统权限设置界面
// 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext
function openPermissionsInSystemSettings(context: common.UIAbilityContext): void {
let wantInfo: Want = {
bundleName: 'com.huawei.hmos.settings',
abilityName: 'com.huawei.hmos.settings.MainAbility',
uri: 'application_info_entry',
parameters: {
pushParams: 'com.example.myapplication' // 打开指定应用的详情页面
}
}
context.startAbility(wantInfo).then(() => {
// ...
}).catch((err: BusinessError) => {
// ...
})
}
使用系统Picker
使用音频Picker(AudioViewPicker)可访问、保存用户公共目录的音频文件
在应用需要申请权限ohos.permission.READ_AUDIO以访问用户公共目录的音频文件时,可以使用FilePicker中的AudioViewPicker替代,使用方式请参考:选择音频类文件。
在应用需要申请权限ohos.permission.WRITE_AUDIO以修改用户公共目录的音频文件时,可以使用FilePicker中的AudioViewPicker替代,使用方式请参考:保存音频类文件。
使用文件Picker(DocumentViewPicker)可访问、保存公共目录中非媒体类型的文件。
在应用需要申请权限ohos.permission.READ_DOCUMENT以访问用户公共目录中非媒体类型的文件时,可以使用FilePicker中的DocumentViewPicker替代,使用方式请参考:选择文档类文件。
在应用需要申请权限ohos.permission.WRITE_DOCUMENT以修改用户公共目录中非媒体类型的文件时,可以使用FilePicker中的DocumentViewPicker替代,使用方式请参考:保存文档类文件。
使用照片Picker(PhotoViewPicker)可访问、保存公共目录的图片或视频文件。
在应用需要申请权限ohos.permission.READ_IMAGEVIDEO以访问用户公共目录的图片或视频文件时,可以使用PhotoViewPicker替代,使用方式请参考:选择媒体库资源。
在应用需要申请权限ohos.permission.WRITE_IMAGEVIDEO以修改用户公共目录的图片或视频文件时,可以使用FilePicker中的PhotoViewPicker替代,使用方式请参考:保存图片或视频类文件。
使用联系人Picker(Contacts Picker)可读取联系人数据。
在应用需要申请权限ohos.permission.READ_CONTACTS以读取联系人数据时,可以使用Contacts Picker替代
使用相机Picker (Camera Picker)可实现拍照、录制。
在应用需要申请权限ohos.permission.CAMERA以使用相机时,可以使用Camera Picker替代,使用方式请参考:cameraPicker.pick。
使用安全控件
粘贴控件(PasteButton)
导入剪贴板依赖。
import pasteboard from '@ohos.pasteboard';
添加输入框和粘贴控件。
PasteButton().
保存控件(SaveButton)
导入文件和媒体库依赖。
mport photoAccessHelper from '@ohos.file.photoAccessHelper';
import fs from '@ohos.file.fs';
SaveButton()
async function savePhotoToGallery(context: common.UIAbilityContext) {
let helper = photoAccessHelper.getPhotoAccessHelper(context);
try {
// onClick触发后5秒内通过createAsset接口创建图片文件,5秒后createAsset权限收回。
let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
// 使用uri打开文件,可以持续写入内容,写入过程不受时间限制
let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
context.resourceManager.getMediaContent($r('app.media.icon').id, 0)
.then(async value => {
let media = value.buffer;
// 写到媒体库文件中
await fs.write(file.fd, media);
await fs.close(file.fd);
promptAction.showToast({ message: '已保存至相册!' });
});
}
catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`Failed to save photo. Code is ${err.code}, message is ${err.message}`);
}
}
位置控件(LocationButton)
引入位置服务依赖。
import geoLocationManager from '@ohos.geoLocationManager';
LocationButton
function getCurrentLocationInfo() {
const requestInfo: geoLocationManager.LocationRequest = {
'priority': geoLocationManager.LocationRequestPriority.FIRST_FIX,
'scenario': geoLocationManager.LocationRequestScenario.UNSET,
'timeInterval': 1,
'distanceInterval': 0,
'maxAccuracy': 0
};
geoLocationManager.getCurrentLocation(requestInfo)
.then((location: geoLocationManager.Location) => {
promptAction.showToast({ message: JSON.stringify(location) });
})
.catch((err: BusinessError) => {
console.error(`Failed to get current location. Code is ${err.code}, message is ${err.message}`);
});
}
ArkData(方舟数据管理)
跨应用数据共享
跨应用数据共享
多对多跨应用数据共享
ArkTS
并发
使用异步并发能力进行开发
异步并发概述 (Promise和async/await)
const promise: Promise<number> = new Promise((resolve: Function, reject: Function) => {
setTimeout(() => {
const randomNumber: number = Math.random();
if (randomNumber > 0.5) {
resolve(randomNumber);
} else {
reject(new Error('Random number is too small'));
}
}, 1000);
})
promise.then((result: number) => {
console.info(`Random number is ${result}`);
}).catch((error: BusinessError) => {
console.error(error.message);
});
async/await
async函数是一个返回Promise对象的函数,用于表示一个异步操作
在async函数内部,可以使用await关键字等待一个Promise对象的解析,并返回其解析值
如果一个async函数抛出异常,那么该函数返回的Promise对象将被拒绝,并且异常信息会被传递给Promise对象的onRejected()方法。
async function myAsyncFunction(): Promise<void> {
const result: string = await new Promise((resolve: Function) => {
setTimeout(() => {
resolve('Hello, world!');
}, 3000);
});
console.info(result); // 输出: Hello, world!
}
myAsyncFunction();
TaskPool
实现任务的函数需要使用装饰器@Concurrent标注,且仅支持在.ets文件中使用。
实现任务的函数需要使用类方法时,该类必须使用装饰器@Sendable标注,且仅支持在.ets文件中使用。
任务函数在TaskPool工作线程的执行耗时不能超过3分钟
容器
线性容器
ArrayList即动态数组,可用来构造全局的数组对象。 当需要频繁读取集合中的元素时,推荐使用ArrayList。
add(element: T)函数每次在数组尾部增加一个元素。
insert(element: T, index: number)在指定位置插入一个元素。
arr[index]获取指定index对应的value值,通过指令获取保证访问速度。
forEach(callbackFn: (value: T, index?: number, arrlist?: ArrayList<T>) => void, thisArg?: Object): void访问整个ArrayList容器的元素。
[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。
arr[index] = xxx修改指定index位置对应的value值。
remove(element: T)删除第一个匹配到的元素。
removeByRange(fromIndex: number, toIndex:number)删除指定范围内的元素。
Deque
Deque可用来构造双端队列对象,存储元素遵循先进先出以及先进后出的规则,双端队列可以分别从队头或者队尾进行访问。
insertFront(element: T)函数每次在队头增加一个元素。
insertEnd(element: T)函数每次在队尾增加一个元素。
getFirst()获取队首元素的value值,但是不进行出队操作
getLast()获取队尾元素的value值,但是不进行出队操作。
popFirst()获取队首元素的value值,并进行出队操作。
popLast()获取队尾元素的value值,并进行出队操作。
forEach(callbackFn:(value: T, index?: number, deque?: Deque<T>) => void, thisArg?: Object)访问整个Deque的元素。
[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。
forEach(callbackFn:(value: T, index?: number, deque?: Deque<T>)=> void, thisArg?: Object)对队列进行修改操作。
popFirst()对队首元素进行出队操作并删除。
popLast()对队尾元素进行出队操作并删除。
Queue
Queue可用来构造队列对象,存储元素遵循先进先出的规则。
非线性容器
HashMap
HashMap可用来存储具有关联关系的key-value键值对集合,存储元素中key是唯一的,每个key会对应一个value值。
set(key: K, value: V)函数每次在HashMap增加一个键值对。
get(key: K)获取key对应的value值。
keys()返回一个迭代器对象,包含map中的所有key值。
values()返回一个迭代器对象,包含map中的所有value值。
entries()返回一个迭代器对象,包含map中的所有键值对。
forEach(callbackFn: (value?: V, key?: K, map?: HashMap<K, V>) => void, thisArg?: Object)访问整个map的元素。
[Symbol.iterator]():IterableIterator<[K,V]>迭代器进行数据访问。
replace(key: K, newValue: V)对指定key对应的value值进行修改操作。
orEach(callbackFn: (value?: V, key?: K, map?: HashMap<K, V>) => void, thisArg?: Object)对map中元素进行修改操作。
remove(key: K)对map中匹配到的键值对进行删除操作。
clear()清空整个map集合。
ArkUi
线性布局 (Row/Column)
//对齐方式
可以通过alignItems属性设置子元素在交叉轴(排列方向的垂直方向)上的对齐方式
交叉轴为垂直方向时,取值为VerticalAlign类型,水平方向取值为HorizontalAlign。
alignSelf属性用于控制单个子元素在容器交叉轴上的对齐方式,
可以通过justifyContent属性设置子元素在容器主轴上的排列方式
FlexAlign
层叠布局 (Stack)
//对齐方式
通过alignContent参数实现位置的相对移动。如图2所示,支持九种对齐方式。
alignContent: Alignment.TopStart
弹性布局 (Flex)
弹性布局(Flex)提供更加有效的方式对容器中的子元素进行排列、对齐和分配剩余空间。常用于页面头部导航栏的均匀分布、页面框架的搭建、多行数据的排列等。
//布局方向
通过设置参数direction,Flex({ direction: FlexDirection.Row }) {
布局换行
FlexWrap. NoWrap(默认值):不换行。
Flex({ wrap: FlexWrap.NoWrap }) {
FlexWrap. Wrap:换行,每一行子元素按照主轴方向排列。
Flex({ wrap: FlexWrap.Wrap }) {
FlexWrap. WrapReverse:换行,每一行子元素按照主轴反方向排列。
//主轴对齐方式
通过justifyContent参数设置子元素在主轴方向的对齐方式。
Flex({ justifyContent: FlexAlign.Start }) {
//交叉轴对齐方式
可以通过Flex组件的alignItems参数设置子元素在交叉轴的对齐方式。
Flex({ alignItems: ItemAlign.Auto }) {
//子元素设置交叉轴对齐
子元素的alignSelf属性也可以设置子元素在父容器交叉轴的对齐格式
//内容对齐
可以通过alignContent参数设置子元素各行在交叉轴剩余空间内的对齐方式,只在多行的Flex布局中生效,可选值有:
FlexAlign.Start
相对布局 (RelativeContainer)
RelativeContainer父组件为锚点,__container__代表父容器的ID。
let AlignRus:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }
}
let AlignRue:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'right': { 'anchor': '__container__', 'align': HorizontalAlign.End }
}
let Mleft:Record<string,number> = { 'left': 20 }
let BWC:Record<string,number|string> = { 'width': 2, 'color': '#6699FF' }
RelativeContainer() {
Row().width(100).height(100)
.backgroundColor("#FF3333")
.alignRules(AlignRus)
.id("row1")
Row().width(100).height(100)
.backgroundColor("#FFCC00")
.alignRules(AlignRue)
.id("row2")
}.width(300).height(300)
.margin(Mleft)
.border(BWC)
以兄弟元素为锚点。
let AlignRus:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
'top': { 'anchor': '__container__', 'align': VerticalAlign.Top },
'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start }
}
let RelConB:Record<string,Record<string,string|VerticalAlign|HorizontalAlign>> = {
'top': { 'anchor': 'row1', 'align': VerticalAlign.Bottom },
'left' : { 'anchor': 'row1', 'align': HorizontalAlign.Start }
}
let Mleft:Record<string,number> = { 'left': 20 }
let BWC:Record<string,number|string> = { 'width': 2, 'color': '#6699FF' }
RelativeContainer() {
Row().width(100).height(100)
.backgroundColor("#FF3333")
.alignRules(AlignRus)
.id("row1")
Row().width(100).height(100)
.backgroundColor("#FFCC00")
.alignRules(RelConB)
.id("row2")
}.width(300).height(300)
.margin(Mleft)
.border(BWC)
设置相对于锚点的对齐位置
在水平方向上,对齐位置可以设置为HorizontalAlign.Start、HorizontalAlign.Center、HorizontalAlign.End。
在竖直方向上,对齐位置可以设置为VerticalAlign.Top、VerticalAlign.Center、VerticalAlign.Bottom。
栅格布局 (GridRow/GridCol)
GridRow({
breakpoints: {
value: ['200vp', '300vp', '400vp', '500vp', '600vp'],
reference: BreakpointsReference.WindowSize
}
}) {
ForEach(this.bgColors, (color:Color, index?:number|undefined) => {
GridCol({
span: {
xs: 2, // 在最小宽度类型设备上,栅格子组件占据的栅格容器2列。
sm: 3, // 在小宽度类型设备上,栅格子组件占据的栅格容器3列。
md: 4, // 在中等宽度类型设备上,栅格子组件占据的栅格容器4列。
lg: 6, // 在大宽度类型设备上,栅格子组件占据的栅格容器6列。
xl: 8, // 在特大宽度类型设备上,栅格子组件占据的栅格容器8列。
xxl: 12 // 在超大宽度类型设备上,栅格子组件占据的栅格容器12列。
}
}) {
Row() {
Text(`${index}`)
}.width("100%").height('50vp')
}.backgroundColor(color)
})
}
排列方向
栅格布局中,可以通过设置GridRow的direction属性来指定栅格子组件在栅格容器中的排列方向。该属性可以设置为GridRowDirection.Row(从左往右排列)或GridRowDirection.RowReverse(从右往左排列),以满足不同的布局需求。通过合理的direction属性设置,可以使得页面布局更加灵活和符合设计要求。
GridRow({ direction: GridRowDirection.Row }){}
媒体查询 (@ohos.mediaquery)
针对设备和应用的属性信息(比如显示区域、深浅色、分辨率),设计出相匹配的布局。
当屏幕发生动态改变时(比如分屏、横竖屏切换),同步更新应用的页面布局。
首先导入媒体查询模块。
import mediaquery from '@ohos.mediaquery';
通过matchMediaSync接口设置媒体查询条件,保存返回的条件监听句柄listener。例如监听横屏事件:
et listener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(orientation: landscape)');
//监听回调
onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
if (mediaQueryResult.matches as boolean) {
// do something here
} else {
// do something here
}
}
listener.on('change', onPortrait);
创建列表 (List)
在垂直列表中,List按垂直方向自动排列ListItemGroup或ListItem。
ListItemGroup用于列表数据的分组展示,其子组件也是ListItem。ListItem表示单个列表项,可以包含单个子组件。
List的子组件必须是ListItemGroup或ListItem,ListItem和ListItemGroup必须配合List来使用。
List除了提供垂直和水平布局能力、超出屏幕时可以滚动的自适应延伸能力之外,还提供了自适应交叉轴方向上排列个数的布局能力。
List() {
// ...
}
.listDirection(Axis.Horizontal)
List组件的交叉轴布局可以通过lanes和alignListItem属性进行设置,lanes属性用于确定交叉轴排列的列表项数量,alignListItem用于设置子组件在交叉轴方向的对齐方式。
List() {
ForEach(this.contacts, (item: Contact) => {
ListItem() {
Row() {
Image(item.icon)
.width(40)
.height(40)
.margin(10)
Text(item.name).fontSize(20)
}
.width('100%')
.justifyContent(FlexAlign.Start)
}
}, (item: Contact) => JSON.stringify(item))
}
.width('100%')
}
List提供了divider属性用于给列表项之间添加分隔线。在设置divider属性时,可以通过strokeWidth和color属性设置分隔线的粗细和颜色。
在使用List组件时,可通过scrollBar属性控制列表滚动条的显示。scrollBar的取值类型为BarState,当取值为BarState.Auto表示按需显示滚动条。
支持分组列表
在List组件中使用ListItemGroup对项目进行分组,可以构建二维列表。
通过给List组件设置sticky属性为StickyStyle.Header,即可实现列表的粘性标题效果。
List组件初始化时,可以通过scroller参数绑定一个Scroller对象,进行列表的滚动控制。
private listScroller: Scroller = new Scroller();
.onClick(() => {
// 点击按钮时,指定跳转位置,返回列表顶部
this.listScroller.scrollToIndex(0)
})
ListItem的swipeAction属性可用于实现列表项的左右滑动功能。
长列表的处理
循环渲染适用于短列表,当构建具有大量列表项的长列表时,如果直接采用循环渲染方式,会一次性加载所有的列表元素,会导致页面启动时间过长,影响用户体验。因此,推荐使用数据懒加载(LazyForEach)方式实现按需迭代加载数据,从而提升列表性能。
创建网格 (Grid/GridItem)
Grid组件为网格容器,其中容器内各条目对应一个GridItem组件,如下图所示。
//设置排列方式
设置行列数量与占比
通过设置行列数量与尺寸占比可以确定网格布局的整体排列方式。Grid组件提供了rowsTemplate和columnsTemplate属性用于设置网格布局行列数量与尺寸占比。
Grid() {
...
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 2fr 1fr')
设置主轴方向
Grid() {
...
}
.maxCount(3)
.layoutDirection(GridDirection.Row)
创建轮播 (Swiper)
Swiper组件提供滑动轮播显示的能力。
通过loop属性控制是否循环播放,该属性默认值为true。
Swiper通过设置autoPlay属性,控制是否自动轮播子组件。该属性默认值为false。
Swiper提供了默认的导航点样式,导航点默认显示在Swiper下方居中位置,开发者也可以通过indicator属性自定义导航点的位置和样式。
.indicator(
Indicator.dot()
.left(0)
.itemWidth(15)
.itemHeight(15)
.selectedItemWidth(30)
.selectedItemHeight(15)
.color(Color.Red)
.selectedColor(Color.Blue)
Swiper支持手指滑动、点击导航点和通过控制器三种方式切换页面,
private swiperController: SwiperController = new SwiperController();
Swiper(this.swiperController) {
this.swiperController.showNext(); // 通过controller切换到后一页
this.swiperController.showPrevious(); // 通过controller切换到前一页
Swiper支持水平和垂直方向上进行轮播,主要通过vertical属性控制。
Swiper支持在一个页面内同时显示多个子组件,通过displayCount属性设置。
开发应用沉浸式效果
窗口全屏布局方案
应用扩展布局,全屏显示
可以通过调用窗口强制全屏布局接口(setWindowLayoutFullScreen())实现界面元素覆盖到状态栏和导航条,获取到状态栏和导航条高度后进行避让处理。
let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口
windowClass.setWindowLayoutFullScreen(isLayoutFullScreen)
使用getWindowAvoidArea()接口获取布局遮挡区域(例如状态栏、导航条)。
// 2. 获取布局避让遮挡的区域
let type = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR; // 以导航条避让为例
let avoidArea = windowClass.getWindowAvoidArea(type);
let bottomRectHeight = avoidArea.bottomRect.height; // 获取到导航条区域的高度
在布局中对具体控件布局避让遮挡的区域。例如对底部Tab组件增加对应高度或设置margin边距,底部若是List组件可增加一个空节点。
.margin({ bottom: this.bottomRectHeight }) // 此处margin具体数值在实际中应与导航条区域高度保持一致
应用扩展布局,隐藏避让区
调用setWindowLayoutFullScreen()接口设置窗口全屏。
调用setSpecificSystemBarEnabled()接口设置状态栏和导航条的具体显示/隐藏状态,此场景下将其设置为隐藏。
// 2. 设置状态栏和导航条隐藏
windowClass.setSpecificSystemBarEnabled('status', false)
.then
// 设置全窗颜色和应用元素颜色一致
windowStage.getMainWindowSync().setWindowBackgroundColor('#008000');
状态栏和导航条颜色不同时,可以使用expandSafeArea属性扩展安全区域属性进行调整。
// 设置顶部绘制延伸到状态栏
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
// 设置底部绘制延伸到导航条
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
设置背景图、视频控件大小为安全区域大小并配置expandSafeArea属性。
Image($r('app.media.bg'))
.height('100%').width('100%')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) // 图片组件的绘制区域扩展至状态栏和导航条。
windowStage.getMainWindowSync().setWindowBackgroundColor('#DCDCDC'); // 配置窗口整体底色
.edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring
// List组件的视窗范围扩展至导航条。
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
当状态栏元素和底部导航条元素不同时,无法单纯通过窗口背景色或者背景图组件延伸实现,此时需要对顶部元素和底部元素分别配置expandSafeArea属性,顶部元素配置expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.TOP]),底部元素配置expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.BOTTOM])。
添加组件
按钮 (Button)
单选框 (Radio)
切换按钮 (Toggle)
进度条 (Progress)
文本显示 (Text/Span)
文本输入 (TextInput/TextArea)
显示图片 (Image)
自定义弹窗 (CustomDialog)
视频播放 (Video)
自定义绘制 (XComponent)
Native XComponent
气泡提示 (Popup)
菜单(Menu)
设置页面路由和组件导航
Navigation
Navigation组件一般作为页面的根容器,包括单页面、分栏和自适应三种显示模式。Navigation组件适用于模块内页面切换
Navigation组件的页面包含主页和内容页。主页由标题栏、内容区和工具栏组成,可在内容区中使用NavRouter子组件实现导航栏功能。内容页主要显示NavDestination子组件中的内容。
NavRouter是配合Navigation使用的特殊子组件,默认提供点击响应处理
Navigation() {
...
}
.mode(NavigationMode.Auto)
Navigation() {
...
}
.mode(NavigationMode.Stack)
将mode属性设置为NavigationMode.Split,Navigation组件即可设置为分栏显示模式。
NavDestination作为子页面的根容器,用于显示Navigation的内容区,其mode属性可以设置子页面的类型。
标准类型
NavDestination组件默认为标准类型,此时mode属性为NavDestinationMode.STANDARD。标准类型NavDestination的生命周期跟随NavPathStack栈中标准Destination变化而改变。
Tabs
Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。
Tabs({ barPosition: BarPosition.End }) {
// TabContent的内容:首页、发现、推荐、我的
...
}
Tabs({ barPosition: BarPosition.Start }) {
// TabContent的内容:关注、视频、游戏、数码、科技、体育、影视
...
}
Tabs({ barPosition: BarPosition.Start }) {
// TabContent的内容:首页、发现、推荐、我的
...
}
.vertical(true)
.barWidth(100)
.barHeight(200)
控制滑动切换的属性为scrollable,默认值为true,表示可以滑动,若要限制滑动切换页签则需要设置为false。
Tabs的barMode属性用于控制导航栏是否可以滚动,默认值为BarMode.Fixed。
Tabs({ barPosition: BarPosition.Start }) {
// TabContent的内容:关注、视频、游戏、数码、科技、体育、影视、人文、艺术、自然、军事
...
}
.barMode(BarMode.Scrollable)
自定义导航栏
此时需要使用Tabs提供的onChange事件方法,监听索引index的变化,并将当前活跃的index值传递给currentIndex,实现页签的切换。
.onChange((index: number) => {
this.currentIndex = index
})
页面路由 (@ohos.router)
页面路由指在应用程序中实现不同页面之间的跳转和数据传递。Router模块通过不同的url地址,可以方便地进行页面路由,轻松地访问不同的页面。本文将从页面跳转、页面返回、页面返回前增加一个询问框和命名路由几个方面介绍Router模块提供的功能。
页面跳转
Router模块提供了两种跳转模式,分别是router.pushUrl()和router.replaceUrl()。这两种模式决定了目标页面是否会替换当前页。
Router模块提供了两种实例模式,分别是Standard和Single。这两种模式决定了目标url是否会对应多个实例。
router.pushUrl({
url: 'pages/Detail' // 目标url
}, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke pushUrl succeeded.');
});
router.replaceUrl({
url: 'pages/Profile' // 目标url
}, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke replaceUrl succeeded.');
})
如果需要在跳转时传递一些数据给目标页面,则可以在调用Router模块的方法时,添加一个params属性,并指定一个对象作为参数。例如:
router.pushUrl({
url: 'pages/Detail', // 目标url
params: paramsInfo // 添加params属性,传递自定义参数
}, (err) => {
if (err) {
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke pushUrl succeeded.');
})
返回到上一个页面。 router.back();
返回到指定页面。
router.back({
url: 'pages/Home'
});
返回到指定页面,并传递自定义参数信息。
router.back({
url: 'pages/Home',
params: {
info: '来自Home页'
}
});
系统默认询问框
// 定义一个返回按钮的点击事件处理函数
function onBackClick(): void {
// 调用router.showAlertBeforeBackPage()方法,设置返回询问框的信息
try {
router.showAlertBeforeBackPage({
message: '您还没有完成支付,确定要返回吗?' // 设置询问框的内容
});
} catch (err) {
let message = (err as BusinessError).message
let code = (err as BusinessError).code
console.error(`Invoke showAlertBeforeBackPage failed, code is ${code}, message is ${message}`);
}
// 调用router.back()方法,返回上一个页面
router.back();
使用图形
绘制几何图形 (Shape)
Shape(value?: PixelMap)
Shape() {
Rect().width(300).height(50)
}
绘制组件单独使用,用于在页面上绘制指定的图形。有7种绘制类型,分别为Circle(圆形)、Ellipse(椭圆形)、Line(直线)、Polyline(折线)、Polygon(多边形)、Path(路径)、Rect(矩形)。
Circle(options?: {width?: string | number, height?: string | number}
Circle({ width: 150, height: 150 })
形状视口viewport
viewPort{ x?: number | string, y?: number | string, width?: number | string, height?: number | string }
自定义样式
Path()
.width(100)
.height(100)
.commands('M150 0 L300 300 L0 300 Z')
.fill("#E87361")
Path()
.width(100)
.height(100)
.fillOpacity(0)
.commands('M150 0 L300 300 L0 300 Z')
.stroke(Color.Red)
使用画布绘制自定义图形 (Canvas)
使用CanvasRenderingContext2D对象在Canvas画布上绘制。
@Entry
@Component
struct CanvasExample1 {
//用来配置CanvasRenderingContext2D对象的参数,包括是否开启抗锯齿,true表明开启抗锯齿。
private settings: RenderingContextSettings = new RenderingContextSettings(true)
//用来创建CanvasRenderingContext2D对象,通过在canvas中调用CanvasRenderingContext2D对象来绘制。
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
//在canvas中调用CanvasRenderingContext2D对象。
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() => {
//可以在这里绘制内容。
this.context.strokeRect(50, 50, 200, 150);
})
}
.width('100%')
.height('100%')
}
}
离屏绘制是指将需要绘制的内容先绘制在缓存区,再将其转换成图片,一次性绘制到Canvas上,加快了绘制速度。过程为:
通过transferToImageBitmap方法将离屏画布最近渲染的图像创建为一个ImageBitmap对象。
通过CanvasRenderingContext2D对象的transferFromImageBitmap方法显示给定的ImageBitmap对象。
@Entry
@Component
struct CanvasExample2 {
//用来配置CanvasRenderingContext2D对象和OffscreenCanvasRenderingContext2D对象的参数,包括是否开启抗锯齿。true表明开启抗锯齿
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
//用来创建OffscreenCanvas对象,width为离屏画布的宽度,height为离屏画布的高度。通过在canvas中调用OffscreenCanvasRenderingContext2D对象来绘制。
private offCanvas: OffscreenCanvas = new OffscreenCanvas(600, 600)
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#F5DC62')
.onReady(() =>{
let offContext = this.offCanvas.getContext("2d", this.settings)
//可以在这里绘制内容
offContext.strokeRect(50, 50, 200, 150);
//将离屏绘值渲染的图像在普通画布上显示
let image = this.offCanvas.transferToImageBitmap();
this.context.transferFromImageBitmap(image);
})
}
.width('100%')
.height('100%')
}
}
使用动画
属性动画
属性变化能够引起UI的变化。例如,enabled属性用于控制组件是否可以响应点击、触摸等事件,但enable属性的变化不会引起UI的变化,因此不适合作为可动画属性。
属性在变化时适合添加动画作为过渡。例如,focusable属性决定当前组件是否可以获得焦点,当focusable属性发生变化时,应立即切换到终点值以响应用户行为,不应该加入动画效果,因此不适合作为可动画属性。
ArkUI提供两种属性动画接口animateTo和animation
使用animateTo产生属性动画
animateTo(value: AnimateParam, event: () => void): void
animateTo({ curve: curves.springMotion() }, () => {
this.animate = !this.animate;
// 第三步:闭包内通过状态变量改变UI界面
// 这里可以写任何能改变UI的逻辑比如数组添加,显隐控制,系统会检测改变后的UI界面与之前的UI界面的差异,对有差异的部分添加动画
// 组件一的rotate属性发生变化,所以会给组件一添加rotate旋转动画
this.rotateValue = this.animate ? 90 : 0;
// 组件二的透明度发生变化,所以会给组件二添加透度的动画
this.opacityValue = this.animate ? 0.6 : 1;
// 组件二的offset属性发生变化,所以会给组件二添加offset偏移动画
this.translateX = this.animate ? 50 : 0;
})
使用animation产生属性动画
.animation({ curve: curves.springMotion() })
this.animate = !this.animate;
// 第四步:闭包内通过状态变量改变UI界面
// 这里可以写任何能改变UI的逻辑比如数组添加,显隐控制,系统会检测改变后的UI界面与之前的UI界面的差异,对有差异的部分添加动画
// 组件一的rotate属性发生变化,所以会给组件一添加rotate旋转动画
this.rotateValue = this.animate ? 90 : 0;
// 组件二的offset属性发生变化,所以会给组件二添加offset偏移动画
this.translateX = this.animate ? 50 : 0;
// 父组件column的opacity属性有变化,会导致其子节点的透明度也变化,所以这里会给column和其子节点的透明度属性都加动画
this.opacityValue = this.animate ? 0.6 : 1;
animation只要检测到其绑定的可动画属性发生变化,就会自动添加属性动画
自定义属性动画
ArkUI提供@AnimatableExtend装饰器,用于自定义可动画属性接口。
自定义可动画属性接口的参数类型仅支持number类型和实现AnimtableArithmetic<T>接口的自定义类型
使用number数据类型和@AnimatableExtend装饰器改变Text组件宽度实现逐帧布局的效果
// 第一步:使用@AnimatableExtend装饰器,自定义可动画属性接口
@AnimatableExtend(Text)
function animatableWidth(width: number) {
.width(width)// 调用系统属性接口,逐帧回调函数每帧修改可动画属性的值,实现逐帧布局的效果。
}
.animatableWidth(this.textWidth)// 第二步:将自定义可动画属性接口设置到组件上
.animation({ duration: 2000, curve: Curve.Ease })// 第三步:为自定义可动画属性接口绑定动画
this.textWidth = this.textWidth == 80 ? 160 : 80;// 第四步:改变自定义可动画属性的参数,产生动画
转场动画
出现/消失转场
可以通过TransitionEffect的组合使用,定义出各式效果
创建TransitionEffect。
// 出现时会是所有转场效果的出现效果叠加,消失时会是所有消失转场效果的叠加
// 用于说明各个effect跟随的动画参数
private effect: object =
TransitionEffect.OPACITY // 创建了透明度转场效果,这里没有调用animation接口,会跟随animateTo的动画参数
// 通过combine方法,添加缩放转场效果,并指定了springMotion(0.6, 1.2)曲线
.combine(TransitionEffect.scale({ x: 0, y: 0 }).animation({ curve: curves.springMotion(0.6, 1.2) }))
// 添加旋转转场效果,这里的动画参数会跟随上面的TransitionEffect,也就是springMotion(0.6, 1.2)
.combine(TransitionEffect.rotate({ angle: 90 }))
// 添加平移转场效果,动画参数会跟随其之上带animation的TransitionEffect,也就是springMotion(0.6, 1.2)
.combine(TransitionEffect.translate({ x: 150, y: 150 })
// 添加move转场效果,并指定了springMotion曲线
.combine(TransitionEffect.move(TransitionEdge.END)).animation({curve: curves.springMotion()}))
// 添加非对称的转场效果,由于这里没有设置animation,会跟随上面的TransitionEffect的animation效果,也就是springMotion
.combine(TransitionEffect.asymmetric(TransitionEffect.scale({ x: 0, y: 0 }), TransitionEffect.rotate({ angle: 90 })));
将转场效果通过transition接口设置到组件。
Text('test')
.transition(effect)
导航转场
导航转场推荐使用Navigation组件实现,可搭配NavDestination组件实现导航功能
@Provide('pathInfos') pathInfos: NavPathStack = new NavPathStack();
@Consume('pathInfos') pathInfos: NavPathStack;
this.pathInfos.pushPathByName(`${item}`, '详情页面参数')
.navDestination(this.PageMap)
mode(NavigationMode.Auto)
模态转场
模态转场是新的界面覆盖在旧的界面上,旧的界面不消失的一种转场方式。
使用bindContentCover构建全屏模态转场效果
import curves from '@ohos.curves';
interface PersonList {
name: string,
cardnum: string
}
@Entry
@Component
struct BindContentCoverDemo {
private personList: Array<PersonList> = [ { name: '王**', cardnum: '1234***********789' }, { name: '宋*', cardnum: '2345***********789' }, { name: '许**', cardnum: '3456***********789' }, { name: '唐*', cardnum: '4567***********789' } ];
// 第一步:定义全屏模态转场效果bindContentCover
// 模态转场控制变量
@State isPresent: boolean = false;
// 第二步:定义模态展示界面
// 通过@Builder构建模态展示界面
@Builder
MyBuilder() {
Column() {
Row() {
Text('选择乘车人')
.fontSize(20)
.fontColor(Color.White)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 30, bottom: 15 })
}
.backgroundColor(0x007dfe)
Row() {
Text('+ 添加乘车人')
.fontSize(16)
.fontColor(0x333333)
.margin({ top: 10 })
.padding({ top: 20, bottom: 20 })
.width('92%')
.borderRadius(10)
.textAlign(TextAlign.Center)
.backgroundColor(Color.White)
}
Column() {
ForEach(this.personList, (item: PersonList, index: number) => {
Row() {
Column() {
if (index % 2 == 0) {
Column()
.width(20)
.height(20)
.border({ width: 1, color: 0x007dfe })
.backgroundColor(0x007dfe)
} else {
Column()
.width(20)
.height(20)
.border({ width: 1, color: 0x007dfe })
}
}
.width('20%')
Column() {
Text(item.name)
.fontColor(0x333333)
.fontSize(18)
Text(item.cardnum)
.fontColor(0x666666)
.fontSize(14)
}
.width('60%')
.alignItems(HorizontalAlign.Start)
Column() {
Text('编辑')
.fontColor(0x007dfe)
.fontSize(16)
}
.width('20%')
}
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: 0xf1f1f1 })
.width('92%')
.backgroundColor(Color.White)
})
}
.padding({ top: 20, bottom: 20 })
Text('确认')
.width('90%')
.height(40)
.textAlign(TextAlign.Center)
.borderRadius(10)
.fontColor(Color.White)
.backgroundColor(0x007dfe)
.onClick(() => {
this.isPresent = !this.isPresent;
})
}
.size({ width: '100%', height: '100%' })
.backgroundColor(0xf5f5f5)
// 通过转场动画实现出现消失转场动画效果
.transition(TransitionEffect.translate({ y: 1000 }).animation({ curve: curves.springMotion(0.6, 0.8) }))
}
build() {
Column() {
Row() {
Text('确认订单')
.fontSize(20)
.fontColor(Color.White)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 30, bottom: 60 })
}
.backgroundColor(0x007dfe)
Column() {
Row() {
Column() {
Text('00:25')
Text('始发站')
}
.width('30%')
Column() {
Text('G1234')
Text('8时1分')
}
.width('30%')
Column() {
Text('08:26')
Text('终点站')
}
.width('30%')
}
}
.width('92%')
.padding(15)
.margin({ top: -30 })
.backgroundColor(Color.White)
.shadow({ radius: 30, color: '#aaaaaa' })
.borderRadius(10)
Column() {
Text('+ 选择乘车人')
.fontSize(18)
.fontColor(Color.Orange)
.fontWeight(FontWeight.Bold)
.padding({ top: 10, bottom: 10 })
.width('60%')
.textAlign(TextAlign.Center)
.borderRadius(15)// 通过选定的模态接口,绑定模态展示界面,ModalTransition是内置的ContentCover转场动画类型,这里选择DEFAULT代表设置上下切换动画效果,通过onDisappear控制状态变量变换。
.bindContentCover(this.isPresent, this.MyBuilder(), {
modalTransition: ModalTransition.DEFAULT,
onDisappear: () => {
this.isPresent = !this.isPresent;
}
})
.onClick(() => {
// 第三步:通过模态接口调起模态展示界面,通过转场动画或者共享元素动画去实现对应的动画效果
// 改变状态变量,显示模态界面
this.isPresent = !this.isPresent;
})
}
.padding({ top: 60 })
}
}
}
共享元素转场
共享元素转场是一种界面切换时对相同或者相似的两个元素做的一种位置和大小匹配的过渡动画效果,也称一镜到底动效。
使用geometryTransition共享元素转场实现一镜到底动效
geometryTransition用于组件内隐式共享元素转场,在视图状态切换过程中提供丝滑的上下文继承过渡体验。
mport curves from '@ohos.curves';
@Entry
@Component
struct IfElseGeometryTransition {
@State isShow: boolean = false;
build() {
Stack({ alignContent: Alignment.Center }) {
if (this.isShow) {
Image($r('app.media.spring'))
.autoResize(false)
.clip(true)
.width(200)
.height(200)
.borderRadius(100)
.geometryTransition("picture")
.transition(TransitionEffect.OPACITY)
// 在打断场景下,即动画过程中点击页面触发下一次转场,如果不加id,则会出现重影
// 加了id之后,新建的spring图片会复用之前的spring图片节点,不会重新创建节点,也就不会有重影问题
// 加id的规则为加在if和else下的第一个节点上,有多个并列节点则也需要进行添加
.id('item1')
} else {
// geometryTransition此处绑定的是容器,那么容器内的子组件需设为相对布局跟随父容器变化,
// 套多层容器为了说明相对布局约束传递
Column() {
Column() {
Image($r('app.media.sky'))
.size({ width: '100%', height: '100%' })
}
.size({ width: '100%', height: '100%' })
}
.width(100)
.height(100)
// geometryTransition会同步圆角,但仅限于geometryTransition绑定处,此处绑定的是容器
// 则对容器本身有圆角同步而不会操作容器内部子组件的borderRadius
.borderRadius(50)
.clip(true)
.geometryTransition("picture")
// transition保证节点离场不被立即析构,设置通用转场效果
.transition(TransitionEffect.OPACITY)
.position({ x: 40, y: 40 })
.id('item2')
}
}
.onClick(() => {
animateTo({
curve: curves.springMotion()
}, () => {
this.isShow = !this.isShow;
})
})
.size({ width: '100%', height: '100%' })
}
}
组件动画
动画曲线
传统曲线
ArkUI提供了贝塞尔曲线、阶梯曲线等传统曲线接口,开发者可参照插值计算进行查阅
class MyCurve {
public title: string;
public curve: Curve;
public color: Color | string;
constructor(title: string, curve: Curve, color: Color | string = '') {
this.title = title;
this.curve = curve;
this.color = color;
}
}
const myCurves: MyCurve[] = [ new MyCurve(' Linear', Curve.Linear, '#317AF7'), new MyCurve(' Ease', Curve.Ease, '#D94838'), new MyCurve(' EaseIn', Curve.EaseIn, '#DB6B42'), new MyCurve(' EaseOut', Curve.EaseOut, '#5BA854'), new MyCurve(' EaseInOut', Curve.EaseInOut, '#317AF7'), new MyCurve(' FastOutSlowIn', Curve.FastOutSlowIn, '#D94838')]
@Entry
@Component
export struct CurveDemo {
@State dRotate: number = 0; // 旋转角度
build() {
Column() {
// 曲线图例
Grid() {
ForEach(myCurves, (item: MyCurve) => {
GridItem() {
Column() {
Row()
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor(item.color)
Text(item.title)
.fontSize(15)
.fontColor(0x909399)
}
.width('100%')
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.padding(10)
.width('100%')
.height(300).margin({top:50})
Stack() {
// 摆动管道
Row()
.width(290)
.height(290)
.border({
width: 15,
color: 0xE6E8EB,
radius: 145
})
ForEach(myCurves, (item: MyCurve) => {
// 小球
Column() {
Row()
.width(30)
.height(30)
.borderRadius(15)
.backgroundColor(item.color)
}
.width(20)
.height(300)
.rotate({ angle: this.dRotate })
.animation({ duration: 2000, iterations: -1, curve: item.curve, delay: 100 })
})
}
.width('100%')
.height(200)
.onClick(() => {
this.dRotate ? null : this.dRotate = 360;
})
}
.width('100%')
}
}
弹簧曲线
ArkUI提供了四种阻尼弹簧曲线接口。
springMotion:创建弹性动画
responsiveSpringMotion:是springMotion动画的一种特例
interpolatingSpring:适合于需要指定初速度的动效场景
springCurve:适合于需要直接指定动画时长的场景
import curves from '@ohos.curves';
class Spring {
public title: string;
public subTitle: string;
public iCurve: ICurve;
constructor(title: string, subTitle: string, iCurve: ICurve) {
this.title = title;
this.iCurve = iCurve;
this.subTitle = subTitle;
}
}
// 弹簧组件
@Component
struct Motion {
@Prop dRotate: number = 0
private title: string = ""
private subTitle: string = ""
private iCurve: ICurve | undefined = undefined
build() {
Column() {
Circle()
.translate({ y: this.dRotate })
.animation({ curve: this.iCurve, iterations: -1 })
.foregroundColor('#317AF7')
.width(30)
.height(30)
Column() {
Text(this.title)
.fontColor(Color.Black)
.fontSize(10).height(30)
Text(this.subTitle)
.fontColor(0xcccccc)
.fontSize(10).width(50)
}
.borderWidth({ top: 1 })
.borderColor(0xf5f5f5)
.width(80)
.alignItems(HorizontalAlign.Center)
.height(100)
}
.height(110)
.margin({ bottom: 5 })
.alignItems(HorizontalAlign.Center)
}
}
@Entry
@Component
export struct SpringCurve {
@State dRotate: number = 0;
private springs: Spring[] = [ new Spring('springMotion', '周期2, 阻尼0.25', curves.springMotion(1, 0.25)), new Spring('responsive' + '\n' + 'SpringMotion', '默认弹性跟手曲线', curves.responsiveSpringMotion(1, 0.25)), new Spring('interpolating' + '\n' + 'Spring', '初始速度10,质量1, 剛度228, 阻尼30', curves.interpolatingSpring(10, 1, 228, 30)), new Spring('springCurve', '初始速度10, 质量1, 剛度228, 阻尼30', curves.springCurve(10, 1, 228, 30)) ];
build() {
Row() {
ForEach(this.springs, (item: Spring) => {
Motion({ title: item.title, subTitle: item.subTitle, iCurve: item.iCurve, dRotate: this.dRotate })
})
}
.justifyContent(FlexAlign.Center).alignItems(VerticalAlign.Bottom)
.width('100%')
.height(437)
.margin({ top: 20 })
.onClick(() => {
this.dRotate = -50;
})
}
}
动画效果
模糊
backdropBlur 为当前组件添加背景模糊效果,入参为模糊半径。
blur 为当前组件添加内容模糊效果,入参为模糊半径。
backgroundBlurStyle 为当前组件添加背景模糊效果,入参为模糊样式。
foregroundBlurStyle 为当前组件添加内容模糊效果,入参为模糊样式。
使用blur为组件添加内容模糊
@Entry
@Component
struct Index1 {
@State radius: number = 0;
@State text: string = '';
@State y: string = '手指不在屏幕上';
aboutToAppear() {
this.text = "按住屏幕上下滑动\n" + "当前手指所在y轴位置 : " + this.y +
"\n" + "当前图片模糊程度为 : " + this.radius;
}
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
Text(this.text)
.height(200)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontFamily("cursive")
.fontStyle(FontStyle.Italic)
Image($r("app.media.wall"))
.blur(this.radius) // 使用blur接口为照片组件添加内容模糊效果
.height('100%')
.width("100%")
.objectFit(ImageFit.Cover)
}.height('100%')
.width("100%")
.onTouch((event?: TouchEvent) => {
if(event){
if (event.type === TouchType.Move) {
this.y = Number(event.touches[0].y.toString()).toString();
this.radius = Number(this.y) / 10; // 根据跟手过程中的滑动距离修改模糊半径,配合模糊接口,形成跟手模糊效果
}
if (event.type === TouchType.Up) {
this.radius = 0;
this.y = '手指离开屏幕';
}
}
this.text = "按住屏幕上下滑动\n" + "当前手指所在y轴位置 : " + this.y +
"\n" + "当前图片模糊程度为 : " + this.radius;
})
}
}
阴影
阴影接口shadow可以为当前组件添加阴影效果,该接口支持两种类型参数,开发者可配置ShadowOptions自定义阴影效果。ShadowOptions模式下,当radius = 0 或者 color 的透明度为0时,无阴影效果。
.shadow({ radius: 10, color: Color.Gray })
.shadow({ radius: 10, color: Color.Gray, offsetX: 20, offsetY: 20 })
色彩
linearGradient 为当前组件添加线性渐变的颜色渐变效果
sweepGradient 为当前组件添加角度渐变的颜色渐变效果。
radialGradient 为当前组件添加径向渐变的颜色渐变效果。
支持交互事件
触屏事件:手指或手写笔在触屏上的单指或单笔操作。
键鼠事件:包括外设鼠标或触控板的操作事件和外设键盘的按键事件。
鼠标事件是指通过连接和使用外设鼠标/触控板操作时所响应的事件。
按键事件是指通过连接和使用外设键盘操作时所响应的事件。
焦点事件:通过以上方式控制组件焦点的能力和响应的事件。
手势事件由绑定手势方法和绑定的手势组成,绑定的手势可以分为单一手势和组合手势两种类型,根据手势的复杂程度进行区分。
绑定手势方法:用于在组件上绑定单一手势或组合手势,并声明所绑定的手势的响应优先级。
单一手势:手势的基本单元,是所有复杂手势的组成部分。
组合手势:由多个单一手势组合而成,可以根据声明的类型将多个单一手势按照一定规则组合成组合手势,并进行使用。
触屏事件
点击事件
onClick(event: (event?: ClickEvent) => void)
拖拽事件
拖拽事件指手指/手写笔长按组件(>=500ms),并拖拽到接收区域释放的事件。
onDragStart(event: (event?: DragEvent, extraParams?: string) => CustomBuilder | DragItemInfo) 拖拽启动接口。当前仅支持自定义pixelmap和自定义组件。
onDragEnter(event: (event?: DragEvent, extraParams?: string) => void) 拖拽进入组件接口。DragEvent定义拖拽发生位置,extraParmas表示用户自定义信息
onDragLeave(event: (event?: DragEvent, extraParams?: string) => void) 拖拽离开组件接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。
onDragMove(event: (event?: DragEvent, extraParams?: string) => void) 拖拽移动接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。
onDrop(event: (event?: DragEvent, extraParams?: string) => void) 拖拽释放组件接口。DragEvent定义拖拽发生位置,extraParmas表示拖拽事件额外信息。
触摸事件
onTouch(event: (event?: TouchEvent) => void)
event.type为TouchType.Down:表示手指按下。
event.type为TouchType.Up:表示手指抬起。
event.type为TouchType.Move:表示手指按住移动。
键鼠事件
onHover(event: (isHover: boolean) => void)
鼠标进入或退出组件时触发该回调。
isHover:表示鼠标是否悬浮在组件上,鼠标进入时为true, 退出时为false。
onMouse(event: (event?: MouseEvent) => void) 当前组件被鼠标按键点击时或者鼠标在组件上悬浮移动时,触发该回调,event返回值包含触发事件时的时间戳、鼠标按键、动作、鼠标位置在整个屏幕上的坐标和相对于当前组件的坐标。
焦点事件
指向当前应用界面上唯一的一个可交互元素,
使用requestFocus主动给指定组件申请焦点、组件focusOnTouch属性为true后点击组件。
监听组件的焦点变化
获焦事件回调 onFocus(event: () => void)
失焦事件回调 onBlur(event:() => void)
设置组件是否获焦 focusable(value: boolean)
使用手势事件
绑定手势方法
gesture(常规手势绑定方法)
.gesture(gesture: GestureType, mask?: GestureMask)
// 采用gesture手势绑定方法绑定TapGesture
.gesture(
TapGesture()
.onAction(() => {
console.info('TapGesture is onAction');
}))
priorityGesture(带优先级的手势绑定方法)
.priorityGesture(gesture: GestureType, mask?: GestureMask)
parallelGesture(并行手势绑定方法)
.parallelGesture(gesture: GestureType, mask?: GestureMask)
单一手势
点击手势(TapGesture)
TapGesture(value?:{count?:number; fingers?:number})
长按手势(LongPressGesture)
LongPressGesture(value?:{fingers?:number; repeat?:boolean; duration?:number})
拖动手势(PanGesture)
PanGesture(value?:{ fingers?:number; direction?:PanDirection; distance?:number})
direction:用于声明触发拖动的手势方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为Pandirection.All。
distance:用于声明触发拖动的最小拖动识别距离,单位为vp,默认值为5。
捏合手势(PinchGesture)
PinchGesture(value?:{fingers?:number; distance?:number})
旋转手势(RotationGesture)
RotationGesture(value?:{fingers?:number; angle?:number})
滑动手势(SwipeGesture)
SwipeGesture(value?:{fingers?:number; direction?:SwipeDirection; speed?:number})
组合手势
组合手势由多种单一手势组合而成,通过在GestureGroup中使用不同的GestureMode来声明该组合手势的类型,支持顺序识别、并行识别和互斥识别三种类型。
顺序识别
GestureGroup(GestureMode.Sequence,
并行识别
GestureGroup(GestureMode.Parallel,
互斥识别
GestureGroup(GestureMode.Exclusive,
应用深浅色适配
onCreate(): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_DARK);
}
窗口管理
管理应用窗口(Stage模型)
窗口沉浸式能力:指对状态栏、导航栏等系统窗口进行控制,减少状态栏导航栏等系统界面的突兀感,从而使用户获得最佳体验的能力。
悬浮窗:全局悬浮窗口是一种特殊的应用窗口
设置应用主窗口
通过getMainWindow接口获取应用主窗口。
windowStage.getMainWindow((err: BusinessError, data) => {
// 2.设置主窗口属性。以设置"是否可触"属性为例。
windowClass.setWindowTouchable(isTouchable, (err: BusinessError) => {
设置应用子窗口
通过createSubWindow接口创建应用子窗口。
windowStage_.createSubWindow("mySubWindow", (err: BusinessError, data) => {
// 2.子窗口创建成功后,设置子窗口的位置、大小及相关属性等。
sub_windowClass.moveWindowTo(300, 300, (err: BusinessError) => {
sub_windowClass.resize(500, 500, (err: BusinessError) => {
// 3.为子窗口加载对应的目标页面。
sub_windowClass.setUIContent("pages/page3", (err: BusinessError) => {
// 3.显示子窗口。
(sub_windowClass as window.Window).showWindow((err: BusinessError) => {
// 4.销毁子窗口。当不再需要子窗口时,可根据具体实现逻辑,使用destroy对其进行销毁。
(sub_windowClass as window.Window).destroyWindow((err: BusinessError) => {
体验窗口沉浸式能力
通过getMainWindow接口获取应用主窗口。
方式一:应用主窗口为全屏窗口时,调用setWindowSystemBarEnable接口,设置导航栏、状态栏不显示,从而达到沉浸式效果。
方式二:调用setWindowLayoutFullScreen接口,设置应用主窗口为全屏布局;然后调用setWindowSystemBarProperties接口,设置导航栏、状态栏的透明度、背景/文字颜色以及高亮图标等属性,使之保持与主窗口显示协调一致,从而达到沉浸式效果。
设置悬浮窗(受限开放)
ohos.permission.SYSTEM_FLOAT_WINDOW权限
//创建悬浮框
let config: window.Configuration = {
name: "floatWindow", windowType: window.WindowType.TYPE_FLOAT, ctx: this.context
};
window.createWindow(config, (err: BusinessError, data) => {
// 2.悬浮窗窗口创建成功后,设置悬浮窗的位置、大小及相关属性等。
windowClass.moveWindowTo(300, 300, (err: BusinessError) => {
windowClass.resize(500, 500, (err: BusinessError) => {
// 3.为悬浮窗加载对应的目标页面。
windowClass.setUIContent("pages/page4", (err: BusinessError) => {
// 3.显示悬浮窗。
(windowClass as window.Window).showWindow((err: BusinessError) => {
// 4.销毁悬浮窗。当不再需要悬浮窗时,可根据具体实现逻辑,使用destroy对其进行销毁。
windowClass.destroyWindow((err: BusinessError) => {
ArkGraphics 2D
ArkGraphics 2D(方舟2D图形服务 )主要提供图形绘制与显示相关的能力。
提供图像处理的一些基本能力,包括对当前图像的亮度调节、模糊化、灰度调节、智能取色等。具体可见@ohos.effectKit (图像效果)。
提供管理抽象化色域对象的基础能力,包括色域的创建、色域基础属性的获取等。具体可见@ohos.graphics.colorSpaceManager (色彩管理)。
提供可针对不同形式的内容指定帧率的能力,可用于开发者自绘制内容。具体可见可变帧率简介。
提供高动态显示的相关能力,具体可见@ohos.graphics.hdrCapability (HDR能力)。
提供自绘制的相关能力,开发者可根据需要,自定义绘制实现UI效果,可自定义绘制基础形状、文本、图片等。具体可见@ohos.graphics.drawing (绘制模块)。
提供图形绘制与显示相关的Native能力,包括NativeWindow、NativeBuffer、NativeImage、NativeVsync、Drawing等模块。
请求动画绘制帧率
.animation({
duration: 1200,
iterations: 10,
expectedFrameRateRange: { // 设置属性动画的帧率范围
expected: 60, // 设置动画的期望帧率为60hz
min: 0, // 设置帧率范围
max: 120, // 设置帧率范围
},
})
请求UI绘制帧率
import displaySync from '@ohos.graphics.displaySync';
DisplaySync实例设置帧率和注册订阅函数。
CreateDisplaySyncSlow() {
let range : ExpectedFrameRateRange = { // 创建和配置帧率参数
expected: 30, // 设置期望绘制帧率为30hz
min: 0, // 配置帧率范围
max: 120 // 配置帧率范围
};
let draw30 = (intervalInfo: displaySync.IntervalInfo) => { // 订阅回调函数,字体大小在25到150之间变化
if (this.isBigger_30) {
this.drawFirstSize += 1;
if (this.drawFirstSize > 150) {
this.isBigger_30 = false;
}
} else {
this.drawFirstSize -= 1;
if (this.drawFirstSize < 25) {
this.isBigger_30 = true;
}
}
};
this.backDisplaySyncSlow = displaySync.create(); // 创建DisplaySync实例
this.backDisplaySyncSlow.setExpectedFrameRateRange(range); // 设置帧率
this.backDisplaySyncSlow.on("frame", draw30); // 订阅frame事件和注册订阅函数
}
this.backDisplaySyncSlow.start();
this.backDisplaySyncFast.stop();
请求自绘制内容绘制帧率
对于基于XComponent进行Native开发的业务,可以请求独立的绘制帧率进行内容开发,
OH_NativeXComponent_SetExpectedFrameRateRange (OH_NativeXComponent *component, OH_NativeXComponent_ExpectedRateRange *range) 设置帧期望的帧率范围。
H_NativeXComponent_RegisterOnFrameCallback (OH_NativeXComponent *component, OH_NativeXComponent_OnFrameCallback *callback) 设置每帧回调函数,同时启动每帧回调。
OH_NativeXComponent_UnRegisterOnFrameCallback (OH_NativeXComponent *component) 取消注册的每帧回调函数,同时停止调用回调函数。
ArkWeb
ArkWeb(方舟Web)提供了Web组件,用于在应用程序中显示Web页面内容,为开发者提供页面加载、页面交互、页面调试等能力。
页面调试:Web组件支持使用Devtools工具调试前端页面。
默认UserAgent定义
使用Web组件加载页面
需要配置ohos.permission.INTERNET网络访问权限。
加载网络页面
可以通过调用loadUrl()接口加载指定的网页
// xxx.ets
import web_webview from '@ohos.web.webview';
import business_error from '@ohos.base';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Button('loadUrl')
.onClick(() => {
try {
// 点击按钮时,通过loadUrl,跳转到www.example1.com
this.controller.loadUrl('www.example1.com');
} catch (error) {
let e: business_error.BusinessError = error as business_error.BusinessError;
console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
}
})
// 组件创建时,加载www.example.com
Web({ src: 'www.example.com', controller: this.controller})
}
}
}
加载本地页面
将资源文件放置在应用的resources/rawfile目录下
// xxx.ets
import web_webview from '@ohos.web.webview';
import business_error from '@ohos.base';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Button('loadUrl')
.onClick(() => {
try {
// 点击按钮时,通过loadUrl,跳转到local1.html
this.controller.loadUrl($rawfile("local1.html"));
} catch (error) {
let e: business_error.BusinessError = error as business_error.BusinessError;
console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
}
})
// 组件创建时,通过$rawfile加载本地文件local.html
Web({ src: $rawfile("local.html"), controller: this.controller })
}
}
}
加载HTML格式的文本数据
Web组件可以通过loadData()接口实现加载HTML格式的文本数据。
// xxx.ets
import web_webview from '@ohos.web.webview';
import business_error from '@ohos.base';
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
build() {
Column() {
Button('loadData')
.onClick(() => {
try {
// 点击按钮时,通过loadData,加载HTML格式的文本数据
this.controller.loadData(
"<html><body bgcolor=\"white\">Source:<pre>source</pre></body></html>",
"text/html",
"UTF-8"
);
} catch (error) {
let e: business_error.BusinessError = error as business_error.BusinessError;
console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
}
})
// 组件创建时,加载www.example.com
Web({ src: 'www.example.com', controller: this.controller })
}
}
}
动态创建web组件
// 创建NodeController
// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用
export class myNodeController extends NodeController {
// 创建节点,需要uiContext
this.rootnode = new BuilderNode(uiContext)
// 创建动态Web组件
this.rootnode.build(wrap, { url:url, controller:control })
设置深色模式
通过darkMode()接口可以配置不同的深色模式,默认关闭。
上传文件
Web组件支持前端页面选择文件上传功能,应用开发者可以使用onShowFileSelector()接口来处理前端页面文件上传的请求。
在新窗口中打开页面
Web组件提供了在新窗口打开页面的能力,开发者可以通过multiWindowAccess()接口来设置是否允许网页在新窗口打开。当有新窗口打开时,应用侧会在onWindowNew()接口中收到Web组件新窗口事件,开发者需要在此接口事件中,新建窗口来处理Web组件窗口请求。
allowWindowOpenMethod()接口设置为true时,前端页面通过JavaScript函数调用的方式打开新窗口。
如果开发者在onWindowNew()接口通知中不需要打开新窗口,需要将ControllerHandler.setWebController()接口参数设置成null。
// xxx.ets
import web_webview from '@ohos.web.webview'
// 在同一page页有两个web组件。在WebComponent新开窗口时,会跳转到NewWebViewComp。
@CustomDialog
struct NewWebViewComp {
controller?: CustomDialogController
webviewController1: web_webview.WebviewController = new web_webview.WebviewController()
build() {
Column() {
Web({ src: "", controller: this.webviewController1 })
.javaScriptAccess(true)
.multiWindowAccess(false)
.onWindowExit(() => {
console.info("NewWebViewComp onWindowExit")
if (this.controller) {
this.controller.close()
}
})
}
}
}
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController()
dialogController: CustomDialogController | null = null
build() {
Column() {
Web({ src: $rawfile("window.html"), controller: this.controller })
.javaScriptAccess(true)
// 需要使能multiWindowAccess
.multiWindowAccess(true)
.allowWindowOpenMethod(true)
.onWindowNew((event) => {
if (this.dialogController) {
this.dialogController.close()
}
let popController: web_webview.WebviewController = new web_webview.WebviewController()
this.dialogController = new CustomDialogController({
builder: NewWebViewComp({ webviewController1: popController })
})
this.dialogController.open()
// 将新窗口对应WebviewController返回给Web内核。
// 如果不需要打开新窗口请调用event.handler.setWebController接口设置成null。
// 若不调用event.handler.setWebController接口,会造成render进程阻塞。
event.handler.setWebController(popController)
})
}
}
}
管理位置权限
Web组件提供位置权限管理能力。开发者可以通过onGeolocationShow()接口对某个网站进行位置权限管理。Web组件根据接口响应结果,决定是否赋予前端页面权限。获取设备位置,需要开发者配置ohos.permission.LOCATION,ohos.permission.APPROXIMATELY_LOCATION权限,并同时在设备上打开应用的位置权限和控制中心的位置信息。
使用隐私模式
开发者在创建Web组件时,可以将可选参数incognitoMode设置为'true',来开启Web组件的隐私模式。 当使用隐私模式时,浏览网页时的cookies、 cache data 等数据不会保存在本地的持久化文件,当隐私模式的Web组件被销毁时,cookies、 cache data等数据将不被记录下来。
Web组件嵌套滚动
内嵌在可滚动容器(Scroll、List...)中的Web组件,接收到滑动手势事件,需要对接ArkUI框架的NestedScrollMode枚举类型,使得Web组件可以嵌套ArkUI可滚动容器,进行嵌套滚动。开发者可以在Web组件创建时,使用nestedScroll属性接口指定默认的嵌套滚动模式,也允许在过程中动态改变嵌套滚动的模式。
nestedScroll入参为一个NestedScrollOptions对象,该对象具有两个属性,分别为scrollForward和scrollBackward,每一个属性都为一个NestedScrollMode枚举类型。
应用侧调用前端页面函数
应用侧可以通过runJavaScript()方法调用前端页面的JavaScript相关函数。
this.webviewController.runJavaScript('htmlTest(param)');
// 传递runJavaScript侧代码方法。
this.webviewController.runJavaScript(`function changeColor(){document.getElementById('text').style.color = 'red'}`);
前端页面调用应用侧函数
注册应用侧代码有两种方式,一种在Web组件初始化调用,使用javaScriptProxy()接口。另外一种在Web组件初始化完成后调用,使用registerJavaScriptProxy()接口。
javaScriptProxy()接口使用示例如下。
// 将对象注入到web端
.javaScriptProxy({
object: this.testObj,
name: "testObjName",
methodList: ["test"],
controller: this.webviewController
})
应用侧使用registerJavaScriptProxy()接口注册。
try {
this.webviewController.registerJavaScriptProxy(this.testObj, "testObjName", ["test", "toString"]);
} catch (error) {
let e: business_error.BusinessError = error as business_error.BusinessError;
console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
}
建立应用侧与前端页面数据通道
前端页面和应用侧之间可以用createWebMessagePorts()接口创建消息端口来实现两端的通信。
管理页面跳转及浏览记录导航
可以通过forward()和backward()接口向前/向后浏览上一个/下一个历史记录。
if (this.webviewController.accessBackward()) {
this.webviewController.backward();
}
页面跳转
当点击网页中的链接需要跳转到应用内其他页面时,可以通过使用Web组件的onLoadIntercept()接口来实现。
跨应用跳转
let url: string = event.data.getRequestUrl();
// 判断链接是否为拨号链接
if (url.indexOf('tel://') === 0) {
// 跳转拨号界面
call.makeCall(url.substring(6), (err) => {
if (!err) {
console.info('make call succeeded.');
} else {
console.info('make call fail, err is:' + JSON.stringify(err));
}
});
return true;
}
}
管理Cookie及数据存储
Cookie是网络访问过程中,由服务端发送给客户端的一小段数据。
try {
web_webview.WebCookieManager.configCookieSync('https://www.example.com', 'value=test');
} catch (error) {
let e: business_error.BusinessError = error as business_error.BusinessError;
console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
}
缓存与存储管理
Cache
使用cacheMode()配置页面资源的缓存模式,Web组件为开发者提供四种缓存模式,分别为:
Default : 优先使用未过期的缓存,如果缓存不存在,则从网络获取。
None : 加载资源使用cache,如果cache中无该资源则从网络中获取。
Online : 加载资源不使用cache,全部从网络中获取。
Only :只从cache中加载资源。
Dom Storage
自定义页面请求响应
开发者通过onInterceptRequest()接口来实现自定义资源请求响应 。
.onInterceptRequest((event) => {
if (event) {
console.info('url:' + event.request.getRequestUrl());
// 拦截页面请求
if (event.request.getRequestUrl() !== 'https://www.example.com/test.html') {
return null;
}
}
使用Web组件打印前端页面
需配置ohos.permission.PRINT打印权限。
使用W3C标准协议接口拉起打印
通过调用应用侧接口拉起打印。
应用侧通过调用createWebPrintDocumentAdapter创建打印适配器,通过将适配器传入打印的print接口调起打印。
try {
let webPrintDocadapter = this.controller.createWebPrintDocumentAdapter('example.pdf');
print.print('example_jobid', webPrintDocadapter, null, getContext());
} catch (error) {
let e: business_error.BusinessError = error as business_error.BusinessError;
console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
}
使用Web组件的下载能力
通过setDownloadDelegate()向Web组件注册一个DownloadDelegate来监听页面触发的下载任务。资源由Web组件来下载,Web组件会通过DownloadDelegate将下载的进度通知给应用。
delegate: web_webview.WebDownloadDelegate = new web_webview.WebDownloadDelegate();
this.delegate.onBeforeDownload((webDownloadItem: web_webview.WebDownloadItem) => {
console.log("will start a download.");
// 传入一个下载路径,并开始下载。
// 如果传入一个不存在的路径,则会下载到默认/data/storage/el2/base/cache/web/目录。
webDownloadItem.start("/data/storage/el2/base/cache/web/" + webDownloadItem.getSuggestedFileName());
})
this.delegate.onDownloadUpdated((webDownloadItem: web_webview.WebDownloadItem) => {
// 下载任务的唯一标识。
console.log("download update guid: " + webDownloadItem.getGuid());
// 下载的进度。
console.log("download update guid: " + webDownloadItem.getPercentComplete());
// 当前的下载速度。
console.log("download update speed: " + webDownloadItem.getCurrentSpeed())
})
this.delegate.onDownloadFailed((webDownloadItem: web_webview.WebDownloadItem) => {
console.log("download failed guid: " + webDownloadItem.getGuid());
// 下载任务失败的错误码。
console.log("download failed guid: " + webDownloadItem.getLastErrorCode());
})
this.delegate.onDownloadFinish((webDownloadItem: web_webview.WebDownloadItem) => {
console.log("download finish guid: " + webDownloadItem.getGuid());
})
this.controller.setDownloadDelegate(this.delegate);
使用Web组件发起一个下载任务
使用startDownload()接口发起一个下载
// xxx.ets
import web_webview from '@ohos.web.webview'
import business_error from '@ohos.base'
@Entry
@Component
struct WebComponent {
controller: web_webview.WebviewController = new web_webview.WebviewController();
delegate: web_webview.WebDownloadDelegate = new web_webview.WebDownloadDelegate();
build() {
Column() {
Button('setDownloadDelegate')
.onClick(() => {
try {
this.delegate.onBeforeDownload((webDownloadItem: web_webview.WebDownloadItem) => {
console.log("will start a download.");
// 传入一个下载路径,并开始下载。
webDownloadItem.start("/data/storage/el2/base/cache/web/" + webDownloadItem.getSuggestedFileName());
})
this.delegate.onDownloadUpdated((webDownloadItem: web_webview.WebDownloadItem) => {
console.log("download update guid: " + webDownloadItem.getGuid());
})
this.delegate.onDownloadFailed((webDownloadItem: web_webview.WebDownloadItem) => {
console.log("download failed guid: " + webDownloadItem.getGuid());
})
this.delegate.onDownloadFinish((webDownloadItem: web_webview.WebDownloadItem) => {
console.log("download finish guid: " + webDownloadItem.getGuid());
})
this.controller.setDownloadDelegate(this.delegate);
} catch (error) {
let e:business_error.BusinessError = error as business_error.BusinessError;
console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
}
})
Button('startDownload')
.onClick(() => {
try {
// 这里指定下载地址为 https://www.example.com/,Web组件会发起一个下载任务将该页面下载下来。
// 开发者需要替换为自己想要下载的内容的地址。
this.controller.startDownload('https://www.example.com/');
} catch (error) {
let e:business_error.BusinessError = error as business_error.BusinessError;
console.error(`ErrorCode: ${e.code}, Message: ${e.message}`);
}
})
Web({ src: 'www.example.com', controller: this.controller })
}
}
}
使用预连接和预加载能力加速web页面的访问
可以通过prepareForPageLoad()来预解析或者预连接将要加载的页面。
Web({ src: 'https://www.example.com/', controller: this.webviewController})
.onAppear(() => {
// 指定第二个参数为true,代表要进行预连接,如果为false该接口只会对网址进行dns预解析
// 第三个参数为要预连接socket的个数。最多允许6个。
web_webview.WebviewController.prepareForPageLoad('https://www.example.com/', true, 2);
})
可以通过prefetchPage()来预加载即将要加载页面。
.onPageEnd(() => {
// 预加载https://www.iana.org/help/example-domains。
this.webviewController.prefetchPage('https://www.iana.org/help/example-domains');
})
解决Web组件本地资源跨域问题
本地资源跨域问题解决方法
需要开发者使用Web组件的onInterceptRequest对本地资源进行拦截替换。
HTTP数据请求
应用通过HTTP发起一个数据请求,支持常见的GET、POST、OPTIONS、HEAD、PUT、DELETE、TRACE、CONNECT方法。
使用该功能需要申请ohos.permission.INTERNET权限。
createHttp() 创建一个http请求。
request() 根据URL地址,发起HTTP网络请求。
requestInStream()10+ 根据URL地址,发起HTTP网络请求并返回流式响应。
destroy() 中断请求任务。
on(type: 'headersReceive') 订阅HTTP Response Header 事件。
off(type: 'headersReceive') 取消订阅HTTP Response Header 事件。
once('headersReceive')8+ 订阅HTTP Response Header 事件,但是只触发一次。
on('dataReceive')10+ 订阅HTTP流式响应数据接收事件。
off('dataReceive')10+ 取消订阅HTTP流式响应数据接收事件。
on('dataEnd')10+ 订阅HTTP流式响应数据接收完毕事件。
off('dataEnd')10+ 取消订阅HTTP流式响应数据接收完毕事件。
on('dataReceiveProgress')10+ 订阅HTTP流式响应数据接收进度事件。
off('dataReceiveProgress')10+ 取消订阅HTTP流式响应数据接收进度事件。
on('dataSendProgress')11+ 订阅HTTP网络请求数据发送进度事件。
off('dataSendProgress')11+ 取消订阅HTTP网络请求数据发送进度事件。
// 引入包名
import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';
// 每一个httpRequest对应一个HTTP请求任务,不可复用
let httpRequest = http.createHttp();
// 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
// 从API 8开始,使用on('headersReceive', Callback)替代on('headerReceive', AsyncCallback)。 8+
httpRequest.on('headersReceive', (header) => {
console.info('header: ' + JSON.stringify(header));
});
httpRequest.request(
// 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
"EXAMPLE_URL",
{
method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
// 当使用POST请求时此字段用于传递请求体内容,具体格式与服务端协商确定
extraData: "data to send",
expectDataType: http.HttpDataType.STRING, // 可选,指定返回数据的类型
usingCache: true, // 可选,默认为true
priority: 1, // 可选,默认为1
connectTimeout: 60000, // 可选,默认为60000ms
readTimeout: 60000, // 可选,默认为60000ms
usingProtocol: http.HttpProtocol.HTTP1_1, // 可选,协议类型默认值由系统自动指定
usingProxy: false, // 可选,默认不使用网络代理,自API 10开始支持该属性
caPath:'/path/to/cacert.pem', // 可选,默认使用系统预制证书,自API 10开始支持该属性
clientCert: { // 可选,默认不使用客户端证书,自API 11开始支持该属性
certPath: '/path/to/client.pem', // 默认不使用客户端证书,自API 11开始支持该属性
keyPath: '/path/to/client.key', // 若证书包含Key信息,传入空字符串,自API 11开始支持该属性
certType: http.CertType.PEM, // 可选,默认使用PEM,自API 11开始支持该属性
keyPassword: "passwordToKey" // 可选,输入key文件的密码,自API 11开始支持该属性
},
multiFormDataList: [ // 可选,仅当Header中,'content-Type'为'multipart/form-data'时生效,自API 11开始支持该属性
{
name: "Part1", // 数据名,自API 11开始支持该属性
contentType: 'text/plain', // 数据类型,自API 11开始支持该属性
data: 'Example data', // 可选,数据内容,自API 11开始支持该属性
remoteFileName: 'example.txt' // 可选,自API 11开始支持该属性
}, {
name: "Part2", // 数据名,自API 11开始支持该属性
contentType: 'text/plain', // 数据类型,自API 11开始支持该属性
// data/app/el2/100/base/com.example.myapplication/haps/entry/files/fileName.txt
filePath: `${getContext(this).filesDir}/fileName.txt`, // 可选,传入文件路径,自API 11开始支持该属性
remoteFileName: 'fileName.txt' // 可选,自API 11开始支持该属性
}
]
}, (err: BusinessError, data: http.HttpResponse) => {
if (!err) {
// data.result为HTTP响应内容,可根据业务需要进行解析
console.info('Result:' + JSON.stringify(data.result));
console.info('code:' + JSON.stringify(data.responseCode));
// data.header为HTTP响应头,可根据业务需要进行解析
console.info('header:' + JSON.stringify(data.header));
console.info('cookies:' + JSON.stringify(data.cookies)); // 8+
// 当该请求使用完毕时,调用destroy方法主动销毁
httpRequest.destroy();
} else {
console.error('error:' + JSON.stringify(err));
// 取消订阅HTTP响应头事件
httpRequest.off('headersReceive');
// 当该请求使用完毕时,调用destroy方法主动销毁
httpRequest.destroy();
}
}
);
requestInStream接口开发步骤
从@ohos.net.http.d.ts中导入http命名空间。
调用createHttp()方法,创建一个HttpRequest对象。
调用该对象的on()方法,可以根据业务需要订阅HTTP响应头事件、HTTP流式响应数据接收事件、HTTP流式响应数据接收进度事件和HTTP流式响应数据接收完毕事件。
调用该对象的requestInStream()方法,传入http请求的url地址和可选参数,发起网络请求。
按照实际业务需要,可以解析返回的响应码。
调用该对象的off()方法,取消订阅响应事件。
当该请求使用完毕时,调用destroy()方法主动销毁。
// 引入包名
import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';
// 每一个httpRequest对应一个HTTP请求任务,不可复用
let httpRequest = http.createHttp();
// 用于订阅HTTP响应头事件
httpRequest.on('headersReceive', (header: Object) => {
console.info('header: ' + JSON.stringify(header));
});
// 用于订阅HTTP流式响应数据接收事件
let res = new ArrayBuffer(0);
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
const newRes = new ArrayBuffer(res.byteLength + data.byteLength);
const resView = new Uint8Array(newRes);
resView.set(new Uint8Array(res));
resView.set(new Uint8Array(data), res.byteLength);
res = newRes;
console.info('res length: ' + res.byteLength);
});
// 用于订阅HTTP流式响应数据接收完毕事件
httpRequest.on('dataEnd', () => {
console.info('No more data in response, data receive end');
});
// 用于订阅HTTP流式响应数据接收进度事件
class Data {
receiveSize: number = 0;
totalSize: number = 0;
}
httpRequest.on('dataReceiveProgress', (data: Data) => {
console.log("dataReceiveProgress receiveSize:" + data.receiveSize + ", totalSize:" + data.totalSize);
});
let streamInfo: http.HttpRequestOptions = {
method: http.RequestMethod.POST, // 可选,默认为http.RequestMethod.GET
// 开发者根据自身业务需要添加header字段
header: {
'Content-Type': 'application/json'
},
// 当使用POST请求时此字段用于传递请求体内容,具体格式与服务端协商确定
extraData: "data to send",
expectDataType: http.HttpDataType.STRING,// 可选,指定返回数据的类型
usingCache: true, // 可选,默认为true
priority: 1, // 可选,默认为1
connectTimeout: 60000, // 可选,默认为60000ms
readTimeout: 60000, // 可选,默认为60000ms。若传输的数据较大,需要较长的时间,建议增大该参数以保证数据传输正常终止
usingProtocol: http.HttpProtocol.HTTP1_1 // 可选,协议类型默认值由系统自动指定
}
httpRequest.requestInStream(
// 填写HTTP请求的URL地址,可以带参数也可以不带参数。URL地址需要开发者自定义。请求的参数可以在extraData中指定
"EXAMPLE_URL",
streamInfo, (err: BusinessError, data: number) => {
console.error('error:' + JSON.stringify(err));
console.info('ResponseCode :' + JSON.stringify(data));
// 取消订阅HTTP响应头事件
httpRequest.off('headersReceive');
// 取消订阅HTTP流式响应数据接收事件
httpRequest.off('dataReceive');
// 取消订阅HTTP流式响应数据接收进度事件
httpRequest.off('dataReceiveProgress');
// 取消订阅HTTP流式响应数据接收完毕事件
httpRequest.off('dataEnd');
// 当该请求使用完毕时,调用destroy方法主动销毁
httpRequest.destroy();
}
);
WebSocket连接
使用WebSocket建立服务器与客户端的双向连接,需要先通过createWebSocket()方法创建WebSocket对象,然后通过connect()方法连接到服务器。
createWebSocket() 创建一个WebSocket连接。
connect() 根据URL地址,建立一个WebSocket连接。
send() 通过WebSocket连接发送数据。
close() 关闭WebSocket连接。
on(type: 'open') 订阅WebSocket的打开事件。
off(type: 'open') 取消订阅WebSocket的打开事件。
on(type: 'message') 订阅WebSocket的接收到服务器消息事件。
off(type: 'message') 取消订阅WebSocket的接收到服务器消息事件。
on(type: 'close') 订阅WebSocket的关闭事件。
off(type: 'close') 取消订阅WebSocket的关闭事件
on(type: 'error') 订阅WebSocket的Error事件。
off(type: 'error') 取消订阅WebSocket的Error事件。
导入需要的webSocket模块。
创建一个WebSocket连接,返回一个WebSocket对象。
(可选)订阅WebSocket的打开、消息接收、关闭、Error事件。
根据URL地址,发起WebSocket连接。
使用完WebSocket连接之后,主动断开连接。
import webSocket from '@ohos.net.webSocket';
import { BusinessError } from '@ohos.base';
let defaultIpAddress = "ws://";
let ws = webSocket.createWebSocket();
ws.on('open', (err: BusinessError, value: Object) => {
console.log("on open, status:" + JSON.stringify(value));
// 当收到on('open')事件时,可以通过send()方法与服务器进行通信
ws.send("Hello, server!", (err: BusinessError, value: boolean) => {
if (!err) {
console.log("Message sent successfully");
} else {
console.log("Failed to send the message. Err:" + JSON.stringify(err));
}
});
});
ws.on('message', (err: BusinessError, value: string | ArrayBuffer) => {
console.log("on message, message:" + value);
// 当收到服务器的`bye`消息时(此消息字段仅为示意,具体字段需要与服务器协商),主动断开连接
if (value === 'bye') {
ws.close((err: BusinessError, value: boolean) => {
if (!err) {
console.log("Connection closed successfully");
} else {
console.log("Failed to close the connection. Err: " + JSON.stringify(err));
}
});
}
});
ws.on('close', (err: BusinessError, value: webSocket.CloseResult) => {
console.log("on close, code is " + value.code + ", reason is " + value.reason);
});
ws.on('error', (err: BusinessError) => {
console.log("on error, error:" + JSON.stringify(err));
});
ws.connect(defaultIpAddress, (err: BusinessError, value: boolean) => {
if (!err) {
console.log("Connected successfully");
} else {
console.log("Connection failed. Err:" + JSON.stringify(err));
}
});
Socket 连接
Socket 连接主要是通过 Socket 进行数据传输,支持 TCP/UDP/Multicast/TLS 协议。