鸿蒙5开发宝藏案例分享---快捷触达的骑行体验

🚲💻

大家好!上次分享了鸿蒙那个超棒的共享单车体验案例,很多朋友留言说想看代码细节。没问题!这就带大家深入代码层,看看那些“丝滑”的体验(扫码直达、实时状态窗、路径规划)到底是怎么敲出来的。官方文档有时像藏宝图,代码才是真金白银!

核心目标再强调: 用HarmonyOS的Scan Kit(扫码直达)、Map Kit(找车导航)、Live View Kit(实况窗)三大能力,把扫码->解锁->骑行->还车->支付的流程做到极简、实时、无感

模块一:扫码直达解锁页 (Scan Kit)

目标: 用户在任何地方扫码,直接跳转到该单车的解锁确认页,跳过打开App、找入口的步骤。

// 1. 导入关键模块 import scanBarcode from '@ohos.abilityAccessCtrl'; // Scan Kit核心模块 import { router } from '@kit.ArkUI'; // 页面路由模块 import { BusinessError } from '@kit.BasicServicesKit'; // 错误处理 // 2. 扫码工具类 (ScanUtil.ts) export class ScanUtil { public static scan(obj: Object): void { // 3. 配置扫码选项:支持所有类型码(ALL)和一维码(ONE_D_CODE),允许多码识别,允许从相册选图 let options: scanBarcode.ScanOptions = { scanTypes: [scanBarcode.ScanType.ALL, scanBarcode.ScanType.ONE_D_CODE], enableMultiMode: true, enableAlbum: true }; try { // 4. 启动扫码并等待结果 (异步Promise) scanBarcode.startScanForResult(getContext(obj), options) .then((result: scanBarcode.ScanResult) => { console.info('扫码结果:', JSON.stringify(result)); // 5. 关键逻辑:判断扫码类型 (假设CyclingConstants.SCAN_TYPE代表单车码) if (result.scanType === CyclingConstants.SCAN_TYPE) { // 6. 设置应用状态:等待解锁 (AppStorage是鸿蒙的状态管理) AppStorage.setOrCreate(CyclingConstants.CYCLING_STATUS, CyclingStatus.WAITING_UNLOCK); // 7. 核心跳转!直接路由到解锁确认页 'pages/ConfirmUnlock' router.pushUrl({ url: 'pages/ConfirmUnlock' }); // 通常这里会把扫码得到的数据(如单车ID)通过params传递给ConfirmUnlock页面 } }) .catch((error: BusinessError) => { console.error('扫码出错:', JSON.stringify(error)); // 处理错误:如提示用户、重试等 }); } catch (error) { console.error('启动扫码失败:', JSON.stringify(error)); } } }

代码解析 & 关键点:

  1. 权限申请 (module.json5): 扫码必须的相机权限!必须在配置文件声明
"requestPermissions": [
  {
    "name": "ohos.permission.CAMERA",
    "reason": "用于扫描共享单车二维码", // 给用户看的理由
    "usedScene": {
      "abilities": ["EntryAbility"], // 在哪个Ability申请
      "when": "always" // 使用时机
    }
  }
],
  1. ScanOptions配置灵活:
    • scanTypes: 指定识别的码类型,非常灵活。
    • enableMultiMode: 是否一次扫多个码(共享单车通常不需要,关掉更快)。
    • enableAlbum: 是否允许从相册选择二维码图片(重要!用户可能截图扫码)。
  1. startScanForResult: 这是启动扫码的核心API,返回一个Promise.then()里处理成功结果,.catch()处理失败。
  2. 结果处理 (result):
    • result.scanType: 识别出的码类型(二维码?条形码?)。
    • result.value: 扫码得到的数据字符串(通常包含单车唯一ID、解锁指令等)。这个例子简化了,实际业务中这里会解析result.value获取单车信息!
  1. 状态管理 (AppStorage): 鸿蒙提供的应用级状态管理。这里设置CYCLING_STATUS = WAITING_UNLOCK,告诉应用“用户扫到码了,等待确认解锁”。这个状态会被解锁页面使用。
  2. router.pushUrl:实现“直达”的关键! 直接路由导航到解锁确认页pages/ConfirmUnlock。用户瞬间从扫码界面跳到了解锁按钮面前,省去所有中间步骤。通常会把单车ID等信息通过params传递过去:router.pushUrl({ url: 'pages/ConfirmUnlock', params: { bikeId: parsedBikeId } })

