HarmonyOS实战—Page Ability简介及各种操作:页面跳转、跨设备迁移
Page Ability基本概念、生命周期、AbilitySlice导航、跨设备迁移
Harmony OS 2 Page Ability
文章目录
Page Ability
一个Page可以由一个或多个AbilitySlice构成,AbilitySlice是指应用的单个页面及其控制逻辑的总和。
当一个Page由多个AbilitySlice共同构成时,这些AbilitySlice页面提供的业务能力应具有高度相关性。
Page Ability基本概念
AbilitySlice路由配置
虽然一个Page可以包含多个AbilitySlice,但是Page进入前台时界面默认只展示一个AbilitySlice。默认展示的AbilitySlice是通过setMainRoute()方法来指定的。如果需要更改默认展示的AbilitySlice,可以通过addActionRoute()方法为此AbilitySlice配置一条路由规则。此时,当其他Page实例期望导航到此AbilitySlice时,可以在Intent中指定Action,详见下文的不同Page间导航。
示例
src/main/java/com/example/harmonyos01java/slice/MainAbilitySlice.java
public class MainAbility extends Ability {
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setMainRoute(MainAbilitySlice.class.getName());
addActionRoute("action.second", SecondAbilitySlice.class.getName());
}
}
src/main/config.json
addActionRoute()方法中使用的动作命名,需要在应用配置文件(config.json)中注册
{
"module": {
"abilities": [
{
"skills":[
{
"actions":[
"action.system.home",
"action.second"
]
}
],
"name": "com.example.harmonyos01java.MainAbility",
"page": "page"
...
}
]
...
}
...
}
Page Ability生命周期
系统管理或用户操作等行为均会引起Page实例在其生命周期的不同状态之间进行转换。Ability类提供的回调机制能够让Page及时感知外界变化,从而正确地应对状态变化(比如释放资源),这有助于提升应用的性能和稳健性。
这一部分我不进行太多的描述
Page生命周期回调
这里我们借用一下官方文档的一张图

