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,获取瓦片行数(列数)、瓦片真实长度等值。
- public void initMapCanvas(boolean isRefresh) {
- // 在某个缩放级别下,瓦片的行数(列数)
- int rowCount = (int) Math.pow(COMPONENT_HALF, ZOOM);
- tileRealLength = OVER_LENGTH * COMPONENT_HALF / rowCount;
- mapComponentWidth = ScreenUtils.getScreenWidth(getContext());
- mapComponentHeight = ScreenUtils.getScreenHeight(getContext());
-
- double minX = centerPoint.getPointX() - mapComponentWidth / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH;
- colMin = (int) Math.floor((minX + OVER_LENGTH) / tileRealLength);
- double maxX = centerPoint.getPointX() + mapComponentWidth / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH;
- colMax = Math.min((int) Math.floor((maxX + OVER_LENGTH) / tileRealLength), rowCount - 1);
- double maxY = centerPoint.getPointY() + mapComponentHeight / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH;
- rowMin = Math.min(rowCount - 1 - (int) Math.floor((maxY + OVER_LENGTH) / tileRealLength), rowCount - 1);
- double minY = centerPoint.getPointY() - mapComponentHeight / COMPONENT_HALF_F * tileRealLength / TILE_LENGTH;
- rowMax = rowCount - 1 - (int) Math.floor((minY + OVER_LENGTH) / tileRealLength);
-
- addDrawTask(this);
- setTouchEventListener(this);
-
- initTiles(isRefresh);
- initElement(isRefresh);
- }
复制代码调用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,主要代码实现如下:
- public void getMyLocation() {
- new LocationHelper().getMyLocation(context, loc -> {
- double locLongitude = loc.getLongitude();
- double locLatitude = loc.getLatitude();
- location = locLongitude + "," + locLatitude;
- if (navMap.getMapElements() == null) {
- setMapCenter(locLongitude, locLatitude);
- getRegionDetail();
- }
- });
- }
复制代码步骤 3 - 加载地图
获取经纬度之后,调用MapElement的setCenterPoint方法,加载地图。
- public void setMapCenter(double lon, double lat) {
- double[] mercators = MapUtils.lonLat2Mercator(lon, lat);
- Point centerPoint = new Point((float) mercators[0], (float) mercators[1]);
- navMap.setCenterPoint(centerPoint);
- MapElement peopleEle = new MapElement(centerPoint.getPointX(), centerPoint.getPointY(), true);
- peopleEle.setActionType(Const.ROUTE_PEOPLE);
- navMap.addElement(peopleEle);
- }
复制代码说明
本Codelab地图加载的实现参考了董昱老师的TinyMap。
6. 绘制导航轨迹
设置起点和终点通过高德地理编码API获取设备所在城市编号localCityCode。
- private void getRegionDetail() {
- String url = String.format(Const.REGION_DETAIL_URL, location, Const.MAP_KAY);
- HttpUtils.getInstance(context).get(url, result -> {
- RegionDetailResult regionDetailResult = GsonUtils.jsonToBean(result, RegionDetailResult.class);
- localCityCode = regionDetailResult.getRegeocode().getAddressComponent().getCitycode();
- });
- }
复制代码在起点或终点输入框中输入关键字,调用高德输入提示的API获取坐标数据展现在列表ListContainer中,从列表中选取终点或者起点。
- public void getInputTips(String keyWords) {
- String url = String.format(Const.INPUT_TIPS_URL, keyWords, Const.MAP_KAY, location, localCityCode);
- HttpUtils.getInstance(context).get(url, result -> {
- InputTipsResult inputTipsResult = GsonUtils.jsonToBean(result, InputTipsResult.class);
- if (inputTipsResult == null) {
- return;
- }
- dataCallBack.setInputTipsView(inputTipsResult.getTips());
- });
- }
复制代码 获取路径规划获得起点坐标location和终点坐标endLocation后,调用高德地图路径规划API,获取路径规划。然后调用 MapHelper的parseRoute方法,解析上面方法获得的路径规划数据result,将创建的地图元素MapElement 对象,添加到TinyMap组件中的elements集合中。
- public void getRouteResult(String endLocation) {
- String url = String.format(Const.ROUTE_URL, location, endLocation, Const.MAP_KAY);
- HttpUtils.getInstance(context).get(url, result -> dataCallBack.setRouteView(result));
- }
复制代码 绘制导航路径通过上一小节获取到了路径规划元素的集合elements,elements是路径上的点的集合。通过HarmonyOS Path类将elements中的元素点连接起来,就成为一条路径了,然后在onDraw方法中绘制path,即路径绘制成功。
- public void onDraw(Component component, Canvas canvas) {
- path.reset();
- grayPath.reset();
- drawTiles(canvas);
- drawRoutePath(canvas);
- drawImageElement(canvas);
- }
-
- private void drawRoutePath(Canvas canvas) {
- for (int i = 1; i < elements.size(); i++) {
- MapElement mapElement = elements.get(i);
- Point point = mapElement.getNowPoint();
- float pointX = point.getPointX();
- float pointY = point.getPointY();
- if (i == 1 || i == elements.size() - 1) {
- path.moveTo(pointX, pointY);
- } else {
- path.lineTo(pointX, pointY);
- }
-
- if (i <= stepPoint) {
- if (i == 1 || i == elements.size() - 1) {
- grayPath.moveTo(pointX, pointY);
- } else {
- grayPath.lineTo(pointX, pointY);
- }
- }
- }
-
- // 绘制导航路径(未走过的路径)
- canvas.drawPath(path, paint);
-
- // 绘制导航路径(已走过的路径)
- canvas.drawPath(grayPath, grayPaint);
- }
复制代码 模拟轨迹移动导航轨迹的移动,实际上是定位图标从路径轨迹集合elements的一个元素的坐标,移动到下一个元素的坐标。
在MainAbilitySlice中触发"开始导航"点击事件。
- [url=home.php?mod=space&uid=2735960]@Override[/url]
- public void onClick(Component component) {
- switch (component.getId()) {
- // 开始导航
- case ResourceTable.Id_start_nav:
- mapManager.startNav();
- setStartNavView();
- break;
- }
- }
复制代码在MapManager中,通过EventHandler对象执行延时任务Runnable,实现定位图标坐标点的持续变化,主要实现代码如下:
- public void startNav() {
- if (!mapEventHandler.hasInnerEvent(task)) {
- mapEventHandler.postTask(task, STEP_DELAY_TIME, EventHandler.Priority.IMMEDIATE);
- connectWatch();
- }
- }
- private Runnable task = new Runnable() {
- @Override
- public void run() {
- // 将定位图标的下一个坐标点的坐标,赋值给当前定位图标
- MapElement peopleElement = navMap.getMapElements().get(0);
- nextElement = navMap.getMapElements().get(stepPoint + 1);
- peopleElement.setMercatorPoint(nextElement.getMercatorPoint());
- peopleElement.setNowPoint(nextElement.getNowPoint());
- peopleElement.setOriginPoint(nextElement.getOriginPoint());
-
- // 调用sendEvent方法,发送更新UI事件消息
- mapEventHandler.sendEvent(1, EventHandler.Priority.IMMEDIATE);
-
- // 再次调用postTask发送延时任务,让任务持续进行,从而实现坐标的持续移动
- mapEventHandler.postTask(task, STEP_DELAY_TIME, EventHandler.Priority.IMMEDIATE);
- stepPoint++;
-
- // 将stepPoint传递给NavMap,绘制已经过的路径时也会用到stepPoint
- navMap.setStepPoint(stepPoint);
- LogUtils.info(TAG, "run......" + stepPoint);
- if (stepPoint >= navMap.getMapElements().size() - 1) {
- mapEventHandler.removeTask(task);
- }
- }
- };
复制代码调用NavMap的invalidate()方法,重新绘制地图,实现轨迹的移动。
- private class MapEventHandler extends EventHandler {
- private MapEventHandler(EventRunner runner) {
- super(runner);
- }
-
- @Override
- public void processEvent(InnerEvent event) {
- ...
- navMap.invalidate();
- }
- }
复制代码 7. 数据流转 数据从手机流转到智能穿戴
数据从手机流转到智能穿戴是通过IDL来实现的(IDL的介绍和使用可以参考IDL章节),具体实现如下:在手机上点击"开始导航"后,会连接智能表的WatchService,同时会拉起WatchAbility,主要实现代码如下:
- // 在MainAbilitySlie中触发点击事件。
- @Override
- public void onClick(Component component) {
- switch (component.getId()) {
- // 开始导航
- case ResourceTable.Id_start_nav:
- mapManager.startNav();
- setStartNavView();
- break;
- }
- }
- // 在MapManager中连接智能穿戴的WatchService,同时会拉起WatchAbility。
- public void startNav() {
- if (!mapEventHandler.hasInnerEvent(task)) {
- mapEventHandler.postTask(task, STEP_DELAY_TIME, EventHandler.Priority.IMMEDIATE);
- connectWatch();
- }
- }
复制代码在MapEventHandler的processEvent方法中,通过requestRemote方法,将数据发送给智能穿戴的WatchService。
- private class MapEventHandler extends EventHandler {
- private MapEventHandler(EventRunner runner) {
- super(runner);
- }
-
- @Override
- public void processEvent(InnerEvent event) {
- super.processEvent(event);
- if (event.eventId != 1) {
- return;
- }
- LogUtils.info(TAG, "processEvent invalidate");
- if (nextElement.getActionType() != null && !nextElement.getActionType().isEmpty()) {
- navListener.onNavListener(nextElement);
- }
- if (proxy != null) {
- requestRemote(nextElement.getActionType() == null ? "" : nextElement.getActionType(),
- nextElement.getActionContent() == null ? "" : nextElement.getActionContent());
- }
- navMap.invalidate();
- }
- }
复制代码WatchService接收数据之后,通过CommentEvent将数据发送给WatchAbility。
- public class WatchRemote extends MapIdlInterfaceStub {
- private WatchRemote(String descriptor) {
- super(descriptor);
- }
-
- @Override
- public void action(String actionType, String actionContent) throws RemoteException {
- LogUtils.info(TAG, "WatchService::action");
- sendEvent(actionType, actionContent);
- }
- }
-
- private void sendEvent(String actionType, String actionContent) {
- LogUtils.info(TAG, "WatchService::sendEvent");
- try {
- Intent intent = new Intent();
- Operation operation = new Intent.OperationBuilder().withAction("com.huawei.map").build();
- intent.setOperation(operation);
- intent.setParam("actionType", actionType);
- intent.setParam("actionContent", actionContent);
- CommonEventData eventData = new CommonEventData(intent);
- CommonEventManager.publishCommonEvent(eventData);
- } catch (RemoteException e) {
- LogUtils.info(TAG, "publishCommonEvent occur exception.");
- }
- }
复制代码在WatchAbilitySlice中接收WatchService发送的数据,然后处理数据。
- private class MyCommonEventSubscriber extends CommonEventSubscriber {
- MyCommonEventSubscriber(CommonEventSubscribeInfo info) {
- super(info);
- }
-
- @Override
- public void onReceiveEvent(CommonEventData commonEventData) {
- Intent intent = commonEventData.getIntent();
- String actionType = intent.getStringParam("actionType");
- String actionContent = intent.getStringParam("actionContent");
- if (actionType != null) {
- if (actionType.equals(Const.STOP_WATCH_ABILITY)) {
- terminateAbility();
- } else {
- actionImg.setPixelMap(ImageUtils.getImageId(actionType));
- contentComponent.setText(actionContent);
- }
- }
- }
- }
复制代码 数据在平板和手机之间流转
手机和平板之间的数据流转是通过数据迁移continueAbility实现的,MainAbility和MainAbilitySlice都要实现IAbilityContinuation接口 。continueAbility的介绍和使用,可以参考流转章节,下面以数据从手机流转到平板为例。
在手机上点击"迁移"按钮,调用continueAbility方法,将会调用onSaveData方法,同时会拉起平板的MainAbilitySlice,平板的MainAbilitySlice会执行onRestoreData方法,具体实现在MainAbilitySlice中,如下:
手机端在onSaveData方法中保存要流转到平板端MainAbilitySlice的数据。
- @Override
- public boolean onSaveData(IntentParams saveData) {
- String elementsString = GsonUtils.objectToString(navMap.getMapElements());
- saveData.setParam(ELEMENT_STRING, elementsString);
- saveData.setParam("stepPoint", mapManager.getStepPoint());
- LogUtils.info(TAG, "onSaveData" + navMap.getMapElements().size());
- return true;
- }
复制代码平板端MainAbilitySlice在onRestoreData方法中取出数据。
- @Override
- public boolean onRestoreData(IntentParams restoreData) {
- if (restoreData.getParam(ELEMENT_STRING) instanceof String) {
- String elementsString = (String) restoreData.getParam(ELEMENT_STRING);
- elements = GsonUtils.jsonToList(elementsString, MapElement.class);
- }
- stepPoint = (int) restoreData.getParam("stepPoint");
- LogUtils.info(TAG, "onRestoreData::elements::" + elements.size());
- return true;
- }
复制代码平板取到数据之后在initView()方法中使用该数据绘制地图。
在平板上点击"迁移"按钮,数据会以以上同样的方式从平板流转到手机。
8. 效果展示数据从手机流转到智能穿戴和平板的效果图如下:
9. 恭喜您目前您已经成功完成了Codelab并且学到了:
- 分布式任务调度。
- HarmonyOS自定义组件的基本使用。
- 位置服务的基本使用。
- IDL的基本使用。
10. 完整示例