调用时机: 在你的首页(Index)、共享单车功能页(BikePage),甚至一个桌面万能卡片(Card)的按钮点击事件里,调用ScanUtil.scan(this)即可触发扫码。


模块二:智能找车与步行导航 (Map Kit)

目标: 在“找车”页面,显示用户位置、车辆位置,并绘制步行路线。

// 1. 导入关键模块 import { MapComponent, mapCommon, map, navi } from '@kit.MapKit'; // 地图核心 import geoLocationManager from '@ohos.geoLocationManager'; // 定位管理 import abilityAccessCtrl from '@ohos.abilityAccessCtrl'; // 权限申请 import { BusinessError } from '@kit.BasicServicesKit'; // 2. 在找车页面 (FindBikePage.ets) @Entry @Component struct FindBikePage { // ... 其他状态变量 ... private mapController?: map.MapComponentController; // 地图控制器 private mapPolyline?: map.MapPolyline; // 用于绘制路线的线对象 private myPosition: mapCommon.LatLng = { latitude: 0, longitude: 0 }; // 用户位置 aboutToAppear(): void { // 3. 初始化地图回调 this.callback = async (err, mapController) => { if (!err) { this.mapController = mapController; this.mapController.on('mapLoad', async () => { // 4. 检查并申请定位权限 const hasPerm = await this.checkLocationPermissions(); if (hasPerm) { this.enableMyLocation(); // 开启定位并获取位置 } }); } }; } // 5. 检查定位权限 private async checkLocationPermissions(): Promise<boolean> { const atManager = abilityAccessCtrl.createAtManager(); try { const permissions = [ 'ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION' ]; const grantStatus = await atManager.checkAccessToken( abilityAccessCtrl.AccessTokenID.BASE, permissions ); return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED; } catch (error) { console.error('检查权限出错', error); return false; } } // 6. 申请定位权限 private requestPermissions(): void { const atManager = abilityAccessCtrl.createAtManager(); atManager.requestPermissionsFromUser( getContext(this) as common.UIAbilityContext, ['ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION'] ).then(() => { this.enableMyLocation(); // 权限获取成功,开启定位 }).catch((err: BusinessError) => { console.error('申请权限失败', err.code, err.message); }); } // 7. 开启定位并获取当前位置 private enableMyLocation(): void { if (!this.mapController) return; // 7.1 设置地图显示我的位置 this.mapController.setMyLocationEnabled(true); this.mapController.setMyLocationControlsEnabled(true); // 显示定位按钮 // 7.2 配置定位请求参数 (高精度、首次定位) let requestInfo: geoLocationManager.CurrentLocationRequest = { priority: geoLocationManager.LocationRequestPriority.FIRST_FIX, scenario: geoLocationManager.LocationRequestScenario.NAVIGATION, maxAccuracy: 50 // 精度要求(米) }; // 7.3 获取当前位置 geoLocationManager.getCurrentLocation(requestInfo) .then(async (location) => { console.info('获取到位置:', location.latitude, location.longitude); // 7.4 坐标转换 (WGS84 -> 国内常用的GCJ02) let mapPosition: mapCommon.LatLng = await map.convertCoordinate( mapCommon.CoordinateType.WGS84, mapCommon.CoordinateType.GCJ02, { latitude: location.latitude, longitude: location.longitude } ); // 7.5 存储用户位置 & 移动地图视角 this.myPosition = mapPosition; AppStorage.setOrCreate('userLat', mapPosition.latitude); AppStorage.setOrCreate('userLon', mapPosition.longitude); let cameraUpdate = map.newCameraPosition({ target: mapPosition, zoom: 16 // 放大到合适级别 }); this.mapController?.animateCamera(cameraUpdate, 1000); // 1秒动画移动到用户位置 }) .catch((err: BusinessError) => { console.error('获取位置失败', err.code, err.message); }); } // 8. 监听地图点击 (用户点选单车位置) private setupMapListeners(): void { this.mapController?.on('mapClick', async (clickedPosition: mapCommon.LatLng) => { // 8.1 清除旧标记和路线 this.mapController?.clear(); this.mapPolyline?.remove(); // 8.2 在点击位置添加一个标记 (Marker) this.marker = await MapUtil.addMarker(clickedPosition, this.mapController); // 8.3 关键!发起步行路径规划 (从用户位置this.myPosition 到 点击位置clickedPosition) const walkingRoutes = await MapUtil.walkingRoutes(clickedPosition, this.myPosition); if (walkingRoutes && walkingRoutes.routes.length > 0) { // 8.4 绘制规划好的步行路线 await MapUtil.paintRoute(walkingRoutes, this.mapPolyline, this.mapController); } }); } build() { Column() { // 9. 集成地图组件 (核心UI) MapComponent({ mapOptions: { ... }, // 地图初始配置 (中心点、缩放级别等) mapCallback: this.callback // 地图加载完成的回调 }) .onClick(() => { this.setupMapListeners(); // 通常在地图加载后设置监听 }) .width('100%') .height('100%') } } } // 10. 路径规划工具类 (MapUtil.ts) export class MapUtil { // 10.1 步行路径规划 public static async walkingRoutes( destination: mapCommon.LatLng, origin?: mapCommon.LatLng ): Promise<navi.RouteResult | undefined> { if (!origin) return undefined; let params: navi.RouteParams = { origins: [origin], // 起点数组 (这里一个) destination: destination, // 终点 type: navi.RouteType.WALKING, // 步行模式 language: 'zh_CN' // 中文结果 }; try { const result = await navi.getWalkingRoutes(params); // 调用Map Kit API console.info('步行路线规划成功', JSON.stringify(result)); return result; } catch (err) { console.error('步行路线规划失败', JSON.stringify(err)); return undefined; } } // 10.2 绘制路线到地图 public static async paintRoute( routeResult: navi.RouteResult, mapPolyline: map.MapPolyline | undefined, mapController?: map.MapComponentController ) { if (!mapController || !routeResult.routes[0]?.overviewPolyline) return; // 清除旧线 mapPolyline?.remove(); // 配置新线的样式 (蓝色,20像素宽) let polylineOption: mapCommon.MapPolylineOptions = { points: routeResult.routes[0].overviewPolyline, // 路线坐标点数组 clickable: true, width: 20, color: 0xFF2970FF, // ARGB 蓝色 zIndex: 10 }; // 添加折线到地图并保存引用 mapPolyline = await mapController.addPolyline(polylineOption); return mapPolyline; } // ... (addMarker 方法类似) ... }

