【鸿蒙 5.0 实战开发】鸿蒙(出行导航类)APP开发——快捷触达的骑行体验

📝往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)

🚩 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?

🚩 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~

🚩 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?

🚩 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?

🚩 记录一场鸿蒙开发岗位面试经历~

📃 持续更新中……


简介

本场景解决方案主要面向涉及共享租赁、即时配送等场景的应用,以共享单车为案例,使用实况窗、地图导航、原生扫码等技术,为消费者的整个骑行流程带来更好的体验。

效果展示

场景说明

场景整体介绍

当前用户想要完成骑行的整个流程,需要先找到应用,再找到功能入口,骑行完成后又需要再重复一遍步骤,对于用户来说操作多,流程繁琐。如果能从首页直接扫码直到解锁界面,换车也只需一步操作即可支付将大大提升用户体验。采用Scan Kit和实况窗实现以下流程:用户扫描共享单车的二维码后,进入解锁页面,点击解锁后,会拉起实况窗显示骑行状态,后续完成还车、支付等操作后,可以实时更新实况窗的状态。

场景优势

本场景结合提供的实况窗、地图导航、扫码等原生能力,可以带给用户更加便捷高效的体验。具体优势如下:

1、使用实况窗技术帮助用户聚焦正在进行的任务,方便快速查看和即时处理。支持在锁屏、通知中心等位置显示卡片,在状态栏显示胶囊形态,以及在状态栏点击胶囊后展开悬浮卡片,让用户一眼可见重点信息。多种显示方式能够将信息即时触达到用户,避免用户反复进出应用或服务的页面。

2、基于Map Kit实现个性化地图呈现、地图搜索和路线规划等功能,手势交互方面,提供了包括缩放、旋转、移动等流畅的交互体验。

场景分析

典型场景

编号场景名称描述实现方案
1扫码解锁首页和共享单车页面均可扫码,扫码成功直达解锁页面。基于ScanKit能够快速实现扫码能力
2地图规划路径选中目的地,展示规划好的最短路径基于MapKit能够快速实现路径规划和路线绘制能力
3实况窗展示骑行状态骑行过程中,用户需要实时查看骑行状态使用实况窗,在整个骑行过程中,即使手机处于锁屏状态下,用户也能一眼可见骑行状态,无需解锁并打开应用。

场景实现

业务流程图

左图为当前骑行场景的流程图,右图是优化后的流程,与原流程对比省去寻找入口和后台应用的步骤,简化了用户的使用步骤,提升用户体验。

骑行状态图

时序图

扫码解锁

效果展示

在首页或者共享单车页面,点击扫码进入扫码界面,可以使用后置摄像头进行扫码,也可以点击图库选择二维码图片进行扫码。

时序图

主要业务流程如下:

关键点说明

1、使用 Scan Kit 实现扫码能力,Scan Kit应用了多项计算机视觉技术和AI算法技术,不仅实现了远距离自动扫码,同时还针对多种复杂扫码场景(如暗光、污损、模糊、小角度、曲面码等)做了识别优化,提升扫码成功率与用户体验。

2、申请系统相机权限,在entry模块的module.json5文件的requestPermissions字段中增加ohos.permission.CAMERA权限。

"requestPermissions": [
  // ...
  {
    "name": "ohos.permission.CAMERA",
    "reason": "$string:EntryAbility_desc",
    "usedScene": {
      "abilities": [
        "EntryAbility"
      ],
      "when": "always"
    }
  }
],

3、支持多种识码类型,常用的是二维码,也支持条形码扫描。

关键代码片段

import { scanBarcode, scanCore } from '@kit.ScanKit';
import { router } from '@kit.ArkUI';
import { CyclingConstants, CyclingStatus } from '../constants/CyclingConstants';
import { BusinessError } from '@kit.BasicServicesKit';
import Logger from './Logger';

export class ScanUtil {
  public static scan(obj: Object): void {
    let options: scanBarcode.ScanOptions = {
      scanTypes: [scanCore.ScanType.ALL,scanCore.ScanType.ONE_D_CODE],
      enableMultiMode: true,
      enableAlbum: true
    };
    try {
      scanBarcode.startScanForResult(getContext(obj), options).then((result: scanBarcode.ScanResult) => {
        Logger.info('[BicycleSharing]', 'Promise scan result: %{public}s', JSON.stringify(result));
        if (result.scanType === CyclingConstants.SCAN_TYPE) {
          AppStorage.setOrCreate(CyclingConstants.CYCLING_STATUS, CyclingStatus.WAITING_UNLOCK);
          router.pushUrl({ url: 'pages/ConfirmUnlock' });
        }
      }).catch((error: BusinessError) => {
        Logger.error(0x0001, '[BicycleSharing]', 'Promise error: %{public}s', JSON.stringify(error));
      });
    } catch (error) {
      Logger.error(0x0001, '[BicycleSharing]', 'failReason: %{public}s', JSON.stringify(error));
    }
  }
}

