JS FA 调用 PA (展示本地相册图片) 原创 精华
一、效果展示
::: hljs-center
:::
二、项目介绍
$\qquad$本项目以 ArkUI(JS)开发界面,利用 JS FA 调用 JAVA PA 的接口功能,通过 JAVA 端拉取本地相册,选择图片后返回,并将图片复制到 JS IMAGE 组件可访问的目录下以及将图片路径返回至 JS 端。最终实现 JS 界面展示本地相册图片的功能。
$\qquad$项目提供两种方案,Internal Ability 以及 LocalParticleAbility ,两种方案思路基本一致,最终效果也都一样。
$\qquad$通过这个项目来和大家分享我目前使用 JS FA 调用 JAVA PA 功能的经验以及 JS 展示相册照片的思路,同时也希望各位能指出我代码中的不足。项目中部分代码来源于各教程。
三、代码结构展示
::: hljs-center
:::
- Java 端
- getPhotoInternalAbility:Internal Ability 方式与 JS 端交互
- getPhotoLocalParticleAbility:LocalParticleAbility 方式与 JS 端交互
- Utils:工具类,保存照片
- MainAbility:主 Ability
- Js 端:基本展示页面(index.hml、index.css、index.js)
四、关键问题
4.1 权限问题
由于要访问用户媒体文件,需要申请媒体文件相关权限。
::: hljs-center
权限 | 说明 |
---|---|
ohos.permission.MEDIA_LOCATION | 允许应用访问用户媒体文件中的地理位置信息 |
ohos.permission.READ_MEDIA | 允许应用读取用户外部存储中的媒体文件信息 |
ohos.permission.WRITE_MEDIA | 允许应用读写用户外部存储中的媒体文件信息 |
:::
config.json 权限配置:
"reqPermissions": [
{
"name": "ohos.permission.READ_MEDIA"
},
{
"name": "ohos.permission.WRITE_MEDIA"
},
{
"name": "ohos.permission.MEDIA_LOCATION"
}
]
可选择添加动态申请权限,添加则有权限提示。
// 动态申请权限
private void requestPermissions() {
String[] permissions =
{
SystemPermission.MEDIA_LOCATION,
SystemPermission.WRITE_MEDIA,
SystemPermission.READ_MEDIA,
};
List<String> permissionFiltered =
Arrays.stream(permissions)
.filter(permission -> verifySelfPermission(permission)
!= IBundleManager.PERMISSION_GRANTED).collect(Collectors.toList());
requestPermissionsFromUser(permissionFiltered.toArray(new String[permissionFiltered.size()]), 0);
}
4.2 页面跳转问题
$\qquad$与正常跳转其它 Ability 页面相同,我们需要通过 Intent 来指定启动目标。
$\qquad$此处我们根据需要选择图片的能力来构建 intent 并传入 startAbility,以达到跳转本地相册的目的。
Intent intent = new Intent();
Operation opt=new Intent.OperationBuilder()
.withAction(IntentConstants.ACTION_CHOOSE)
.build();
intent.setOperation(opt);
intent.addFlags(Intent.FLAG_NOT_OHOS_COMPONENT);
intent.setType("image/*");
abilityContext.startAbility(intent,code); // 此处 code 随意
$\qquad$关键点在于 abilityContext 的获取,很容易我们可以得知 startAbility 是抽象类 AbilityContext 中的方法,MainAbility 继承自 AceAbility ,而 AceAbility 的父类 Ability 则是 AbilityContext 的具体实现类。
$\qquad$我们操作的主线程(UI 线程)上下文应当是由 MainAbility 管理,此时我们需要从一个 Ability 跳转至另一个 Ability,很明显 abilityContext 的获取与 MainAbility 有关。
$\qquad$通过浏览官方文档不难发现,Internal Ability 以及 LocalParticleAbility 的实现均与 MainAbility 有着密切关系,这两种方式都存在注册与注销的方法,所需参数不正是 MainAbility ?
4.2.1 Internal Ability 方式
getPhotoInternalAbility
private AbilityContext abilityContext;
/**
* Internal ability 注册接口。
*/
public static void register(AbilityContext abilityContext) {
instance = new getPhotoInternalAbility();
instance.onRegister(abilityContext);
}
private void onRegister(AbilityContext abilityContext) {
this.abilityContext = abilityContext;
...
}
MainAbility
@Override
public void onStart(Intent intent) {
getPhotoInternalAbility.register(this);
super.onStart(intent);
}
4.2.2 LocalParticleAbility
getPhotoLocalParticleAbility
// 重写注册方法即可
private AbilityContext abilityContext;
@Override
public void register(AceAbility ability) {
abilityContext = ability;
LocalParticleAbility.super.register(ability);
}
MainAbility
@Override
public void onStart(Intent intent) {
super.onStart(intent);
getPhotoLocalParticleAbility.getInstance().register(this);
}
4.2.3 JS 端
JS 侧只需简单调用 JAVA 有关跳转页面的代码即可。
例:
makeAction(bundleName, abilityName, code, abilityType, data) {
const action = {};
action.bundleName = bundleName;
action.abilityName = abilityName;
action.messageCode = code;
action.abilityType = abilityType;
action.data = data;
action.syncOption = 0;
return action;
},
async testGetPhotoInternalAbility() {
const action = this.makeAction('cn.crcrc.arkui_example',
'cn.crcrc.arkui_example.ability.getPhotoInternalAbility', 1001, 1, {});
const result = await FeatureAbility.callAbility(action);
console.info(result)
if(result != null){
this.avatarURL = result
}
},
testGetPhotoLocalParticleAbility(){
this.javaInterface = createLocalParticleAbility('cn.crcrc.arkui_example.ability.getPhotoLocalParticleAbility');
this.javaInterface.getPhotoUri().then(result => {
console.info(result);
if(result != null){
this.avatarURL = result
}
}, error => {
console.error('testGetPhotoLocalParticleAbility error');
});
}
4.3 返回值问题
$\qquad$AbilityContext.startAbility 的结果是可以通过 Ability 的回调方法 onAbilityResult 来获得。通过以上分析,显而易见,由于是通过 MainAbility 来 startAbility 的,那么 onAbilityResult 自然是需在 MainAbility 重写以获得结果。
$\qquad$此时便要解决将相册返回值传递给 Internal Ability 或 LocalParticleAbility,进而返回所得图片地址给 JS 端的问题。
$\qquad$此处我选择通过有序公共事件来传播消息。基本思路即在 MainAbility 的 onAbilityResult 中发布事件,附带相册图片 Uri 值;在 Internal Ability 或 LocalParticleAbility 中订阅事件,得到 Uri 值后通过 Utils 类将图片经自定义处理后存入 JS IMAGE 组件可访问的地址中。
MainAbility
/*
* 发布有序的公共事件
*/
private void orderlyEventPublish(String data) {
HiLog.info(LABEL, "发布有序公共事件开始");
//1.构建一个Intent对象,包含了自定义的事件的标识符
Intent intent = new Intent();
Operation oper = new Intent.OperationBuilder().withAction("cn.crcrc.arkui_example.event") //就是自定义的公共事件的标识
.build();
intent.setOperation(oper);
//2.构建CommonEventData对象
CommonEventData commonEventData = new CommonEventData(intent);
//仅仅只有有序的公共事件,才能携带的两个专用属性,可选的参数,不是必须的
commonEventData.setData(data);
commonEventData.setCode(1001);
//配置公共事件的对应权限
CommonEventPublishInfo publishInfo = new CommonEventPublishInfo();
publishInfo.setOrdered(true);
//3.核心的发布事件的动作,发布的公共事件,有序的公共事件
try {
CommonEventManager.publishCommonEvent(commonEventData, publishInfo);
HiLog.info(LABEL, "发布有序公共事件完成");
} catch (RemoteException e) {
e.printStackTrace();
}
}
/**
* 跳转回调
*/
@Override
protected void onAbilityResult(int requestCode, int resultCode, Intent resultData) {
super.onAbilityResult(requestCode, resultCode, resultData);
try {
System.out.println("APP LOG resultData:" + resultData.getUriString());
// 通过发布有序公共事件传递信息
orderlyEventPublish(resultData.getUriString());
} catch (Exception e) {
orderlyEventPublish("fail");
e.printStackTrace();
}
}
Internal Ability 或 LocalParticleAbility 中订阅事件,例:
/**
* 订阅事件
*/
private void subscribeCommonEvent() {
if(!isSubscribe){
HiLog.info(LABEL,"订阅开始:");
//1.构建MatchingSkills对象
MatchingSkills matchingSkills = new MatchingSkills();
matchingSkills.addEvent("cn.crcrc.arkui_example.event"); //订阅自定义的公共事件
//2.构建订阅信息对象
CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
//3.构建订阅者对象
subscriber = new CommonEventSubscriber(subscribeInfo){
// 回调方法
@Override
public void onReceiveEvent(CommonEventData commonEventData) {
HiLog.info(LABEL,"已接收公共事件");
Boolean setImaDataResult = false;
if(commonEventData.getData() != null)
{
System.out.println("接收到数据:" + commonEventData.getData());
if(!commonEventData.getData().equals("fail"))
{
Uri uri = Uri.parse(commonEventData.getData());
long l = System.currentTimeMillis();
String fileName = String.valueOf(l);
setImaDataResult = utils.setImaData(uri,fileName);
photoUriString = "internal://app/" + String.valueOf(l) + ".jpg";
HiLog.info(LABEL,"js 访问图片路径:" + photoUriString);
}
latch.countDown();
}else {
HiLog.info(LABEL,"已接收公共事件,但数据为空");
System.out.println("APP LOG 已接收公共事件,但数据为空");
}
// 目前 onReceiveEvent 只能在 ui 主线程上执行
// 耗时任务派发到子线程异步执行,保证不阻塞 ui 线程
final AsyncCommonEventResult result = goAsyncCommonEvent();
Boolean finalSetImaDataResult = setImaDataResult;
handler.postTask(new Runnable() {
@Override
public void run() {
if(finalSetImaDataResult){
HiLog.info(LABEL,"进行数据库操作");
// 存入数据库
}
HiLog.info(LABEL,"数据库操作完成");
result.finishCommonEvent();//结束事件
}
});
}
};
//4.订阅公共事件的核心动作
try {
CommonEventManager.subscribeCommonEvent(subscriber);
isSubscribe = true;
} catch (RemoteException e) {
e.printStackTrace();
}
}else{
HiLog.info(LABEL,"公共事件不能重复订阅");
}
}
可将订阅以及取消订阅动作分别放入注册与注销动作中,以 LocalParticleAbility 为例
@Override
public void register(AceAbility ability) {
abilityContext = ability;
LocalParticleAbility.super.register(ability);
utils = new Utils(abilityContext); // 得到实例
// 注册公共事件
subscribeCommonEvent();
}
@Override
public void deregister(AceAbility ability) {
abilityContext = null;
LocalParticleAbility.super.deregister(ability);
// 取消订阅
try{
CommonEventManager.unsubscribeCommonEvent(subscriber);
}catch(RemoteException e) {
HiLog.error(LABEL,"Exception occurred during unsubscribeCommonEvent invocation.");
}
}
注意:
- JS 调用的 JAVA 端方法会在订阅事件中获取到图片地址之前返回,导致 JS 端获得空值。故此处我采取 CountDownLatch 方法以同步传递数据。
- 初始化订阅事件时,其中的回调方法会执行一次,故其中要加判断条件,防止 latch.countDown() 出错。
- 由于可能不选择图片直接返回,导致 onAbilityResult 的 resultData 为空,故 resultData.getUriString 应 catch 异常。
$\qquad$其中解决 JAVA 端提前返回值应该有其它解决方法,比如用 LocalParticleAbility 的 CallBack 方法开线程等图片返回值,并将其异步返回。或者 JAVA 端先将其存储下来, JS 端调 JAVA PA 相关函数查询对应键值即可。
五、调试日志
::: hljs-center
:::
六、对比与总结
$\qquad$编写代码时,很容易体会到 LocalParticleAbility 方式较 Internal Ability 方便,尤其是 JS 端与 JAVA 端参数传递上。
$\qquad$LocalParticleAbility 方式就好像 JS 端直接调用其中方法实现功能一样,而 Internal Ability 需要在 onRemoteRequest 根据传递的 code 判断执行哪个方法。
$\qquad$此项目再结合下 JAVA 或 JS 端数据库功能,即可实现如下效果:
::: hljs-center
:::
项目中有关图片转存的函数实现,查看上传的相应文件即可。
其实 JS 端亦可拉起图片选择能力,但需要返回值的对应方法要 API7 才支持。
$$ $$
var str = {
"want": {
"type": "image/*",
"action": "android.intent.action.GET_CONTENT",
"flags": wantConstant.Flags.FLAG_NOT_OHOS_COMPONENT
},
};
featureAbility.startAbilityForResult(str, (error, data) => {
if (error) {
console.error('Operation failed. Cause: ' + error);
return;
}
console.info('Operation succeeded: ' + data);
});
若您对此项目的功能实现有更好的思路或方法,望不吝赐教!
楼主表情包收的挺有意思,代码也写的好