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

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

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

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

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

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

关键代码详解 (TypeScript/ArkTS)

typescript 复制代码
typescript
复制
// 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):​ ​ 扫码必须的相机权限!​​必须在配置文件声明​​:

    json 复制代码
    json
    复制
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "用于扫描共享单车二维码", // 给用户看的理由
        "usedScene": {
          "abilities": ["EntryAbility"], // 在哪个Ability申请
          "when": "always" // 使用时机
        }
      }
    ],
  2. ScanOptions 配置灵活:​

    • scanTypes: 指定识别的码类型,非常灵活。
    • enableMultiMode: 是否一次扫多个码(共享单车通常不需要,关掉更快)。
    • enableAlbum: 是否允许从相册选择二维码图片(重要!用户可能截图扫码)。
  3. startScanForResult:​ ​ 这是启动扫码的核心API,返回一个Promise.then()里处理成功结果,.catch()处理失败。

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

    • result.scanType: 识别出的码类型(二维码?条形码?)。
    • result.value: 扫码得到的数据字符串(通常包含单车唯一ID、解锁指令等)。​这个例子简化了,实际业务中这里会解析result.value获取单车信息!​
  5. ​状态管理 (AppStorage):​ ​ 鸿蒙提供的应用级状态管理。这里设置CYCLING_STATUS = WAITING_UNLOCK,告诉应用"用户扫到码了,等待确认解锁"。这个状态会被解锁页面使用。

  6. router.pushUrl:​ ​ ​​实现"直达"的关键!​ ​ 直接路由导航到解锁确认页pages/ConfirmUnlock。用户瞬间从扫码界面跳到了解锁按钮面前,省去所有中间步骤。通常会把单车ID等信息通过params传递过去:router.pushUrl({ url: 'pages/ConfirmUnlock', params: { bikeId: parsedBikeId } })

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


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

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

关键代码详解 (地图初始化、定位、路径规划与绘制)

typescript 复制代码
typescript
复制
// 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):​ ​ 定位权限同样​​必须声明​​:

    json 复制代码
    json
    复制
    "requestPermissions": [
      {
        "name": "ohos.permission.LOCATION",
        "reason": "用于查找附近的共享单车和导航"
      },
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "用于更精准的找车定位"
      }
    ],
  2. MapComponent:​ ​ 地图的UI组件。mapCallback 在地图​​加载完成​ ​后触发,此时才能安全地获取mapController进行操作。

  3. ​定位流程 (enableMyLocation):​

    • setMyLocationEnabled(true): 让地图显示用户位置蓝点。
    • getCurrentLocation: 获取​一次​ 精确位置。对于持续追踪,需用on('locationChange')监听。
    • ​坐标转换 (convertCoordinate):​ ​非常重要!​ 设备GPS返回的是WGS84坐标,国内地图服务(如GCJ02)需要转换才能准确显示。
  4. ​路径规划 (getWalkingRoutes):​

    • 调用 navi.getWalkingRoutes(params) 是核心。传入起点(origins)、终点(destination)、类型(WALKING)。
    • 返回的 RouteResult 包含路线信息,其中 overviewPolyline​一串压缩过的经纬度点​,用于绘制路线。
  5. ​绘制路线 (addPolyline):​

    • 使用 mapController.addPolyline(options) 绘制折线。
    • options.points 传入路线规划得到的坐标点数组 (overviewPolyline 需要先解码,示例代码假设MapUtil.walkingRoutes内部或返回结果已处理)。
    • 通过 width, color 等属性定制路线外观。
  6. ​交互流程:​​ 用户点击地图 -> 获取点击点坐标 -> 清除旧数据 -> 添加新Marker -> 规划并绘制到该Marker的步行路线。


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

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

关键代码详解 (创建、更新、销毁实况窗)

kotlin 复制代码
typescript
复制
// 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)、是否持久化等。
  2. ​状态管理:​ ​ 实况窗内容不是静态的!updateLiveView 方法根据业务状态 (RIDING, WAITING_PAYMENT, PAYMENT_COMPLETED) ​​动态更新​liveViewData 的各个部分,然后调用 update()stop() 刷新界面。

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

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

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

    • LiveViewDataBuilder 中配置 setLiveViewLockScreenAbilityNamesetLiveViewLockScreenPicture

    • 实现一个继承自 LiveViewLockScreenExtensionAbility 的Ability。

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

    • ​声明扩展Ability (module.json5):​

      json 复制代码
      json
      复制
      "extensionAbilities": [
        {
          "name": "LiveViewLockScreenExtAbility",
          "type": "liveViewLockScreen", // 类型必须为liveViewLockScreen
          "srcEntry": "./ets/entryability/LiveViewLockScreenExtAbility.ets",
          "exported": true // 允许系统访问
        }
      ],
  6. ​服务开通:​ ​ 使用实况窗能力​​前​ ​,需要在 AppGallery Connect 后台为你的应用开通 Live View Kit 服务权益。


📌 总结与思考

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

  1. ScanUtil.scan() 被调用 -> 扫码成功 -> router.pushUrl 直达解锁页。
  2. 用户点击解锁 -> 调用 LiveViewController.startLiveView() 创建实况窗 (显示骑行中)。
  3. 骑行中
相关推荐
Ticnix25 分钟前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人28 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl32 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅35 分钟前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人43 分钟前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_1 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus1 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
玖月晴空1 小时前
探索关于Spec 和Skills 的一些实战运用-Kiro篇
前端·aigc·代码规范