[文章]HarmonyOS教程—基于分布式能力实现地图导航流转

阅读量0
1
3
1. 介绍      
本篇Codelab将为我们展示手机-车机-智能穿戴的无缝衔接,实现如下场景:在手机上搜索目的地,启动导航,上车后,在手机点击"迁移"按钮,导航流转到车机上;下车后,在车机上点击"迁移"按钮,导航流转到手表和手机,通过手表的提示信息,实现到达目的地前最后一公里的步行导航,场景展示如下。
说明
本篇Codelab为模拟导航,为了方便演示,下文将以平板来模拟车机使用的场景。

2. 搭建HarmonyOS环境
我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。
  • 安装DevEco Studio,详情请参考下载和安装软件。
  • 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
    • 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
    • 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
  • 开发者可以参考以下链接,完成设备调试的相关配置:
    • 使用真机进行调试
    • 使用模拟器进行调试
您可以利用如下设备完成Codelab:
  • 开启了开发者模式的两部HarmonyOS真机
  • 装有HarmonyOS系统的智能穿戴,且和手机通过"运动健康"APP完成配对。

说明
1、本篇Codelab中使用到了Gson类库,如果因为网络环境无法加载导致程序编译不通过,请开发者自行下载,将下载的jar放到项目libs目录下手动添加依赖。

3. 代码结构解读      
在本篇Codelab中我们只对核心代码进行讲解,您可以在后面章节中下载完整代码,首先来介绍下整个工程的代码结构:
  • IMapIdlInterface.idl:接口中定义了action方法,用于本地Ability和远程Service之间通信。
  • bean:用于封装地图API接口返回的结果。
  • map:存放地图和路径导航的相关类。
  • provider:用于加载输入提示列表的数据。
  • slice:MainAbilitySlice为手机和平板地图加载的主页面,WatchAbilitySlice为智能穿戴导航提示页面。
  • util:封装了公共方法,包括Gson工具类、网络请求工具类、图像工具类、日志工具类、地图工具类、权限工具类、屏幕工具类。
  • MainAbility:实现IAbilityContinuation接口用于数据迁移,同时动态权限的申请也在这里实现。
  • WatchAbility:用于显示智能穿戴导航提示页面。
  • WatchService:智能穿戴端Service Ability,供手机端连接。

4. 相关权限      
本程序开发需要申请以下三类权限,应用权限的申请可以参考权限章节。
访问Internet权限:
ohos.permission.INTERNET
获取位置信息权限:
ohos.permission.LOCATION
分布式数据管理相关权限:
ohos.permission.DISTRIBUTED_DATASYNC
ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE
ohos.permission.GET_DISTRIBUTED_DEVICE_INFO
ohos.permission.GET_BUNDLE_INFO
说明
其中ohos.permission.LOCATION和ohos.permission.DISTRIBUTED_DATASYNC权限需要动态申请。

5. 地图加载地图相关API说明
本Codelab的地图和导航数据,是通过高德地图相关API接口来获取的,具体使用可以参考高德地图开发者官网。在Const文件中定义了以下API接口常量:
  • TILE_URL:地图瓦片数据加载API,用于加载地图。
  • REGION_DETAIL_URL:地理/逆地理编码API,将经纬度转换为详细结构化的地址。
  • INPUT_TIPS_URL:输入提示API ,提供根据用户输入的关键词查询返回地址列表。
  • ROUTE_URL:路径规划API ,进行线路查询返回路径数据。
说明
1、以上均为高德地图API接口,若用于商业用途,请遵守高德地图商业协议。
2、使用这些接口前,开发者需要将Const文件中的MAP_KEY修改为自己应用的key,key值的获取可以参考高德地图开发者官网。

