鸿蒙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. 骑行中
相关推荐
Aphasia31112 分钟前
模式验证库——zod
前端·react.js
lexiangqicheng1 小时前
es6+和css3新增的特性有哪些
前端·es6·css3
拉不动的猪2 小时前
都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理
前端·javascript·vue.js
烛阴2 小时前
Python枚举类Enum超详细入门与进阶全攻略
前端·python
孟孟~2 小时前
npm run dev 报错:Error: error:0308010C:digital envelope routines::unsupported
前端·npm·node.js
孟孟~2 小时前
npm install 报错:npm error: ...node_modules\deasync npm error command failed
前端·npm·node.js
狂炫一碗大米饭2 小时前
一文打通TypeScript 泛型
前端·javascript·typescript
wh_xia_jun2 小时前
在 Spring Boot 中使用 JSP
java·前端·spring boot
二十雨辰2 小时前
[HTML5]快速掌握canvas
前端·html
tingkeiii3 小时前
【react+antd+vite】优雅的引入svg和阿里巴巴图标
前端·react.js·前端框架