地图路径规划

效果展示

进入找车页面后,可以点击任意位置模拟自行车的所在地,地图将进行步行路线规划并增加标记点。

时序图

关键点说明

1、使用 Map Kit 实现地图能力,Map Kit可以帮助开发者实现个性化地图呈现、地图搜索和路线规划等功能,轻松完成地图构建工作。

2、参考文档 配置AppGallery Connect指南 去AppGallery Connect开通地图服务。注意要在工程中entry模块的module.json5文件中配置client_id。

3、启用“我的位置”之前,您需要确保您的应用可以获取用户定位。需要申请ohos.permission.LOCATION和ohos.permission.APPROXIMATELY_LOCATION权限。

关键代码片段

1、导入Map Kit

import { MapComponent, mapCommon, map } from '@kit.MapKit';

2、集成地图组件,初始化地图页面

aboutToAppear(): void {
  // initialize map
  this.callback = async (err, mapController) => {
    let hasPermissions = false;
    if (!err) {
      this.mapController = mapController;
      this.mapController.on('mapLoad', async () => {
        hasPermissions = await MapUtil.checkPermissions(this.mapController);
        if (!hasPermissions) {
          this.requestPermissions();
        }
        if (hasPermissions) {
          let requestInfo: geoLocationManager.CurrentLocationRequest = {
            'priority': geoLocationManager.LocationRequestPriority.FIRST_FIX,
            'scenario': geoLocationManager.LocationRequestScenario.UNSET,
            'maxAccuracy': 0
          };
          let locationChange = async (): Promise<void> => {
          };
          geoLocationManager.on('locationChange', requestInfo, locationChange);
          geoLocationManager.getCurrentLocation(requestInfo).then(async (result) => {
            let mapPosition: mapCommon.LatLng =
              await map.convertCoordinate(mapCommon.CoordinateType.WGS84, mapCommon.CoordinateType.GCJ02, result);
            AppStorage.setOrCreate('longitude', mapPosition.longitude);
            AppStorage.setOrCreate('latitude', mapPosition.latitude);
            let cameraPosition: mapCommon.CameraPosition = {
              target: mapPosition,
              zoom: 15,
              tilt: 0,
              bearing: 0
            };
            let cameraUpdate = map.newCameraPosition(cameraPosition);
            mapController?.animateCamera(cameraUpdate, 1000);
          })
        }
      });
      // ...
    }
  };
}

build() {
  // ...
    Column() {
      MapComponent({
        mapOptions: this.mapOption,
        mapCallback: this.callback
      })
        // ...
    }
    // ...
}

3、向用户申请授予定位权限,启动“我的位置”功能

requestPermissions(): void {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  atManager.requestPermissionsFromUser(getContext() as common.UIAbilityContext,
    ['ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION'])
    .then(() => {
      this.mapController?.setMyLocationEnabled(true);
      this.mapController?.setMyLocationControlsEnabled(true);
      this.mapController?.setCompassControlsEnabled(false);
      this.mapController?.setMyLocationStyle({ displayType: mapCommon.MyLocationDisplayType.FOLLOW });
      geoLocationManager.getCurrentLocation().then(async (result) => {
        let mapPosition: mapCommon.LatLng =
          await map.convertCoordinate(mapCommon.CoordinateType.WGS84, mapCommon.CoordinateType.GCJ02, result);
        AppStorage.setOrCreate('longitude', mapPosition.longitude);
        AppStorage.setOrCreate('latitude', mapPosition.latitude);
        let cameraPosition: mapCommon.CameraPosition = {
          target: mapPosition,
          zoom: 15,
          tilt: 0,
          bearing: 0
        };
        let cameraUpdate = map.newCameraPosition(cameraPosition);
        this.mapController?.animateCamera(cameraUpdate, 1000);
      })
    })
    .catch((err: BusinessError) => {
      Logger.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
    })
}

4、监听点击事件