加载地图组件
地图的加载是通过加载高德地图的地图瓦片来实现的。根据设置的瓦片大小,将手机屏幕分割成若干块,每一块加载一个地图瓦片,所有瓦片拼接起来,就成了一个完整的地图。地图的加载功能封装到了自定义组件NavMap中,实现步骤如下:
步骤 1 -  实现地图组件
通过initMapCanvas,获取瓦片行数(列数)、瓦片真实长度等值。

  1. public void initMapCanvas(boolean isRefresh) {
  2.     // 在某个缩放级别下,瓦片的行数(列数)
  3.     int rowCount = (int) Math.pow(COMPONENT_HALF, ZOOM);
  4.     tileRealLength = OVER_LENGTH * COMPONENT_HALF / rowCount;
  5.     mapComponentWidth = ScreenUtils.getScreenWidth(getContext());
  6.     mapComponentHeight = ScreenUtils.getScreenHeight(getContext());

  7.     double minX = centerPoint.getPointX() - mapComponentWidth / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH;
  8.     colMin = (int) Math.floor((minX + OVER_LENGTH) / tileRealLength);
  9.     double maxX = centerPoint.getPointX() + mapComponentWidth / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH;
  10.     colMax = Math.min((int) Math.floor((maxX + OVER_LENGTH) / tileRealLength), rowCount - 1);
  11.     double maxY = centerPoint.getPointY() + mapComponentHeight / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH;
  12.     rowMin = Math.min(rowCount - 1 - (int) Math.floor((maxY + OVER_LENGTH) / tileRealLength), rowCount - 1);
  13.     double minY = centerPoint.getPointY() - mapComponentHeight / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH;
  14.     rowMax = rowCount - 1 - (int) Math.floor((minY + OVER_LENGTH) / tileRealLength);

  15.     addDrawTask(this);
  16.     setTouchEventListener(this);

  17.     initTiles(isRefresh);
  18.     initElement(isRefresh);
  19. }
复制代码
调用initTiles方法,请求高德地图API获取瓦片,调用setTiles方法,给瓦片数据集合mapTiles设置数据。

private void initTiles(boolean isRefresh) {     if (mapTiles == null || isRefresh) {         mapTiles = new CopyOnWriteArrayList<>();     }     mapTiles.removeIf(mapTile -> !mapTile.isInBoundary(rowMin, rowMax, colMin, colMax));      getContext().getGlobalTaskDispatcher(TaskPriority.DEFAULT).asyncDispatch(this::setTiles); }
在NavMap的onDraw()方法中调用drawTiles,绘制地图瓦片。

private void drawTiles(Canvas canvas) {     for (MapTile mapTile : mapTiles) {         canvas.drawPixelMapHolder(mapTile, mapTile.getNowPointX(), mapTile.getNowPointY(), linePaint);     } }
步骤 2 -  获取当前位置
为了加载用户所在地区的地图,本程序需要先获取设备的位置信息(经度和纬度),获取当前位置信息的方法封装在LocationHelper里面,再通过经纬度调用高德API获取CityCode,主要代码实现如下:

  1. public void getMyLocation() {
  2.     new LocationHelper().getMyLocation(context, loc -> {
  3.         double locLongitude = loc.getLongitude();
  4.         double locLatitude = loc.getLatitude();
  5.         location = locLongitude + "," + locLatitude;
  6.         if (navMap.getMapElements() == null) {
  7.             setMapCenter(locLongitude, locLatitude);
  8.             getRegionDetail();
  9.         }
  10.     });
  11. }
复制代码
步骤 3 -  加载地图
获取经纬度之后,调用MapElement的setCenterPoint方法,加载地图。

  1. public void setMapCenter(double lon, double lat) {
  2.     double[] mercators = MapUtils.lonLat2Mercator(lon, lat);
  3.     Point centerPoint = new Point((float) mercators[0], (float) mercators[1]);
  4.     navMap.setCenterPoint(centerPoint);
  5.     MapElement peopleEle = new MapElement(centerPoint.getPointX(), centerPoint.getPointY(), true);
  6.     peopleEle.setActionType(Const.ROUTE_PEOPLE);
  7.     navMap.addElement(peopleEle);
  8. }
复制代码
说明
本Codelab地图加载的实现参考了董昱老师的TinyMap。