代码解析 & 关键点:

  1. 权限 (module.json5): 定位权限同样必须声明
"requestPermissions": [
  {
    "name": "ohos.permission.LOCATION",
    "reason": "用于查找附近的共享单车和导航"
  },
  {
    "name": "ohos.permission.APPROXIMATELY_LOCATION",
    "reason": "用于更精准的找车定位"
  }
],
  1. MapComponent: 地图的UI组件。mapCallback 在地图加载完成后触发,此时才能安全地获取mapController进行操作。
  2. 定位流程 (enableMyLocation):
    • setMyLocationEnabled(true): 让地图显示用户位置蓝点。
    • getCurrentLocation: 获取一次精确位置。对于持续追踪,需用on('locationChange')监听。
    • 坐标转换 (convertCoordinate):非常重要! 设备GPS返回的是WGS84坐标,国内地图服务(如GCJ02)需要转换才能准确显示。
  1. 路径规划 (getWalkingRoutes):
    • 调用 navi.getWalkingRoutes(params) 是核心。传入起点(origins)、终点(destination)、类型(WALKING)。
    • 返回的 RouteResult 包含路线信息,其中 overviewPolyline 一串压缩过的经纬度点,用于绘制路线。
  1. 绘制路线 (addPolyline):
    • 使用 mapController.addPolyline(options) 绘制折线。
    • options.points 传入路线规划得到的坐标点数组 (overviewPolyline 需要先解码,示例代码假设MapUtil.walkingRoutes内部或返回结果已处理)。
    • 通过 width, color 等属性定制路线外观。
  1. 交互流程: 用户点击地图 -> 获取点击点坐标 -> 清除旧数据 -> 添加新Marker -> 规划并绘制到该Marker的步行路线。

模块三:实况窗展示骑行状态 (Live View Kit)