this.mapController.on('mapClick', async (position) => {
  this.mapController?.clear();
  this.marker?.remove();
  let requestInfo: geoLocationManager.CurrentLocationRequest = {
    'priority': geoLocationManager.LocationRequestPriority.FIRST_FIX,
    'scenario': geoLocationManager.LocationRequestScenario.UNSET,
    'maxAccuracy': 0
  };
  let locationChange = async (location: geoLocationManager.Location): Promise<void> => {
    let wgs84Position: mapCommon.LatLng = {
      latitude: location.latitude,
      longitude: location.longitude
    };
    let gcj02Posion: mapCommon.LatLng =
      await map.convertCoordinate(mapCommon.CoordinateType.WGS84, mapCommon.CoordinateType.GCJ02,
        wgs84Position);
    this.myPosition = gcj02Posion
  };
  geoLocationManager.on('locationChange', requestInfo, locationChange);
  this.marker = await MapUtil.addMarker(position, this.mapController);
  const walkingRoutes = await MapUtil.walkingRoutes(position, this.myPosition);
  await MapUtil.paintRoute(walkingRoutes!, this.mapPolyline, this.mapController);
});

5、启动步行路径规划

public static async walkingRoutes(position: mapCommon.LatLng, myPosition?: mapCommon.LatLng) {
  let params: navi.RouteParams = {
    origins: [myPosition!],
    destination: position,
    language: 'zh_CN'
  };
  try {
    const result = await navi.getWalkingRoutes(params);
    Logger.info('naviDemo', 'getWalkingRoutes success result =' + JSON.stringify(result));
    return result;
  } catch (err) {
    Logger.error('naviDemo', 'getWalkingRoutes fail err =' + JSON.stringify(err));
  }
  return undefined;
}

6、绘制路线

public static async paintRoute(routeResult: navi.RouteResult, mapPolyline?: map.MapPolyline,
  mapController?: map.MapComponentController) {
  mapPolyline?.remove();
  let polylineOption: mapCommon.MapPolylineOptions = {
    points: routeResult.routes[0].overviewPolyline!,
    clickable: true,
    startCap: mapCommon.CapStyle.BUTT,
    endCap: mapCommon.CapStyle.BUTT,
    geodesic: false,
    jointType: mapCommon.JointType.BEVEL,
    visible: true,
    width: 20,
    zIndex: 10,
    gradient: false,
    color: 0xFF2970FF
  }
  mapPolyline = await mapController?.addPolyline(polylineOption);
}

实况窗展示骑行状态

效果展示

点击解锁后,会拉起实况窗显示骑行状态,后续完成还车、支付等操作后,可以实时更新实况窗的状态。支持在锁屏、通知中心等位置显示卡片,在状态栏显示胶囊形态,以及在状态栏点击胶囊后展开悬浮卡片,让用户一眼可见骑行状态。

时序图

关键点说明

1、使用Live View Kit实现实况窗服务,Live View Kit支持应用将订单或者服务的实时状态信息变化在设备的关键界面展示。

2、参考文档 开通实况窗权益 去AppGallery Connect开通实况窗服务。

3、此场景中只用了本地实况窗的能力,本地更新或结束实况窗依赖于您的应用进程,若业务需要,可以在本地创建实况窗后使用Push Kit远程更新或结束实况窗。

关键代码片段

1、导入Live View Kit

import { liveViewManager } from '@kit.LiveViewKit';

2、创建实况窗

public async startLiveView(context: LiveViewContext,
  liveViewEnvironment?: LiveViewEnvironment): Promise<liveViewManager.LiveViewResult> {
  // build liveView
  this.liveViewData = await LiveViewController.buildDefaultView(context);
  let env = liveViewEnvironment;
  if (!env) {
    env = {
      id: 0,
      event: 'RENT'
    };
  }
  this.liveNotification = LiveNotification.from(context, env);
  return await this.liveNotification.create(this.liveViewData);
}

private static async buildDefaultView(context: LiveViewContext) {
  const layoutData = new TextLayoutBuilder()
    .setTitle(CyclingConstants.DEFAULT_VIEW_LAYOUT_TITLE)
    .setContent(CyclingConstants.WAITING_PAYMENT_LAYOUT_CONTENT)
    .setDescPic('bike_page.png');

  const capsule = new TextCapsuleBuilder()
    .setIcon('white_bike.png')
    .setBackgroundColor(CyclingConstants.CAPSULE_COLOR)
    .setTitle(CyclingConstants.DEFAULT_VIEW_RIDING)

  const liveViewData = new LiveViewDataBuilder()
    .setTitle(CyclingConstants.DEFAULT_VIEW_RIDING)
    .setContentText(CyclingConstants.DEFAULT_VIEW_RIDING_TIME)
    .setContentColor(CyclingConstants.CONTENT_COLOR)
    .setLayoutData(layoutData)
    .setCapsule(capsule)
    .setWant(await LiveViewController.buildWantAgent(context.want))

  return liveViewData;
};