6. 绘制导航轨迹      
设置起点和终点
通过高德地理编码API获取设备所在城市编号localCityCode。

  1. private void getRegionDetail() {
  2.     String url = String.format(Const.REGION_DETAIL_URL, location, Const.MAP_KAY);
  3.     HttpUtils.getInstance(context).get(url, result -> {
  4.         RegionDetailResult regionDetailResult = GsonUtils.jsonToBean(result, RegionDetailResult.class);
  5.         localCityCode = regionDetailResult.getRegeocode().getAddressComponent().getCitycode();
  6.     });
  7. }
复制代码
在起点或终点输入框中输入关键字,调用高德输入提示的API获取坐标数据展现在列表ListContainer中,从列表中选取终点或者起点。

  1. public void getInputTips(String keyWords) {
  2.     String url = String.format(Const.INPUT_TIPS_URL, keyWords, Const.MAP_KAY, location, localCityCode);
  3.     HttpUtils.getInstance(context).get(url, result -> {
  4.         InputTipsResult inputTipsResult = GsonUtils.jsonToBean(result, InputTipsResult.class);
  5.         if (inputTipsResult == null) {
  6.             return;
  7.         }
  8.         dataCallBack.setInputTipsView(inputTipsResult.getTips());
  9.     });
  10. }
复制代码
获取路径规划
获得起点坐标location和终点坐标endLocation后,调用高德地图路径规划API,获取路径规划。然后调用 MapHelper的parseRoute方法,解析上面方法获得的路径规划数据result,将创建的地图元素MapElement 对象,添加到TinyMap组件中的elements集合中。

  1. public void getRouteResult(String endLocation) {
  2.     String url = String.format(Const.ROUTE_URL, location, endLocation, Const.MAP_KAY);
  3.     HttpUtils.getInstance(context).get(url, result -> dataCallBack.setRouteView(result));
  4. }
复制代码
绘制导航路径
通过上一小节获取到了路径规划元素的集合elements,elements是路径上的点的集合。通过HarmonyOS Path类将elements中的元素点连接起来,就成为一条路径了,然后在onDraw方法中绘制path,即路径绘制成功。

  1. public void onDraw(Component component, Canvas canvas) {
  2.     path.reset();
  3.     grayPath.reset();
  4.     drawTiles(canvas);
  5.     drawRoutePath(canvas);
  6.     drawImageElement(canvas);
  7. }
  8.          
  9. private void drawRoutePath(Canvas canvas) {
  10.     for (int i = 1; i < elements.size(); i++) {
  11.         MapElement mapElement = elements.get(i);
  12.         Point point = mapElement.getNowPoint();
  13.         float pointX = point.getPointX();
  14.         float pointY = point.getPointY();
  15.         if (i == 1 || i == elements.size() - 1) {
  16.             path.moveTo(pointX, pointY);
  17.         } else {
  18.             path.lineTo(pointX, pointY);
  19.         }

  20.         if (i <= stepPoint) {
  21.             if (i == 1 || i == elements.size() - 1) {
  22.                 grayPath.moveTo(pointX, pointY);
  23.             } else {
  24.                 grayPath.lineTo(pointX, pointY);
  25.             }
  26.         }
  27.     }

  28.     // 绘制导航路径(未走过的路径)
  29.     canvas.drawPath(path, paint);

  30.     // 绘制导航路径(已走过的路径)
  31.     canvas.drawPath(grayPath, grayPaint);
  32. }
复制代码
模拟轨迹移动
导航轨迹的移动,实际上是定位图标从路径轨迹集合elements的一个元素的坐标,移动到下一个元素的坐标。
在MainAbilitySlice中触发"开始导航"点击事件。

  1. [url=home.php?mod=space&uid=2735960]@Override[/url]
  2. public void onClick(Component component) {
  3.     switch (component.getId()) {
  4.         // 开始导航
  5.         case ResourceTable.Id_start_nav:
  6.             mapManager.startNav();
  7.             setStartNavView();
  8.             break;
  9.     }
  10. }