目标: 解锁后,在状态栏(胶囊)、通知中心、锁屏实时显示骑行状态/时长/费用;还车后变待支付;支付后结束。

// 1. 导入关键模块 import liveViewManager, { LiveViewDataBuilder, TextLayoutBuilder, TextCapsuleBuilder, LiveNotification, LiveViewContext } from '@kit.LiveViewKit'; import { BusinessError } from '@kit.BasicServicesKit'; import wantAgent from '@ohos.app.ability.wantAgent'; // 用于定义点击动作 // 2. 实况窗控制类 (LiveViewController.ts) export class LiveViewController { private liveViewData?: liveViewManager.LiveViewData; // 当前实况窗数据 private liveNotification?: LiveNotification; // 实况窗通知对象 // 3. 创建并显示实况窗 (在用户点击"解锁"后调用) public async startLiveView(context: LiveViewContext): Promise<liveViewManager.LiveViewResult> { // 3.1 构建默认的实况窗数据 (骑行中状态) this.liveViewData = await this.buildDefaultView(context); // 3.2 创建LiveNotification对象 (关联环境信息,如业务类型'RENT') let env: liveViewManager.LiveViewEnvironment = { id: 0, event: 'RENT' }; this.liveNotification = LiveNotification.from(context, env); // 3.3 创建并显示实况窗! return await this.liveNotification.create(this.liveViewData); } // 4. 构建默认骑行中状态的实况窗数据 private static async buildDefaultView(context: LiveViewContext): Promise<liveViewManager.LiveViewData> { // 4.1 构建展开态卡片布局 (锁屏/通知中心看到的卡片) const layoutData = new TextLayoutBuilder() .setTitle('骑行中') // 卡片标题 .setContent('已骑行 0 分钟') // 卡片内容 (初始0分钟) .setDescPic('bike_icon.png'); // 卡片右侧图标 // 4.2 构建胶囊态 (状态栏看到的小胶囊) const capsule = new TextCapsuleBuilder() .setIcon('bike_small.png') // 胶囊图标 .setBackgroundColor('#FF00FF00') // 胶囊背景色 (绿色) .setTitle('骑行中'); // 胶囊文字 // 4.3 构建点击动作 (点击实况窗跳转回App的骑行页面) const wantAgentInfo: wantAgent.WantAgentInfo = { wants: [ { bundleName: context.bundleName, abilityName: 'EntryAbility', parameters: { route: 'pages/RidingPage' } // 跳转到骑行页 } ], operationType: wantAgent.OperationType.START_ABILITY, requestCode: 0 }; const wantAgentObj = await wantAgent.getWantAgent(wantAgentInfo); // 4.4 构建完整的LiveViewData const liveViewData = new LiveViewDataBuilder() .setTitle('骑行中') // 主标题 .setContentText(['已骑行 0 分钟']) // 内容文本数组 (可多行) .setContentColor('#FFFFFFFF') // 内容文字颜色 (白色) .setLayoutData(layoutData) // 设置卡片布局 .setCapsule(capsule) // 设置胶囊样式 .setWant(wantAgentObj) // 设置点击动作 // (可选) 配置锁屏沉浸态扩展Ability (见后面) .setLiveViewLockScreenAbilityName('LiveViewLockScreenExtAbility') .setLiveViewLockScreenPicture('bike_lock_icon.png') .build(); // 构建完成 return liveViewData; } // 5. 更新实况窗状态 (骑行中 -> 待支付 -> 支付完成) public async updateLiveView(status: number, context: LiveViewContext): Promise<liveViewManager.LiveViewResult> { if (!this.liveViewData || !this.liveNotification) { console.error('实况窗未创建或数据为空'); return { code: -1 }; } switch (status) { case CyclingStatus.RIDING: // 骑行中 (更新计时) // ... 更新 this.liveViewData 的计时文本 (e.g., '已骑行 5 分钟') ... return await this.liveNotification.update(this.liveViewData); case CyclingStatus.WAITING_PAYMENT: // 还车成功,待支付 // 5.1 更新标题、内容、胶囊文字 this.liveViewData.primary.title = '待支付'; this.liveViewData.primary.content = [{ text: '骑行结束,点击支付', textColor: '#FFFFFFFF' }]; this.liveViewData.capsule.title = '待支付'; // 5.2 更新点击动作 (点击跳转到支付页) this.liveViewData.primary.clickAction = await this.buildWantAgent(context, 'pages/PaymentPage'); // 5.3 更新卡片布局 this.liveViewData.primary.layoutData = new TextLayoutBuilder() .setTitle('待支付') .setContent('费用:¥2.50') .setDescPic('payment_icon.png'); return await this.liveNotification.update(this.liveViewData); case CyclingStatus.PAYMENT_COMPLETED: // 支付完成 // 5.4 更新为最终状态 this.liveViewData.primary.title = '支付成功'; this.liveViewData.primary.content = [{ text: '行程已完成,感谢使用', textColor: '#FFFFFFFF' }]; this.liveViewData.capsule.title = '完成'; // 5.5 关键!停止实况窗 (显示最终状态几秒后消失) return await this.liveNotification.stop(this.liveViewData); default: return { code: -1 }; } } // ... (buildWantAgent 辅助方法) ... } // 6. 锁屏沉浸态实况窗扩展Ability (LiveViewLockScreenExtAbility.ets) import { LiveViewLockScreenExtensionAbility, UIExtensionContentSession } from '@kit.LiveViewKit'; import hilog from '@ohos.hilog'; export default class LiveViewLockScreenExtAbility extends LiveViewLockScreenExtensionAbility { onSessionCreate(want: Want, session: UIExtensionContentSession) { hilog.info(0x0000, 'LiveViewLock', '锁屏扩展Ability创建会话'); // 6.1 加载自定义的锁屏实况窗UI页面 session.loadContent('pages/LiveViewLockScreenPage'); // 这个页面你用ArkUI自己设计! } // ... (其他生命周期方法 onForeground, onBackground, onDestroy) ... }

