写点什么

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

作者:莓创技术
  • 2025-06-17
    广东
  • 本文字数:9551 字

    阅读完需:约 31 分钟

鸿蒙宝藏案例详解:共享单车“丝滑”骑行体验的代码实现 🚲💻

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


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

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

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

关键代码详解 (TypeScript/ArkTS)

// 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 配置灵活:​

  2. scanTypes: 指定识别的码类型,非常灵活。

  3. enableMultiMode: 是否一次扫多个码(共享单车通常不需要,关掉更快)。

  4. enableAlbum: 是否允许从相册选择二维码图片(重要!用户可能截图扫码)。

  5. startScanForResult:​​ 这是启动扫码的核心 API,返回一个Promise.then()里处理成功结果,.catch()处理失败。

  6. ​结果处理 (result):​

  7. result.scanType: 识别出的码类型(二维码?条形码?)。

  8. result.value: 扫码得到的数据字符串(通常包含单车唯一 ID、解锁指令等)。​​这个例子简化了,实际业务中这里会解析result.value获取单车信息!​

  9. ​状态管理 (AppStorage):​​ 鸿蒙提供的应用级状态管理。这里设置CYCLING_STATUS = WAITING_UNLOCK,告诉应用“用户扫到码了,等待确认解锁”。这个状态会被解锁页面使用。

  10. 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@Componentstruct 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):​

  3. setMyLocationEnabled(true): 让地图显示用户位置蓝点。

  4. getCurrentLocation: 获取​​一次​​精确位置。对于持续追踪,需用on('locationChange')监听。

  5. ​坐标转换 (convertCoordinate):​​ ​​非常重要!​​ 设备 GPS 返回的是 WGS84 坐标,国内地图服务(如 GCJ02)需要转换才能准确显示。

  6. ​路径规划 (getWalkingRoutes):​

  7. 调用 navi.getWalkingRoutes(params) 是核心。传入起点(origins)、终点(destination)、类型(WALKING)。

  8. 返回的 RouteResult 包含路线信息,其中 overviewPolyline 是​​一串压缩过的经纬度点​​,用于绘制路线。

  9. ​绘制路线 (addPolyline):​

  10. 使用 mapController.addPolyline(options) 绘制折线。

  11. options.points 传入路线规划得到的坐标点数组 (overviewPolyline 需要先解码,示例代码假设MapUtil.walkingRoutes内部或返回结果已处理)。

  12. 通过 widthcolor 等属性定制路线外观。

  13. ​交互流程:​​ 用户点击地图 -> 获取点击点坐标 -> 清除旧数据 -> 添加新 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:​​ 构建实况窗数据的核心工具。它定义了:

  2. ​主信息 (primary):​​ 标题、内容文本/颜色、点击动作(WantAgent)、卡片布局(LayoutData)、锁屏扩展能力名/参数/图片。

  3. ​胶囊态 (capsule):​​ 状态栏显示的图标、背景色、文字。

  4. ​其他:​​ 显示时长(keepTime)、是否持久化等。

  5. ​状态管理:​​ 实况窗内容不是静态的!updateLiveView 方法根据业务状态 (RIDINGWAITING_PAYMENTPAYMENT_COMPLETED) ​​动态更新​​ liveViewData 的各个部分,然后调用 update() 或 stop() 刷新界面。

  6. WantAgent:​​ ​​实现点击交互的关键!​​ 定义了用户点击实况窗(胶囊或卡片)后要执行的动作。最常见的就是跳转回 App 的特定页面(如骑行页、支付页)。wantAgent 模块用于构建这个意图。

  7. LiveNotification:​​ 负责实况窗的生命周期管理 (createupdatestop)。.from(context, env) 将实况窗与特定的业务环境(env)关联起来。

  8. ​沉浸态锁屏实况窗 (高级):​

  9. 在 LiveViewDataBuilder 中配置 setLiveViewLockScreenAbilityName 和 setLiveViewLockScreenPicture

  10. 实现一个继承自 LiveViewLockScreenExtensionAbility 的 Ability。

  11. 在 onSessionCreate 方法中,使用 session.loadContent('你的自定义UI页面路径') 加载你用 ArkUI 编写的​​自定义锁屏卡片界面​​。这让你可以展示比默认模板更丰富的信息(比如地图缩略图、更详细的费用明细)。

  12. ​声明扩展 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. 骑行中

用户头像

莓创技术

关注

一只会打代码的羊 2020-03-20 加入

还未添加个人简介

评论

发布
暂无评论
鸿蒙5开发宝藏案例分享---快捷触达的骑行体验_莓创技术_InfoQ写作社区