复制代码
在MapManager中,通过EventHandler对象执行延时任务Runnable,实现定位图标坐标点的持续变化,主要实现代码如下:

  1. public void startNav() {
  2.     if (!mapEventHandler.hasInnerEvent(task)) {
  3.         mapEventHandler.postTask(task, STEP_DELAY_TIME, EventHandler.Priority.IMMEDIATE);
  4.         connectWatch();
  5.     }
  6. }
  7. private Runnable task = new Runnable() {
  8.     @Override
  9.     public void run() {
  10.         // 将定位图标的下一个坐标点的坐标,赋值给当前定位图标
  11.         MapElement peopleElement = navMap.getMapElements().get(0);
  12.         nextElement = navMap.getMapElements().get(stepPoint + 1);
  13.         peopleElement.setMercatorPoint(nextElement.getMercatorPoint());
  14.         peopleElement.setNowPoint(nextElement.getNowPoint());
  15.         peopleElement.setOriginPoint(nextElement.getOriginPoint());

  16.         // 调用sendEvent方法,发送更新UI事件消息
  17.         mapEventHandler.sendEvent(1, EventHandler.Priority.IMMEDIATE);

  18.         // 再次调用postTask发送延时任务,让任务持续进行,从而实现坐标的持续移动
  19.         mapEventHandler.postTask(task, STEP_DELAY_TIME, EventHandler.Priority.IMMEDIATE);
  20.         stepPoint++;

  21.         // 将stepPoint传递给NavMap,绘制已经过的路径时也会用到stepPoint
  22.         navMap.setStepPoint(stepPoint);
  23.         LogUtils.info(TAG, "run......" + stepPoint);
  24.         if (stepPoint >= navMap.getMapElements().size() - 1) {
  25.             mapEventHandler.removeTask(task);
  26.         }
  27.     }
  28. };
复制代码
调用NavMap的invalidate()方法,重新绘制地图,实现轨迹的移动。

  1. private class MapEventHandler extends EventHandler {
  2.     private MapEventHandler(EventRunner runner) {
  3.         super(runner);
  4.     }

  5.     @Override
  6.     public void processEvent(InnerEvent event) {
  7. ...
  8.         navMap.invalidate();
  9.     }
  10. }
复制代码
7. 数据流转 数据从手机流转到智能穿戴
数据从手机流转到智能穿戴是通过IDL来实现的(IDL的介绍和使用可以参考IDL章节),具体实现如下:在手机上点击"开始导航"后,会连接智能表的WatchService,同时会拉起WatchAbility,主要实现代码如下:

  1. // 在MainAbilitySlie中触发点击事件。
  2. @Override
  3. public void onClick(Component component) {
  4.     switch (component.getId()) {
  5.         // 开始导航
  6.         case ResourceTable.Id_start_nav:
  7.             mapManager.startNav();
  8.             setStartNavView();
  9.             break;
  10.     }
  11. }
  12. // 在MapManager中连接智能穿戴的WatchService,同时会拉起WatchAbility。
  13. public void startNav() {
  14.     if (!mapEventHandler.hasInnerEvent(task)) {
  15.         mapEventHandler.postTask(task, STEP_DELAY_TIME, EventHandler.Priority.IMMEDIATE);
  16.         connectWatch();
  17.     }
  18. }
复制代码
在MapEventHandler的processEvent方法中,通过requestRemote方法,将数据发送给智能穿戴的WatchService。
  1. private class MapEventHandler extends EventHandler {
  2.     private MapEventHandler(EventRunner runner) {
  3.         super(runner);
  4.     }

  5.     @Override
  6.     public void processEvent(InnerEvent event) {
  7.         super.processEvent(event);
  8.         if (event.eventId != 1) {
  9.             return;
  10.         }
  11.         LogUtils.info(TAG, "processEvent invalidate");
  12.         if (nextElement.getActionType() != null && !nextElement.getActionType().isEmpty()) {
  13.             navListener.onNavListener(nextElement);
  14.         }
  15.         if (proxy != null) {
  16.             requestRemote(nextElement.getActionType() == null ? "" : nextElement.getActionType(),
  17.                 nextElement.getActionContent() == null ? "" : nextElement.getActionContent());
  18.         }
  19.         navMap.invalidate();
  20.     }
  21. }