代码解析 & 关键点:

  1. LiveViewDataBuilder: 构建实况窗数据的核心工具。它定义了:
    • 主信息 (primary): 标题、内容文本/颜色、点击动作(WantAgent)、卡片布局(LayoutData)、锁屏扩展能力名/参数/图片。
    • 胶囊态 (capsule): 状态栏显示的图标、背景色、文字。
    • 其他: 显示时长(keepTime)、是否持久化等。
  1. 状态管理: 实况窗内容不是静态的!updateLiveView 方法根据业务状态 (RIDING, WAITING_PAYMENT, PAYMENT_COMPLETED) 动态更新 liveViewData 的各个部分,然后调用 update() stop() 刷新界面。
  2. WantAgent:实现点击交互的关键! 定义了用户点击实况窗(胶囊或卡片)后要执行的动作。最常见的就是跳转回App的特定页面(如骑行页、支付页)。wantAgent 模块用于构建这个意图。
  3. LiveNotification: 负责实况窗的生命周期管理 (create, update, stop)。.from(context, env) 将实况窗与特定的业务环境(env)关联起来。
  4. 沉浸态锁屏实况窗 (高级):
    • LiveViewDataBuilder 中配置 setLiveViewLockScreenAbilityName setLiveViewLockScreenPicture
    • 实现一个继承自 LiveViewLockScreenExtensionAbility 的Ability。
    • onSessionCreate 方法中,使用 session.loadContent('你的自定义UI页面路径') 加载你用ArkUI编写的自定义锁屏卡片界面。这让你可以展示比默认模板更丰富的信息(比如地图缩略图、更详细的费用明细)。
    • 声明扩展Ability (module.json5):
"extensionAbilities": [
  {
    "name": "LiveViewLockScreenExtAbility",
    "type": "liveViewLockScreen", // 类型必须为liveViewLockScreen
    "srcEntry": "./ets/entryability/LiveViewLockScreenExtAbility.ets",
    "exported": true // 允许系统访问
  }
],
  1. 服务开通: 使用实况窗能力,需要在 AppGallery Connect 后台为你的应用开通 Live View Kit 服务权益。

总结与思考

把这三块核心代码串起来,就构成了那个“丝滑”骑行体验的骨架:

  1. ScanUtil.scan() 被调用 -> 扫码成功 -> router.pushUrl 直达解锁页。
  2. 用户点击解锁 -> 调用 LiveViewController.startLiveView() 创建实况窗 (显示骑行中)。
  3. 骑行中
posted @ 2025-06-12 11:39  woo爷说前端  阅读(29)  评论(0)    收藏  举报