非常直观的表现了Page的若干个状态
- INITIAL
- onStart前
- onStop后
- INACTIVE
- onStart后
- onInactice后(Page失去焦点执行:即 返回键、或者 另一个Ability到前台 时)
- onForeground后 (Page处于BACKGROUND状态重新回到前台时)
- ACTIVE
- onActive后(Page来到前台 时)
- BACKGROUND
- onBackground后 (Page不再对用户可见 时)
AbilitySlice生命周期
AbilitySlice作为Page的组成单元,其生命周期是依托于其所属Page生命周期的,AbilitySlice生命周期回调与Page的相应回调类似,因此不再赘述。
AbilitySlice和Page具有相同的生命周期状态和同名的回调,当Page生命周期发生变化时,它的AbilitySlice也会发生相同的生命周期变化。此外,AbilitySlice还具有独立于Page的生命周期变化,这发生在同一Page中的AbilitySlice之间导航时,此时Page的生命周期状态不会改变。
Page与AbilitySlice生命周期关联
Page和AbilitySlice的生命周期很多时候都是一致的,值得注意的是他们都需要编写他们的onStart方法
- Page:开发者必须重写onStart回调,并在此配置默认展示的AbilitySlice
- AbilitySlice:开发者必须重写AbilitySlice的onStart()回调,并在此方法中通过setUIContent()方法设置页面
当AbilitySlice处于前台且具有焦点时,其生命周期状态随着所属Page的生命周期状态的变化而变化。当一个Page拥有多个AbilitySlice时,例如:MyAbility下有FooAbilitySlice和BarAbilitySlice,当前FooAbilitySlice处于前台并获得焦点,并即将导航到BarAbilitySlice,在此期间的生命周期状态变化顺序为:
- FooAbilitySlice(原前台AbilitySlice)从ACTIVE状态变为INACTIVE状态。
- BarAbilitySlice(原非前台AbilitySlice)则从INITIAL状态首先变为INACTIVE状态,然后变为ACTIVE状态(假定此前BarAbilitySlice未曾启动)。
- FooAbilitySlice(原前台AbilitySlice)从INACTIVE状态变为BACKGROUND状态。
对应两个slice的生命周期方法回调顺序为:
FooAbilitySlice.onInactive() --> BarAbilitySlice.onStart() --> BarAbilitySlice.onActive() --> FooAbilitySlice.onBackground()
在整个流程中,MyAbility始终处于ACTIVE状态。但是,当Page被系统销毁时,其所有已实例化的AbilitySlice将联动销毁,而不仅是处于前台的AbilitySlice。
AbilitySlice间导航
同一Page内导航
当发起导航的AbilitySlice和导航目标的AbilitySlice处于同一个Page时,您可以通过present()方法实现导航。
这个也是我们上一个博客中有用到的
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
Button button = (Button) findComponentById(ResourceTable.Id_button);
// 点击按钮跳转至第二个页面
button.setClickedListener(listener -> present(new SecondAbilitySlice(), new Intent()));
}
如果开发者希望在用户从导航目标AbilitySlice返回时,能够获得其返回结果,则应当使用presentForResult()实现导航。用户从导航目标AbilitySlice返回时,系统将回调onResult()来接收和处理返回结果,开发者需要重写该方法。返回结果由导航目标AbilitySlice在其生命周期内通过**setResult()**进行设置。
@Override
protected void onStart(Intent intent) {
...
Button button = ...;
button.setClickedListener(listener -> presentForResult(new TargetSlice(), new Intent(), 0));
...
}
@Override
protected void onResult(int requestCode, Intent resultIntent) {
if (requestCode == 0) {
// Process resultIntent here.
}
}
在我的理解下requestCode是用来辨别不同的接受请求的AbilitySlice的,
值得注意的是,系统为每个Page维护了一个AbilitySlice实例的栈,每个进入前台的AbilitySlice实例均会入栈。当开发者在调用present()或presentForResult()时指定的AbilitySlice实例已经在栈中存在时,则栈中位于此实例之上的AbilitySlice均会出栈并终止其生命周期。
不同Page内导航
AbilitySlice作为Page的内部单元,以Action的形式对外暴露,因此可以通过配置Intent的Action导航到目标AbilitySlice。Page间的导航可以使用**startAbility()或startAbilityForResult()**方法,获得返回结果的回调为onAbilityResult()。在Ability中调用setResult()可以设置返回结果。
这里我们尝试着制作一个demo
-
新建一个新java项目(第二个)
-
在
src/main/java/xxx.xxx/slice下新建一个SecondAbilitySlice类,使其继承于AbilitySlice类 -
在
src/main/java/xxx.xxx下新建一个SecondAbility类,使其继承于Ability类 -
在
src/main/resources/base/layout新建一个ability_second.xml的Layout Resource File -
在
src/main/resources/base/graphic新建一个background_button.xml的Graphic Resourse File -
修改
src/main/resources/base/element/string.json、src/main/resources/base/en.element/string.json为{ "string": [ { "name": "entry_MainAbility", "value": "entry_MainAbility" }, { "name": "mainability_description", "value": "Java_Empty Ability" }, { "name": "entry_SecondAbility", "value": "entry_SecondAbility" }, { "name": "secondability_description", "value": "Java_Empty Ability" }, { "name": "string_tip_to_jump", "value": "Press the button to jump" }, { "name": "string_tip_jump_success", "value": "Jump success" } ] }修改
src/main/resources/base/zh.element/string.json为{ "string": [ { "name": "entry_MainAbility", "value": "entry_MainAbility" }, { "name": "mainability_description", "value": "Java_Empty Ability" }, { "name": "entry_SecondAbility", "value": "entry_SecondAbility" }, { "name": "secondability_description", "value": "Java_Empty Ability" }, { "name": "string_tip_to_jump", "value": "按下按钮以跳转" }, { "name": "string_tip_jump_success", "value": "跳转成功" } ] } -
我们需要在config.json中添加HAP包的配置信息
{ "app": { ... }, "deviceConfig": {}, "module": { ... "abilities": [ { "skills": [ { "entities": [ "entity.system.home" ], "actions": [ "action.system.home" ] } ], "orientation": "unspecified", "name": "com.example.harmonyos02pagenavigator.MainAbility", "icon": "$media:icon", "description": "$string:mainability_description", "label": "$string:entry_MainAbility", "type": "page", "launchType": "standard" }, { "skills": [ { "entities": [ "entity.system.home" ], "actions": [ "action.system.home", "action.second" ] } ], "orientation": "unspecified", "name": "com.example.harmonyos02pagenavigator.SecondAbility", "icon": "$media:icon", "description": "$string:secondability_description", "label": "$string:entry_SecondAbility", "type": "page", "launchType": "standard" } ] } } -
添加完毕后我们开始编写前面新建的几个空类
SecondAbilitySlice
package com.example.harmonyos02pagenavigator.slice; import com.example.harmonyos02pagenavigator.ResourceTable; import ohos.aafwk.ability.AbilitySlice; import ohos.aafwk.content.Intent; public class SecondAbilitySlice extends AbilitySlice { @Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_ability_second); } }SecondAbility
package com.example.harmonyos02pagenavigator; import com.example.harmonyos02pagenavigator.slice.MainAbilitySlice; import com.example.harmonyos02pagenavigator.slice.SecondAbilitySlice; import ohos.aafwk.ability.Ability; import ohos.aafwk.content.Intent; public class SecondAbility extends Ability { @Override protected void onStart(Intent intent) { super.onStart(intent); super.setMainRoute(SecondAbilitySlice.class.getName()); addActionRoute("action。second",SecondAbilitySlice.class.getName()); } } -
开始编写前面新建的几个新xml
ability_second.xml
<?xml version="1.0" encoding="utf-8"?> <DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_parent" ohos:width="match_parent" ohos:orientation="vertical"> <Text ohos:id="$+id:string_jump_success" ohos:width="match_content" ohos:height="match_content" ohos:text="$string:string_tip_jump_success" ohos:text_color="#000000" ohos:text_size="32fp" ohos:center_in_parent="true"/> </DependentLayout>background_button.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="rectangle"> <corners ohos:radius="100"/> <solid ohos:color="#007DFF"/> </shape> -
修改已经存在的layout ability_main.xml
<?xml version="1.0" encoding="utf-8"?> <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_parent" ohos:width="match_parent" ohos:alignment="center" ohos:orientation="vertical"> <Button ohos:id="$+id:button_jump" ohos:height="match_content" ohos:width="match_content" ohos:text_color="#FFFFFF" ohos:text="$string:string_tip_to_jump" ohos:padding="8vp" ohos:left_padding="50vp" ohos:right_padding="50vp" ohos:background_element="$graphic:background_button" ohos:layout_alignment="horizontal_center" ohos:text_size="25vp" /> </DirectionalLayout> -
修改已经存在的类
src/main/java/xxx.xxx/slice/MainAbilitySlice
package com.example.harmonyos02pagenavigator.slice; import com.example.harmonyos02pagenavigator.ResourceTable; import ohos.aafwk.ability.AbilitySlice; import ohos.aafwk.content.Intent; import ohos.aafwk.content.Operation; import ohos.agp.components.Button; public class MainAbilitySlice extends AbilitySlice { @Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_ability_main); Button button = (Button) findComponentById(ResourceTable.Id_button_jump); Intent secondIntent = new Intent(); Operation operation = new Intent.OperationBuilder().withAction("action.second").build(); secondIntent.setOperation(operation); button.setClickedListener(listener -> startAbility(secondIntent)); } @Override public void onActive() { super.onActive(); } @Override public void onForeground(Intent intent) { super.onForeground(intent); } } -
运行测试