复制代码
WatchService接收数据之后,通过CommentEvent将数据发送给WatchAbility。
  1. public class WatchRemote extends MapIdlInterfaceStub {
  2.     private WatchRemote(String descriptor) {
  3.         super(descriptor);
  4.     }

  5.     @Override
  6.     public void action(String actionType, String actionContent) throws RemoteException {
  7.         LogUtils.info(TAG, "WatchService::action");
  8.         sendEvent(actionType, actionContent);
  9.     }
  10. }

  11. private void sendEvent(String actionType, String actionContent) {
  12.     LogUtils.info(TAG, "WatchService::sendEvent");
  13.     try {
  14.         Intent intent = new Intent();
  15.         Operation operation = new Intent.OperationBuilder().withAction("com.huawei.map").build();
  16.         intent.setOperation(operation);
  17.         intent.setParam("actionType", actionType);
  18.         intent.setParam("actionContent", actionContent);
  19.         CommonEventData eventData = new CommonEventData(intent);
  20.         CommonEventManager.publishCommonEvent(eventData);
  21.     } catch (RemoteException e) {
  22.         LogUtils.info(TAG, "publishCommonEvent occur exception.");
  23.     }
  24. }
复制代码
在WatchAbilitySlice中接收WatchService发送的数据,然后处理数据。

  1. private class MyCommonEventSubscriber extends CommonEventSubscriber {
  2.     MyCommonEventSubscriber(CommonEventSubscribeInfo info) {
  3.         super(info);
  4.     }

  5.     @Override
  6.     public void onReceiveEvent(CommonEventData commonEventData) {
  7.         Intent intent = commonEventData.getIntent();
  8.         String actionType = intent.getStringParam("actionType");
  9.         String actionContent = intent.getStringParam("actionContent");
  10.         if (actionType != null) {
  11.             if (actionType.equals(Const.STOP_WATCH_ABILITY)) {
  12.                 terminateAbility();
  13.             } else {
  14.                 actionImg.setPixelMap(ImageUtils.getImageId(actionType));
  15.                 contentComponent.setText(actionContent);
  16.             }
  17.         }
  18.     }
  19. }
复制代码
数据在平板和手机之间流转
手机和平板之间的数据流转是通过数据迁移continueAbility实现的,MainAbility和MainAbilitySlice都要实现IAbilityContinuation接口 。continueAbility的介绍和使用,可以参考流转章节,下面以数据从手机流转到平板为例。

在手机上点击"迁移"按钮,调用continueAbility方法,将会调用onSaveData方法,同时会拉起平板的MainAbilitySlice,平板的MainAbilitySlice会执行onRestoreData方法,具体实现在MainAbilitySlice中,如下:

手机端在onSaveData方法中保存要流转到平板端MainAbilitySlice的数据。
  1. @Override
  2. public boolean onSaveData(IntentParams saveData) {
  3.     String elementsString = GsonUtils.objectToString(navMap.getMapElements());
  4.     saveData.setParam(ELEMENT_STRING, elementsString);
  5.     saveData.setParam("stepPoint", mapManager.getStepPoint());
  6.     LogUtils.info(TAG, "onSaveData" + navMap.getMapElements().size());
  7.     return true;
  8. }
复制代码
平板端MainAbilitySlice在onRestoreData方法中取出数据。

  1. @Override
  2. public boolean onRestoreData(IntentParams restoreData) {
  3.     if (restoreData.getParam(ELEMENT_STRING) instanceof String) {
  4.         String elementsString = (String) restoreData.getParam(ELEMENT_STRING);
  5.         elements = GsonUtils.jsonToList(elementsString, MapElement.class);
  6.     }
  7.     stepPoint = (int) restoreData.getParam("stepPoint");
  8.     LogUtils.info(TAG, "onRestoreData::elements::" + elements.size());
  9.     return true;
  10. }
复制代码
平板取到数据之后在initView()方法中使用该数据绘制地图。
在平板上点击"迁移"按钮,数据会以以上同样的方式从平板流转到手机。

8. 效果展示数据从手机流转到智能穿戴和平板的效果图如下:


9. 恭喜您目前您已经成功完成了Codelab并且学到了:
  • 分布式任务调度。
  • HarmonyOS自定义组件的基本使用。
  • 位置服务的基本使用。
  • IDL的基本使用。

10. 完整示例  


回帖

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
链接复制成功,分享给好友