手把手教HDC2021趣味闯关赛平行视界服务流转 原创 精华
一, 前言
上一篇 轻松玩转HDC2021趣味闯关赛平行视界服务流转 帖子里是基于Codelabs里Java电影卡片,平行视界Sample集成的,此帖从空白项目开始,一步步教你如何在项目里,学到列表显示,平行视界,服务卡片,服务流转各知识点。下面大慨说一下项目的开发流程:
- 实现List显示, 点击某项跳转到RightAbility界面
- 添加JS服务卡片, 点击跳转到List列表
- 平行视界, 点击List列表, 左边显示列表,右边显示详情图片
- 服务流转, 在详情,点击连接图标, 选择流转设备; 再点击流转图标==
二, 实现效果
B站视频:https://www.bilibili.com/video/BV1tL4y1q7A6/
三, 工程搭建
打开DevEco Studio 3.0.0.600开发工具, 点击菜单File -> New -> New Project, 弹出以下框,按截图选择创建:
下一步后的界面以下,请按照标注操作。
空模板项目到此,就创建完成了,下面我们来按照上面说的步骤一步一步来完成。
四, 工程讲解
1. 实现List显示, 点击某项跳转到RightAbility界面
首先把List列表显示出来,下面是List列表的XML布局文件,由于List列表项是数据驱动的,这里只提供List容器,并给予id就可以。
<?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">
<ListContainer
ohos:id="$+id:list"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:orientation="vertical"/>
</DirectionalLayout>
从上面效果图可以看出,列表项左边显示图片,右边显示文字,下面有一条分割线,XML布局文件以下:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_content"
ohos:width="match_parent"
ohos:orientation="vertical">
<DirectionalLayout
ohos:height="100vp"
ohos:width="match_parent"
ohos:padding="10vp"
ohos:orientation="horizontal">
<Image
ohos:id="$+id:img"
ohos:height="match_parent"
ohos:scale_mode="zoom_center"
ohos:width="120vp"></Image>
<Text
ohos:id="$+id:title"
ohos:height="match_parent"
ohos:width="match_content"
ohos:padding="5vp"
ohos:text_size="16fp"
ohos:left_margin="10vp"/>
</DirectionalLayout>
<Component
ohos:height="1vp"
ohos:width="match_parent"
ohos:background_element="#CCCCCC"/>
</DirectionalLayout>
List列表的页面布局就说完了,下面来说一下Java代码:先介绍一下列表项的实体类,也就两个属性,一个是图片,一个是标题
import ohos.media.image.PixelMap;
public class Item {
// 显示图片
private PixelMap img;
// 显示每项标题
private String title;
public PixelMap getImg() {
return img;
}
public void setImg(PixelMap img) {
this.img = img;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
下面介绍一下List列表的核心部分就是适配器类的实现:大部分代码都有注释,适配器类首先要继承BaseItemProvider 基础类,然后实现里面相应的方法。
/**
* 列表适配器类
*/
public class ListAdapter extends BaseItemProvider {
// 上下文
private static Context context;
// 数据项列表
private List<Item> items;
// 自定义事件处理
private MyEventHandle myEventHandle;
/**
* 构造方法
* @param items
* @param context
*/
public ListAdapter(List<Item> items, Context context) {
this.items = items == null ? new ArrayList() : items;
this.context = context;
}
/**
* 获取列表有多少项
* @return
*/
@Override
public int getCount() {
return items == null ? 0 : items.size();
}
/**
* 获取当前项数据
* @param i
* @return
*/
@Override
public Item getItem(int i) {
return this.items.get(i);
}
/**
* 获取当前Id
* @param i
* @return
*/
@Override
public long getItemId(int i) {
return i;
}
/**
* 获取组件内容
* @param i
* @param component
* @param componentContainer
* @return
*/
@Override
public Component getComponent(int i, Component component, ComponentContainer componentContainer) {
ViewHolder viewHolder = null;
Component cmp = component;
if (cmp == null) {
cmp = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_item, null, false);
// 初始化项布局
viewHolder = new ViewHolder();
// 获取图片组件
viewHolder.img = (Image)cmp.findComponentById(ResourceTable.Id_img);
// 获取标题组件
viewHolder.title = (Text)cmp.findComponentById(ResourceTable.Id_title);
// 缓存起来
cmp.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) cmp.getTag();
}
viewHolder.img.setPixelMap(items.get(i).getImg());
viewHolder.title.setText(items.get(i).getTitle());
viewHolder.img.setClickedListener(listener -> itemClick(i));
viewHolder.title.setClickedListener(listener -> itemClick(i));
return cmp;
}
public void replace(Collection<Item> listConstructor) {
if (listConstructor == null) {
return;
}
this.items = null;
// 重新初始化listContainer中的数据
this.items = new ArrayList<>(0);
// 将重新得到的项数据放到listContainer中
this.items.addAll(listConstructor);
// 刷新listContainer,调用getComponent方法重新设置页面元素布局
notifyDataChanged();
}
/**
* 内部类,封装列表项组件
*/
private static class ViewHolder {
// 显示图片
Image img;
// 显示标题
Text title;
}
/**
* 点击图片或标题事件
* @param index
*/
private void itemClick(int index) {
// 初始化处理程序
initHandler();
// 获取内部事件
InnerEvent event = InnerEvent.get(1, 0, index);
myEventHandle.sendEvent(event);
}
/**
* 初始化处理程序
*/
private void initHandler() {
EventRunner runner = EventRunner.getMainEventRunner();
if (runner == null) {
return;
}
myEventHandle = new MyEventHandle(runner);
}
/**
* 自定义处理事件
*/
public static class MyEventHandle extends EventHandler {
MyEventHandle(EventRunner runner) throws IllegalArgumentException {
super(runner);
}
@Override
protected void processEvent(InnerEvent event) {
super.processEvent(event);
int eventId = event.eventId;
int index = (Integer) event.object;
if (eventId == 1) {
IntentParams intentParams = new IntentParams();
intentParams.setParam("index", index); // 选择列表项下标
// 跳转到 RightAbility 分屏
Intent intent = new Intent();
intent.setParams(intentParams);
ElementName element = new ElementName("", context.getBundleName(), RightAbility.class.getName());
intent.setElement(element);
context.startAbility(intent, 0);
}
}
}
}
项目用的数据是静态数据,在一个Utils封装好的,以下:
public class Utils {
private static final HiLogLabel TAG = new HiLogLabel(HiLog.LOG_APP, 0xD001400, "Utils");
private static final List<Integer> PICTURE_IDS = Arrays.asList(ResourceTable.Media_1,
ResourceTable.Media_2, ResourceTable.Media_3,
ResourceTable.Media_4, ResourceTable.Media_5,
ResourceTable.Media_6, ResourceTable.Media_7,
ResourceTable.Media_8, ResourceTable.Media_9,
ResourceTable.Media_10);
private static List<PixelMap> resourcePixelMaps;
public static void transResourceIdsToListOnce(Context context) {
resourcePixelMaps = new ArrayList<>(0);
// Set the pixel map
for (int index: PICTURE_IDS) {
InputStream source = null;
ImageSource imageSource;
try {
source = context.getResourceManager().getResource(index);
imageSource = ImageSource.create(source, null);
resourcePixelMaps.add(imageSource.createPixelmap(null));
} catch (IOException | NotExistException e) {
HiLog.error(TAG, "Get Resource PixelMap error");
} finally {
try {
assert source != null;
source.close();
} catch (IOException e) {
HiLog.error(TAG, "getPixelMap source close error");
}
}
}
}
/**
* 封装列表项数据
* @return
*/
public static List<Item> getItems() {
List<Item> items = new ArrayList<>();
int index = 1;
for (PixelMap pixelMap : resourcePixelMaps) {
Item item = new Item();
item.setImg(pixelMap);
item.setTitle("测试标题 " + index);
items.add(item);
index++;
}
return items;
}
/**
* 根据下标获取列表项数据
* @param index
* @return
*/
public static Item getItem(int index) {
List<Item> items = getItems();
return items.get(index);
}
}
在MainAbilitySlice加载显示出列表容器里的内容:
public class MainAbilitySlice extends AbilitySlice {
// 列表容器
private static ListContainer listContainer;
// 列表适配器类
private static ListAdapter listAdapter;
// 上下文
private static Context context;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
context = getContext();
// 初始化界面
initView();
// 将 Media 中的图像转换为 List<PixelMap>。 使用默认大小
Utils.transResourceIdsToListOnce(context);
// UI线程更新列表数据
getUITaskDispatcher().delayDispatch(() -> initData(Utils.getItems()), 10);
}
/**
* 初始化界面
*/
private void initView() {
// 获取List容器
listContainer = (ListContainer) findComponentById(ResourceTable.Id_list);
// 初始化列表适配器
listAdapter = new ListAdapter(null, context);
// 设置列表容器项数据提供者
listContainer.setItemProvider(listAdapter);
}
/**
* 更新界面数据
* @param items
*/
public static void initData(List<Item> items) {
context.getUITaskDispatcher().asyncDispatch(() -> {
listContainer.setItemProvider(listAdapter);
listAdapter.replace(items);
});
}
}
这样主界面的List列表就完成了,项目用的素材图片是放在resources -> media下的,下来介绍一下点击列表项跳转到详情页面,这里我们要创建一个Page Ability, 取名为RightAbility,同时在使用平行视界时,这个Ability显示在平板的右边。
界面布局分为上下显示,上面显示标题和后面用到的服务流转图标,下面显示点击的图片
<?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">
<DependentLayout
ohos:id="$+id:container_title"
ohos:height="50vp"
ohos:padding="10vp"
ohos:width="match_parent">
<Text
ohos:id="$+id:right_title"
ohos:height="match_parent"
ohos:width="match_content"
ohos:padding="5vp"
ohos:text_size="16fp"
ohos:left_margin="10vp"/>
<Image
ohos:id="$+id:imgConnect"
ohos:left_margin="10vp"
ohos:right_margin="10vp"
ohos:height="30vp"
ohos:width="30vp"
ohos:background_element="$media:connect"
ohos:left_of="$id:imgCirculation"/>
<Image
ohos:id="$+id:imgCirculation"
ohos:height="30vp"
ohos:width="30vp"
ohos:right_margin="10vp"
ohos:background_element="$media:circulation"
ohos:align_parent_right="true"/>
</DependentLayout>
<Image
ohos:id="$+id:right_img"
ohos:height="match_parent"
ohos:scale_mode="zoom_center"
ohos:width="match_parent"></Image>
</DirectionalLayout>
在新Page Ability接收到跳转过来的参数,显示相应的标题和图片
public class RightAbilitySlice extends AbilitySlice {
private static Context context; // 上下文
private static int paramIndex; // 页面跳转参数
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_right);
context = getContext();
// 如果页面跳转参数index为空, 默认为0
paramIndex = intent.getIntParam("index", 0);
Item item = Utils.getItem(paramIndex);
// 初始化界面
initView(item);
}
private void initView(Item item) {
Image img = (Image)findComponentById(ResourceTable.Id_right_img);
img.setPixelMap(item.getImg());
Text title = (Text)findComponentById(ResourceTable.Id_right_title);
title.setText(item.getTitle());
Image btnSelect = (Image)findComponentById(ResourceTable.Id_imgConnect);
Image btnShare = (Image)findComponentById(ResourceTable.Id_imgCirculation);
btnSelect.setClickedListener(va -> {
// 打开设备选择框(连接)
//deviceDialog.showDeviceList();
});
btnShare.setClickedListener(va -> {
// 流转
//circulation();
});
}
}
- 添加JS服务卡片, 点击跳转到List列表
先创建一个JS服务卡片,步骤以下:
默认服务卡片里面内容,不是我要的效果,我简单修改一下后:
<div class="card_root_layout">
<div class="button_containers">
<div class="item_first_container">
<div class="button_left">
<image src="/common/images/1.jpg" onclick="routerEvent"></image>
</div>
<div class="button_right">
<image src="/common/images/2.jpg" onclick="routerEvent"></image>
</div>
</div>
<div class="item_second_container">
<div class="button_left">
<image src="/common/images/3.jpg" onclick="routerEvent"></image>
</div>
<div class="button_right">
<image src="/common/images/4.jpg" onclick="routerEvent"></image>
</div>
</div>
</div>
</div>
.card_root_layout {
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 10px;
background-color: #FFFFFF;
}
.button_containers {
flex-direction: column;
align-items: center;
justify-content: center;
}
.item_first_container {
flex-weight: 0.4;
flex-direction: row;
align-items: center;
justify-content: center;
}
.item_second_container {
flex-weight: 0.4;
flex-direction: row;
align-items: center;
justify-content: center;
}
.button_left {
flex-weight: 0.5;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
}
.button_right {
flex-weight: 0.5;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
}
{
"actions": {
"routerEvent": {
"action": "router",
"abilityName": "com.army.study.MainAbility"
}
}
}
简单的服务卡片就修改好了,点击服务卡片,跳转到List列表界面。
- 平行视界, 点击List列表, 左边显示列表,右边显示详情图片
实现平行视界,注意三点就可以了,第一点,平行视界目前只能在两个Java Page Ability实现,第二点在resources -> rawfile目录下添加一个文件easygo.json, 第三点修改config.json配置,就可以了,下面我们就一点一点来完成平行视界效果.
第一点:上面已经创建好两个Java Page Ability了,一个是MainAbility, 一个是RightAbility。
第二点:添加easygo.json内容为
{
"easyGoVersion": "1.0",
"client": "com.army.study", // 保持和config.json bundleName一致
"logicEntities": [
{
"head": {
"function": "magicwindow",
"required": "true"
},
"body": {
"mode": "1",
"abilityPairs": [
{
"from": "com.army.study.MainAbility", // 显示在平行视界左边的列表页面
"to": "com.army.study.RightAbility" // 显示在平行视界右边的详情页面
}
],
"UX": {
"isDraggable": "true",
"supportRotationUxCompat": "true",
"supportDraggingToFullScreen": "ALL"
}
}
}
]
}
第三点:修改config.json在moudle节点下添加以下:
"metaData": {
"customizeData": [
{
"name": "EasyGoClient",
"value": "true"
}
]
}
平行视界效果就完成了。
-
服务流转, 在详情,点击连接图标, 选择流转设备; 再点击流转图标
服务流转也是有几点注意的,第一点当然就是授权了,除了在config.json配置了权限申请,还要在应用入口加上动态授权,第二点就是实现流转的Abitily和AbilitySlic都要实现IAbilityContinuation接口,第三点就是显示当然环境下,在线设备可以流转到的设备,这里使用到了Codelabs平行视界Sample里封装好的DeviceDialog类,里面使用的流转任务管理服务管理类IContinuationRegisterManager, 简化了很多代码。下面就开始一 一介绍吧。
第一点授权,在config.json里module节点下添加以下配置
"reqPermissions": [
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"usedScene":
{
"ability": [
"com.army.study.MainAbility",
"com.army.study.RightAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO",
"usedScene":
{
"ability": [
"com.army.study.MainAbility",
"com.army.study.RightAbility"
],
"when": "always"
}
}
]
在应用入口MainAbility里onStart()方法添加动态授权:
// 声明跨端迁移访问的权限
requestPermissionsFromUser(new String[]{SystemPermission.DISTRIBUTED_DATASYNC}, 0);
第二点在RightAbility和RightAbilitySlice多实现IAbilityContinuation接口,其实RightAbility实现接口后,在实现方法里面返回true就可以,具体逻辑是在RightAbilitySlice中实现。
public class RightAbility extends Ability implements IAbilityContinuation {
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setMainRoute(RightAbilitySlice.class.getName());
}
@Override
public boolean onStartContinuation() {
return true;
}
@Override
public boolean onSaveData(IntentParams intentParams) {
return true;
}
@Override
public boolean onRestoreData(IntentParams intentParams) {
return true;
}
@Override
public void onCompleteContinuation(int i) {
}
}
public class RightAbilitySlice extends AbilitySlice implements IAbilityContinuation {
private boolean isCirculation = false; // 是否流转中
private static String selectDeviceId; // 选择的设备id
private DeviceDialog deviceDialog; // 设备选择
private static Context context; // 上下文
private static int paramIndex; // 页面跳转参数
private static int removeIndex; // 流转参数
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_right);
context = getContext();
// 如果页面跳转参数index为空,说明是流转过来参数, 使用流转removeIndex
paramIndex = intent.getIntParam("index", removeIndex);
Item item = Utils.getItem(paramIndex);
// 初始化界面
initView(item);
// 初始化设备选择
deviceDialog = new DeviceDialog(getContinuationRegisterManager(),RightAbilitySlice.this);
}
private void initView(Item item) {
Image img = (Image)findComponentById(ResourceTable.Id_right_img);
img.setPixelMap(item.getImg());
Text title = (Text)findComponentById(ResourceTable.Id_right_title);
title.setText(item.getTitle());
Image btnSelect = (Image)findComponentById(ResourceTable.Id_imgConnect);
Image btnShare = (Image)findComponentById(ResourceTable.Id_imgCirculation);
btnSelect.setClickedListener(va -> {
// 打开设备选择框(连接)
deviceDialog.showDeviceList();
});
btnShare.setClickedListener(va -> {
// 流转
circulation();
});
}
/**
* 流转
*/
private void circulation() {
if (isCirculation) {
Utils.createToastDialog(context, "正在流转,请稍后再试!");
return;
}
if (selectDeviceId == null || "".equals(selectDeviceId)) {
// 选择设备以后才能流转(迁移)
Utils.createToastDialog(context, "请选择连接设备后进行流转操作!");
return;
}
isCirculation = true;
continueAbility(selectDeviceId);
}
/**
* 设置选择设备Id
* @param deviceId
*/
public static void setDeviceId(String deviceId) {
selectDeviceId = deviceId;
}
@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) {
// 流转前保存index
intentParams.setParam("removeIndex", paramIndex);
return true;
}
@Override
public boolean onRestoreData(IntentParams intentParams) {
// 获取流转过来的index
removeIndex = Integer.parseInt(intentParams.getParam("removeIndex").toString());
return true;
}
@Override
public void onCompleteContinuation(int i) {
}
}
这里说一下详情页参数index,有两个地方传递了,一个是在单设备时,从列表点击跳转到详情页,传递了参数index,另一个是服务流转到另一个设备时,流转前通过onSaveData保存index,到另一个设备通过onRestoreData获取到流转过来的index.
第三点就是使用封装好的DeviceDialog类,代码不多,我都添加注释了,把这个类理解了,分布式工作就省了不少代码了。
public class DeviceDialog {
private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, "设备对话框");
// 上下文
private final Context context;
// 获取流转任务管理服务管理类
private final IContinuationRegisterManager continuationRegisterManager;
// 注册传输任务管理服务后返回的能力令牌
private int abilityToken;
// 用户在设备列表中选择设备后返回的设备ID
private String selectDeviceId;
/**
* 初始化设备对话框,设置传输任务管理服务管理类,并注册传输任务管理服务管理类。
* @param continuationRegisterManager
* @param slice
*/
public DeviceDialog(IContinuationRegisterManager continuationRegisterManager, Context slice) {
this.continuationRegisterManager = continuationRegisterManager;
this.context = slice;
// 注册流转任务管理服务管理类
registerManager();
}
/**
* 注册流转任务管理服务管理类
*/
private void registerManager() {
// 增加过滤条件
ExtraParams params = new ExtraParams();
String[] devTypes = new String[]{ExtraParams.DEVICETYPE_SMART_PAD, ExtraParams.DEVICETYPE_SMART_PHONE};
params.setDevType(devTypes);
continuationRegisterManager.register(context.getBundleName(), params, callback, requestCallback);
}
// 设置流转任务管理服务设备状态变更的回调
private final IContinuationDeviceCallback callback = new IContinuationDeviceCallback() {
@Override
public void onDeviceConnectDone(String str, String str1) {
// 在用户选择设备后设置设备ID
selectDeviceId = str;
continuationRegisterManager.updateConnectStatus(abilityToken, selectDeviceId,
DeviceConnectState.CONNECTED.getState(), null);
// 返回设备ID
returnDeviceId();
}
@Override
public void onDeviceDisconnectDone(String str) {
}
};
// 设置注册流转任务管理服务回调
private final RequestCallback requestCallback = new RequestCallback() {
@Override
public void onResult(int result) {
abilityToken = result;
}
};
/**
* 返回设备ID
*/
private void returnDeviceId() {
HiLog.info(LABEL_LOG, "deviceid::" + selectDeviceId);
// 将选择设备Id, 告诉RightAbilitySlice
RightAbilitySlice.setDeviceId(selectDeviceId);
}
/**
* 打开设备选择框
*
* @since 2021-09-10
*/
public void showDeviceList() {
// 设置过滤设备类型
ExtraParams params = new ExtraParams();
String[] devTypes = new String[]{ExtraParams.DEVICETYPE_SMART_PAD, ExtraParams.DEVICETYPE_SMART_PHONE};
params.setDevType(devTypes);
// 注册
continuationRegisterManager.register(context.getBundleName(), params, callback, requestCallback);
// 显示选择设备列表
continuationRegisterManager.showDeviceList(abilityToken, params, null);
}
/**
* 断开流转任务管理服务
*
* @since 2021-09-10
*/
public void clearRegisterManager() {
// 解注册流转任务管理服务
continuationRegisterManager.unregister(abilityToken, null);
// 断开流转任务管理服务连接
continuationRegisterManager.disconnect();
}
}
到这里就介绍完我说的四个步骤了,帖子有些长,要有点耐心看才行,如果不能集中看完,也可以直接同步源码,先运行起来,看看效果,然后再一步一步的去理解就,最后合在一起就成了。
第一时间拜读楼主文章。
文章有些长, 代码太多了, 建议可以先过一遍文字描述, 再结合代码注释来看, 容易理解到讲到的知识点.