跨设备迁移
跨设备迁移(下文简称“迁移”)支持将Page在同一用户的不同设备间迁移,以便支持用户无缝切换的诉求。以Page从设备A迁移到设备B为例,迁移动作主要步骤如下:
- 设备A上的Page请求迁移。
- HarmonyOS处理迁移任务,并回调设备A上Page的保存数据方法,用于保存迁移必须的数据。
- HarmonyOS在设备B上启动同一个Page,并回调其恢复数据方法。

看上去十分复杂的东西,官方文档却不是很长,官方应该是觉得这个部分不会特别难,所以我决定做一个demo来熟悉这个技术
-
新建Java Phone Application项目
-
修改string.json
src/main/resources/base/element/string.json、src/main/resources/base/zh.element/string.json{ "string": [ { "name": "entry_MainAbility", "value": "跨设备迁移" }, { "name": "mainability_description", "value": "Java_Empty Ability" }, { "name": "mainability_tip", "value": "输入内容以跨设备迁移" }, { "name": "mainability_button", "value": "跨设备迁移" }, { "name": "mainability_success", "value": "跨设备成功" } ] }src/main/resources/base/element/en.string.json{ "string": [ { "name": "entry_MainAbility", "value": "Cross-equipment migration" }, { "name": "mainability_description", "value": "Java_Empty Ability" }, { "name": "mainability_tip", "value": "Enter content migrate across equipment" }, { "name": "mainability_button", "value": "cross-equipment migration" }, { "name": "mainability_success", "value": "success" } ] } -
修改config.json
-
添加跨设备迁移权限
{ ... "module": { ... "reqPermissions": [ { "name": "ohos.permission.DISTRIBUTED_DATASYNC" } ] } }
-
-
修改ability_background.xml (ui修改)
<DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="match_parent" ohos:width="match_parent" ohos:alignment="center" ohos:orientation="vertical"> <TextField ohos:id="$+id:message_textfield" ohos:height="match_content" ohos:width="300vp" ohos:hint="$string:mainability_tip" ohos:left_margin="24vp" ohos:padding="5vp" ohos:background_element="$graphic:background_text_field" ohos:text_alignment="center" ohos:right_margin="24vp" ohos:text_size="16vp"/> <Button ohos:id="$+id:button_jump" ohos:height="match_content" ohos:width="match_content" ohos:text_color="#FFF" ohos:text="$string:mainability_button" ohos:top_margin="30vp" ohos:padding="8vp" ohos:left_padding="50vp" ohos:right_padding="50vp" ohos:background_element="$graphic:background_button" ohos:layout_alignment="horizontal_center" ohos:text_size="15vp" /> </DirectionalLayout> -
新建两个Graphic Resource File
background_text_field.xml
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="rectangle"> <corners ohos:radius="200"/> <solid ohos:color="#0d000000"/> <stroke ohos:color="#33000000" ohos:width="0.4vp"/> </shape>background_button.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:shape="rectangle"> <corners ohos:radius="100"/> <solid ohos:color="#007DFF"/> </shape> -
使MainAbility实现IAbilityContinuation
几乎没有什么实现
package com.example.harmonyos02cemigration; import com.example.harmonyos02cemigration.slice.MainAbilitySlice; import ohos.aafwk.ability.Ability; import ohos.aafwk.ability.IAbilityConnection; import ohos.aafwk.ability.IAbilityContinuation; import ohos.aafwk.content.Intent; import ohos.aafwk.content.IntentParams; import ohos.bundle.IBundleManager; import ohos.security.SystemPermission; public class MainAbility extends Ability implements IAbilityContinuation { @Override public void onStart(Intent intent) { super.onStart(intent); super.setMainRoute(MainAbilitySlice.class.getName()); requestPermissions(); } /** * */ private void requestPermissions() { if (verifySelfPermission(SystemPermission.DISTRIBUTED_DATASYNC) != IBundleManager.PERMISSION_GRANTED) { requestPermissionsFromUser(new String[] {SystemPermission.DISTRIBUTED_DATASYNC}, 0); } } /** * Page请求迁移后,系统首先回调此方法,开发者可以在此回调中决策当前是否可以执行迁移, * 比如,弹框让用户确认是否开始迁移。 * @return */ @Override public boolean onStartContinuation() { //return false; return true; } /** * 如果onStartContinuation()返回true,则系统回调此方法, * 开发者在此回调中保存必须传递到另外设备上以便恢复Page状态的数据。 * @param intentParams 用于保存数据的一个结构类似于map的数据结构 * @return */ @Override public boolean onSaveData(IntentParams intentParams) { //return false; return true; } /** * 源侧设备上Page完成保存数据后,系统在目标侧设备上回调此方法, * 开发者在此回调中接受用于恢复Page状态的数据。注意,在目标侧设备上的Page会重新启动其生命周期, * 无论其启动模式如何配置。且系统回调此方法的时机在onStart()之前。 * @param intentParams 用于恢复数据的一个结构类似于map的数据结构 * @return */ @Override public boolean onRestoreData(IntentParams intentParams) { //return false; return true; } /** * 目标侧设备上恢复数据一旦完成,系统就会在源侧设备上回调Page的此方法, * 以便通知应用迁移流程已结束。开发者可以在此检查迁移结果是否成功, * 并在此处理迁移结束的动作,例如,应用可以在迁移完成后终止自身生命周期。 * @param i */ @Override public void onCompleteContinuation(int i) { terminateAbility(); } /** * 如果开发者使用continueAbilityReversibly()而不是continueAbility(), * 则此后可以在源侧设备上使用reverseContinueAbility()进行回迁。这种场景下, * 相当于同一个Page(的两个实例)同时在两个设备上运行,迁移完成后,如果目标侧设备上Page因任何原因终止, * 则源侧Page通过此回调接收终止通知。 */ @Override public void onRemoteTerminated() { terminateAbility(); } } -
使MainAbilitySlice实现IAbilityContinuation
由于顺序是
-
a方 b方安装好app
-
a方打开app
-
a方app申请跨设备迁跃 答应
-
a方输入数据
-
a方触发开始跨设备迁跃
- onStartContinuation 开始迁跃
- onSaveData 保存数据 将输入的数据保存于传入的参数IntentParams(map)中
-
b方自动打开app
-
b方开始跨设备迁跃
- onRestoreData 恢复数据 通过传入的参数IntentParams(map)来恢复
- onStart 开始b方app的Ability的初始化
-
a方继续跨设备迁跃
- onCompleteContinuation 结束迁跃
在本项目中,我们设计a方开始跨设备迁跃完毕后直接关闭项目(本项目在Ability中实现了,而非在AbilitySlice中实现),结束其生命周期,所以我们将内容重新显示位于b方app的Ability运行onStart时实现,用到b方 onRestoreData 获取到的参数,放入输入框中,在开始跨设备时保存数据
package com.example.harmonyos02cemigration.slice; import com.example.harmonyos02cemigration.ResourceTable; import ohos.aafwk.ability.AbilitySlice; import ohos.aafwk.ability.IAbilityContinuation; import ohos.aafwk.content.Intent; import ohos.aafwk.content.IntentParams; import ohos.agp.components.Component; import ohos.agp.components.TextField; import ohos.agp.window.dialog.ToastDialog; import ohos.hiviewdfx.HiLog; public class MainAbilitySlice extends AbilitySlice implements IAbilityContinuation { private static final String INPUT_CONTENT = "input content"; private TextField textField ; private String message; private boolean isContinued; @Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_ability_main); findComponentById(ResourceTable.Id_button_jump).setClickedListener(this::ceMigrate); textField = (TextField) findComponentById(ResourceTable.Id_message_textfield); if (isContinued && message != null) { textField.setText(message); } } private void ceMigrate(Component component){ String messageSend = textField.getText(); if (messageSend.isEmpty()) { new ToastDialog(this).setText("输入信息不为空").show(); return; } try { continueAbility(); } catch (IllegalStateException illegalStateException) { new ToastDialog(this).setText("发送失败").show(); return; } } @Override public void onActive() { super.onActive(); } @Override public void onForeground(Intent intent) { super.onForeground(intent); } @Override public boolean onStartContinuation() { return true; } @Override public boolean onSaveData(IntentParams intentParams) { intentParams.setParam(INPUT_CONTENT,textField.getText()); return true; } @Override public boolean onRestoreData(IntentParams intentParams) { if (intentParams.getParam(INPUT_CONTENT) instanceof String) { message = (String) intentParams.getParam(INPUT_CONTENT); isContinued = true; } return true; } @Override public void onCompleteContinuation(int i) { } @Override public void onRemoteTerminated() { } } -
-
运行测试
- 两部手机都安装好这个app,打开a方的该app,询问是否允许使用多设备协同时点击始终允许

- 输入一些数据在TextField中并点击跨设备迁移

-
b方手机成功打开,并且显示出a方的数据

-
很快a方手机自动终止该app生命周期
更多推荐



所有评论(0)