3、更新和结束实况窗

public async updateLiveView(status: number, context: LiveViewContext): Promise<liveViewManager.LiveViewResult> {
  // update liveView
  const liveViewData = this.liveViewData!;
  switch (status) {
    case CyclingStatus.WAITING_PAYMENT:
      liveViewData.primary.title = CyclingConstants.WAITING_PAYMENT_TITLE;
      liveViewData.primary.content = [
        {
          text:  CyclingConstants.WAITING_PAYMENT_CONTENT,
          textColor: CyclingConstants.CONTENT_COLOR
        }
      ];
      liveViewData.primary.clickAction = await LiveViewController.buildWantAgent(context.want);
      liveViewData.primary.layoutData = new TextLayoutBuilder()
        .setTitle(CyclingConstants.WAITING_PAYMENT_LAYOUT_TITLE)
        .setContent(CyclingConstants.WAITING_PAYMENT_LAYOUT_CONTENT)
        .setDescPic('bike_page.png');

      liveViewData.capsule = new TextCapsuleBuilder()
        .setIcon('white_bike.png')
        .setBackgroundColor(CyclingConstants.CAPSULE_COLOR)
        .setTitle(CyclingConstants.WAITING_PAYMENT_LAYOUT_TITLE)
      break;
    case CyclingStatus.PAYMENT_COMPLETED:
      liveViewData.primary.title = CyclingConstants.WAITING_PAYMENT_TITLE;
      liveViewData.primary.clickAction = await LiveViewController.buildWantAgent(context.want);
      liveViewData.primary.content = [
        {
          text: CyclingConstants.WAITING_PAYMENT_PAY,
          textColor: CyclingConstants.CONTENT_COLOR
        },
        {
          text: CyclingConstants.WAITING_PAYMENT_PAY_SUCCESS,
          textColor: CyclingConstants.CONTENT_COLOR
        }
      ];

      liveViewData.primary.layoutData = new TextLayoutBuilder()
        .setTitle(CyclingConstants.WAITING_PAYMENT_PAY_END)
        .setContent(CyclingConstants.WAITING_PAYMENT_LAYOUT_CONTENT)
        .setDescPic('bike_page.png');

      liveViewData.capsule = new TextCapsuleBuilder()
        .setIcon('white_bike.png')
        .setBackgroundColor(CyclingConstants.CAPSULE_COLOR)
        .setTitle(CyclingConstants.PAYMENT_COMPLETED_CAPSULE_TITLE)

      return await this.liveNotification!.stop(liveViewData);
    default:
      break;
  }

  return await this.liveNotification!.update(liveViewData);
}

4、开发用户自定义向沉浸态实况窗

export default class LiveViewLockScreenExtAbility extends LiveViewLockScreenExtensionAbility {
  onCreate() {
    hilog.info(0x0000, 'LiveViewLockScreenTag', 'LiveViewLockScreenExtAbility onCreate begin.');
  }

  onForeground() {
    hilog.info(0x0000, 'LiveViewLockScreenTag', 'LiveViewLockScreenExtAbility onForeground begin.');
  }

  onBackground() {
    hilog.info(0x0000, 'LiveViewLockScreenTag', 'LiveViewLockScreenExtAbility onBackground begin.');
  }

  onDestroy() {
    hilog.info(0x0000, 'LiveViewLockScreenTag', 'LiveViewLockScreenExtAbility onDestroy begin.');
  }

  onSessionCreate(_want: Want, session: UIExtensionContentSession) {
    hilog.info(0x0000, 'LiveViewLockScreenTag', 'LiveViewLockScreenExtAbility onSessionCreate begin.');
    session.loadContent('pages/LiveViewLockScreenPage');
  }

  onSessionDestroy(_session: UIExtensionContentSession) {
  }
}

5、在LiveViewDataBuilder中配置沉浸态实况窗参数

this.primary = {
  title: '',
  content: [
    {
      text: '',
      textColor: ''
    }
  ],
  keepTime: CyclingConstants.KEEP_TIME,
  clickAction: undefined,
  layoutData: undefined,
  liveViewLockScreenPicture: 'icBike.png',
  liveViewLockScreenAbilityName: 'LiveViewLockScreenExtAbility',
  liveViewLockScreenAbilityParameters: parameters
};

6、在module.json5中配置拓展的ability

"extensionAbilities": [
  {
    "name": "LiveViewLockScreenExtAbility",
    "type": "liveViewLockScreen",
    "srcEntry": "./ets/entryability/LiveViewLockScreenExtAbility.ets",
    "exported": true
  }
],
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值