当前位置:   article > 正文

鸿蒙(OpenHarmony)开发实战:WiFi扫码自动配网_鸿蒙wifi配网

鸿蒙wifi配网

背景

随着移动互联网的发展,WiFi已成为人们生活中不可或缺的网络接入方式。但在连接WiFi时,用户常需要手动输入一个复杂的密钥,这带来了一定的不便。针对这一痛点,利用QR码连接WiFi的方案应运而生。QR码连接WiFi的工作流程是:商家或公共场所提供含有WiFi密钥的QR码,用户只需使用手机扫一扫即可读取密钥信息并连接WiFi,无需手动输入,这种连接方式大大简化了用户的操作。随着智能手机摄像头识别能力的提升,以及用户需求的引领,利用QR码连接WiFi的方式未来还将得到更广泛的应用,为用户提供更稳定便捷的上网体验。它利用了移动互联网时代的技术优势,解决了传统WiFi连接中的痛点,是一种值得推广的网络连接方式。

效果

页面截图

配网连接中配网连接成功配网连接失败

图片

图片

图片

优势

使用QR码连接WiFi具有以下优势:

  1. 提高了连接成功率,避免因手动输入密钥错误导致的连接失败问题。

  2. 加快了连接速度,扫码相对于手动输入更高效方便。

  3. 提升了用户体验,无需记忆和输入复杂密钥,操作更人性化。

  4. 方便密钥分享和更改,通过更新QR码即可实现。

  5. 在一些需要频繁连接不同WiFi的场景下尤其便利,如酒店、餐厅、机场等。

  6. 一些App可以自动识别WiFi二维码,实现零点击连接。

开发与实现

开发环境

开发平台:windows10、DevEco Studio 3.1 Release 

系统:OpenHarmony 3.2 Release,API9(Full SDK 3.2.11.9) 

设备:SD100(工业平板设备、平台:RK3568、屏幕像素:1920 * 1200)

项目开发

需求分析

1、支持相机扫码,并可以解析二维码信息;

2、获取二维码中的wifi连接信息,自动完成网络连接

3、网络连接成功,则提示用户成功;

4、网络连接失败,则提示用户失败,可以重新连接;

5、UI界面符合OpenHarmony设计原则,应用界面简洁高效、自然流畅。

项目流程图

图片

界面

说明:从需求上分析,可以有两个界面,一是扫码界面、二是wifi连接等待和显示结果界面。

图片

详细开发

一、创建项目

说明:通过DevEco Studio创建一个OpenHarmony的项目。

图片

图片

二、申请权限

说明:在应用中涉及到使用相机和wifi的操作,需要动态申请一些必要的权限,我们可以在 EntryAbility.ts中实现,EntryAbility.ts继承UIAbility,用于管理应用的生面周期,在OnCreate是实例冷启动时触发,在此函数中实现权限申请。具体代码如下:

  1. let permissionList: Array<Permissions> = [
  2.   "ohos.permission.GET_WIFI_INFO",
  3.   "ohos.permission.INTERNET",
  4.   'ohos.permission.CAMERA',
  5.   'ohos.permission.READ_MEDIA',
  6.   'ohos.permission.WRITE_MEDIA',
  7.   'ohos.permission.MEDIA_LOCATION',
  8.   'ohos.permission.LOCATION',
  9.   'ohos.permission.APPROXIMATELY_LOCATION'
  10. ]
  11. onCreate(want, launchParam) {
  12.   hilog.info(0x0000'testTag''%{public}s''Ability onCreate');
  13.   this.requestPermissions()
  14. }
  15. private requestPermissions() {
  16.   let AtManager = abilityAccessCtrl.createAtManager()
  17.   AtManager.requestPermissionsFromUser(this.context, permissionList).then(async (data=> {
  18.     Logger.info(`${TAG} data permissions: ${JSON.stringify(data.permissions)}`)
  19.     Logger.info(`${TAG} data authResult: ${JSON.stringify(data.authResults)}`)
  20.     // 判断授权是否完成
  21.     let resultCount: number = 0
  22.     for (let result of data.authResults) {
  23.       if (result === 0) {
  24.         resultCount += 1
  25.       }
  26.     }
  27.     let permissionResult : boolean = false
  28.     if (resultCount === permissionList.length) {
  29.       permissionResult = true
  30.     }
  31.     AppStorage.SetOrCreate(KEY_IS_PERMISSION, true)
  32.     this.sendPermissionResult(permissionResult)
  33.   })
  34. }
  35. sendPermissionResult(result : boolean) {
  36.   let eventData: emitter.EventData = {
  37.     data: {
  38.       "result": result
  39.     }
  40.   };
  41.   let innerEvent: emitter.InnerEvent = {
  42.     eventId: EVENT_PERMISSION_ID,
  43.     priority: emitter.EventPriority.HIGH
  44.   };
  45.   emitter.emit(innerEvent, eventData);
  46.   Logger.info(`${TAG} sendPermissionResult`)
  47. }
  48. onDestroy() {
  49.   Logger.info(`${TAG} onDestroy`)
  50.   emitter.off(EVENT_PERMISSION_ID)
  51. }

代码解析

1、在应用中使用到相机和操作wifi需要根据需要动态申请相关权限,具体的权限用途可以查看:应用权限列表

2、应用动态授权需要使用到@ohos.abilityAccessCtrl (程序访问控制管理),通过abilityAccessCtrl.createAtManager()获取到访问控制对象 AtManager。

3、通过AtManager.requestPermissionsFromUser() 拉起请求用户授权弹窗,由用户动态授权。

4、授权成功后通过Emitter(@ohos.events.emitter)向主界面发送授权结果。

5、在onDestroy()应用退出函数中取消Emitter事件订阅。

三、首页

说明:首页即为扫码页面,用于识别二维码获取二维码信息,为网络连接准备。所以此页面有有个功能,加载相机和识别二维码。

媒体相机

相机的启动借鉴社区提供的代码案例:二维码扫码

  • 相机功能在CameraServices中,源码参考CameraServices.ets

  • 获取相机实例使用到媒体相机接口@ohos.multimedia.camera (相机管理)。

  • 首先使用camera.getCameraManager方法获取相机管理器,然后使用cameraManager.getSupportedCameras方法得到设备列表, 这里默认点亮列表中的首个相机;

  • 打开相机:使用 cameraManager.createCameraInput方法创建CameraInput实例,调用open方法打开相机;

  • 获取相机输出流:使用getSupportedOutputCapability查询相机设备在模式下支持的输出能力,然后使用createPreviewOutput创建相机输出流。

  • 获取拍照输出流,使用@ohos.multimedia.image接口的 createImageReceiver 方法创建ImageReceiver实例,并通过其getReceivingS_urfaceId()获取S_urfaceId,通过CameraManager.createPhotoOutput()函数构建拍照输出流,并将imageReceive 的 S_urfaceId与其建立绑定关系。

  • 获取相片输出:首先使用createCaptureSession方法创建捕获会话的实例,然后使用beginConfig方法配置会话,接下来使用addInput方法添加一个摄像头输入流,使用addOutput添加一个摄像头和相机照片的输出流,使用commitConfig方法提交会话配置后,调用会话的start方法开始捕获相片输出。

  • 这里也可以使用相机预览流获取图像数据,但在界面上需要预览,所以这里需要构建两条预览流,一条预览流用于显示,在XComponent组件中渲染,另外一条预览流用于获取头像数据用于解析,根据实践发现,开启两条预览流后,相机帧率为:7fsp,表现为预览卡顿,所以为提升预览效果,使用定时拍照的方式获取图像数据。

  • 获取图像的在SaveCameraAsset.ets中实现,扫码页面启动后每间隔1.5s调用PhotoOutput.capture()实现拍照,通过imageReceiver.on('imageArrival')接收图片,使用imageReceiver.readNextImage()获取图像对象,通过Image.getComponent()获取图像缓存数据。

具体实现代码:

CameraService
  1. import camera from '@ohos.multimedia.camera';
  2. import image from '@ohos.multimedia.image';
  3. import SaveCameraAsset from './SaveCameraAsset'
  4. import { QRCodeScanConst, SCAN_TYPE } from './QRCodeScanConst'
  5. import { Logger } from '@ohos/common'
  6. import common from '@ohos.app.ability.common'
  7. let TAG: string = 'CameraService'
  8. /**
  9.  * 拍照保存图片回调
  10.  */
  11. export interface FunctionCallBack {
  12.   onCaptureSuccess(thumbnail: image.PixelMap, resourceUri: string): void
  13.   onCaptureFailure(): void
  14.   onRecordSuccess(thumbnail: image.PixelMap): void
  15.   onRecordFailure(): void
  16.   /**
  17.    * 缩略图
  18.    */
  19.   thumbnail(thumbnail: image.PixelMap): void
  20.   /**
  21.    * AI 识别结果
  22.    * @param result 识别结果
  23.    */
  24.   aiResult(result: string): void
  25. }
  26. export interface PreviewCallBack {
  27.   onFrameStart()
  28.   onFrameEnd()
  29. }
  30. export interface MetaDataCallBack {
  31.   onRect(rect: camera.Rect)
  32. }
  33. export default class CameraService {
  34.   private static instance: CameraService = null
  35.   private mCameraManager: camera.CameraManager = null
  36.   private mCameraCount: number = 0 // 相机总数
  37.   private mCameraMap: Map<string, Array<camera.CameraDevice>> = new Map()
  38.   private mCurCameraDevice: camera.CameraDevice = null
  39.   private mCameraInput: camera.CameraInput = null
  40.   private mPreviewOutput: camera.PreviewOutput = null
  41.   private mPreviewOutputByImage: camera.PreviewOutput = null
  42.   private mPhotoOutput: camera.PhotoOutput = null
  43.   private mSaveCameraAsset: SaveCameraAsset = new SaveCameraAsset()
  44.   private mCaptureSession: camera.CaptureSession
  45.   private mMetadataOutput: camera.MetadataOutput
  46.   private constructor() {
  47.   }
  48.   /**
  49.    * 单例
  50.    */
  51.   public static getInstance(): CameraService {
  52.     if (this.instance === null) {
  53.       this.instance = new CameraService()
  54.     }
  55.     return this.instance
  56.   }
  57.   /**
  58.    * 初始化
  59.    */
  60.   public async initCamera(): Promise<number> {
  61.     Logger.info(`${TAG} initCamera`)
  62.     if (this.mCameraManager === null) {
  63.       this.mCameraManager = camera.getCameraManager(AppStorage.Get('context'))
  64.       // 注册监听相机状态变化
  65.       this.mCameraManager.on('cameraStatus', (cameraStatusInfo) => {
  66.         Logger.info(`${TAG} camera Status: ${JSON.stringify(cameraStatusInfo)}`)
  67.       })
  68.       // 获取相机列表
  69.       let cameras: Array<camera.CameraDevice> = this.mCameraManager.getSupportedCameras()
  70.       if (cameras) {
  71.         this.mCameraCount = cameras.length
  72.         Logger.info(`${TAG} mCameraCount: ${this.mCameraCount}`)
  73.         if (this.mCameraCount === 0) {
  74.           return this.mCameraCount
  75.         }
  76.         for (let i = 0; i < cameras.length; i++) {
  77.           Logger.info(`${TAG} --------------Camera Info-------------`)
  78.           const tempCameraId: string = cameras[i].cameraId
  79.           Logger.info(`${TAG} camera_id: ${tempCameraId}`)
  80.           Logger.info(`${TAG} cameraPosition: ${cameras[i].cameraPosition}`)
  81.           Logger.info(`${TAG} cameraType: ${cameras[i].cameraType}`)
  82.           const connectionType = cameras[i].connectionType
  83.           Logger.info(`${TAG} connectionType: ${connectionType}`)
  84.           // 判断本地相机还是远程相机
  85.           if (connectionType === camera.ConnectionType.CAMERA_CONNECTION_BUILT_IN) {
  86.             // 本地相机
  87.             this.displayCameraDevice(QRCodeScanConst.LOCAL_DEVICE_ID, cameras[i])
  88.           } else if (connectionType === camera.ConnectionType.CAMERA_CONNECTION_REMOTE) {
  89.             // 远程相机 相机ID格式 :deviceID__Camera_cameraID 例如:3c8e510a1d0807ea51c2e893029a30816ed940bf848754749f427724e846fab7__Camera_lcam001
  90.             const cameraKey: string = tempCameraId.split('__Camera_')[0]
  91.             Logger.info(`${TAG} cameraKey: ${cameraKey}`)
  92.             this.displayCameraDevice(cameraKey, cameras[i])
  93.           }
  94.         }
  95.         // todo test 选择首个相机
  96.         this.mCurCameraDevice = cameras[0]
  97.         Logger.info(`${TAG} mCurCameraDevice: ${this.mCurCameraDevice.cameraId}`)
  98.       }
  99.     }
  100.     return this.mCameraCount
  101.   }
  102.   /**
  103.    * 处理相机设备
  104.    * @param key
  105.    * @param cameraDevice
  106.    */
  107.   private displayCameraDevice(keystring, cameraDevice: camera.CameraDevice) {
  108.     Logger.info(`${TAG} displayCameraDevice ${key}`)
  109.     if (this.mCameraMap.has(key&& this.mCameraMap.get(key)?.length > 0) {
  110.       Logger.info(`${TAG} displayCameraDevice has mCameraMap`)
  111.       // 判断相机列表中是否已经存在此相机
  112.       let isExist: boolean = false
  113.       for (let item of this.mCameraMap.get(key)) {
  114.         if (item.cameraId === cameraDevice.cameraId) {
  115.           isExist = true
  116.           break
  117.         }
  118.       }
  119.       // 添加列表中没有的相机
  120.       if (!isExist) {
  121.         Logger.info(`${TAG} displayCameraDevice not exist , push ${cameraDevice.cameraId}`)
  122.         this.mCameraMap.get(key).push(cameraDevice)
  123.       } else {
  124.         Logger.info(`${TAG} displayCameraDevice has existed`)
  125.       }
  126.     } else {
  127.       let cameras: Array<camera.CameraDevice> = []
  128.       Logger.info(`${TAG} displayCameraDevice push ${cameraDevice.cameraId}`)
  129.       cameras.push(cameraDevice)
  130.       this.mCameraMap.set(key, cameras)
  131.     }
  132.   }
  133.  /**
  134.    * 创建相机输入流
  135.    * @param cameraIndex 相机下标
  136.    * @param deviceId 设备ID
  137.    */
  138.   public async createCameraInput(cameraIndex?: number, deviceId?: string) {
  139.     Logger.info(`${TAG} createCameraInput`)
  140.     if (this.mCameraManager === null) {
  141.       Logger.error(`${TAG} mCameraManager is null`)
  142.       return
  143.     }
  144.     if (this.mCameraCount <= 0) {
  145.       Logger.error(`${TAG} not camera device`)
  146.       return
  147.     }
  148.     if (this.mCameraInput) {
  149.       this.mCameraInput.close()
  150.     }
  151.     if (deviceId && this.mCameraMap.has(deviceId)) {
  152.       if (cameraIndex < this.mCameraMap.get(deviceId)?.length) {
  153.         this.mCurCameraDevice = this.mCameraMap.get(deviceId)[cameraIndex]
  154.       } else {
  155.         this.mCurCameraDevice = this.mCameraMap.get(deviceId)[0]
  156.       }
  157.     }
  158.     Logger.info(`${TAG} mCurCameraDevice: ${this.mCurCameraDevice?.cameraId}`)
  159.     try {
  160.       this.mCameraInput = this.mCameraManager.createCameraInput(this.mCurCameraDevice)
  161.       Logger.info(`${TAG} mCameraInput: ${JSON.stringify(this.mCameraInput)}`)
  162.       this.mCameraInput.on('error', this.mCurCameraDevice, (error=> {
  163.         Logger.error(`${TAG} CameraInput error: ${JSON.stringify(error)}`)
  164.       })
  165.       await this.mCameraInput.open()
  166.     } catch (err) {
  167.       if (err) {
  168.         Logger.error(`${TAG} failed to createCameraInput`)
  169.       }
  170.     }
  171.   }
  172.   /**
  173.    * 释放相机输入流
  174.    */
  175.   public async releaseCameraInput() {
  176.     Logger.info(`${TAG} releaseCameraInput`)
  177.     if (this.mCameraInput) {
  178.       try {
  179.         await this.mCameraInput.close()
  180.         Logger.info(`${TAG} releaseCameraInput closed`)
  181.       } catch (err) {
  182.         Logger.error(`${TAG} releaseCameraInput ${err}}`)
  183.       }
  184.       this.mCameraInput = null
  185.     }
  186.   }
  187.   /**
  188.    * 创建相机预览输出流
  189.    */
  190.   public async createPreviewOutput(s_urfaceId: string, callback?: PreviewCallBack) {
  191.     Logger.info(`${TAG} createPreviewOutput s_urfaceId ${s_urfaceId}`)
  192.     if (this.mCameraManager === null) {
  193.       Logger.error(`${TAG} createPreviewOutput mCameraManager is null`)
  194.       return
  195.     }
  196.     // 获取当前相机设备支持的输出能力
  197.     let cameraOutputCap = this.mCameraManager.getSupportedOutputCapability(this.mCurCameraDevice)
  198.     if (!cameraOutputCap) {
  199.       Logger.error(`${TAG} createPreviewOutput getSupportedOutputCapability error}`)
  200.       return
  201.  }
  202.     Logger.info(`${TAG} createPreviewOutput cameraOutputCap ${JSON.stringify(cameraOutputCap)}`)
  203.     let previewProfilesArray = cameraOutputCap.previewProfiles
  204.     let previewProfiles: camera.Profile
  205.     if (!previewProfilesArray || previewProfilesArray.length <= 0) {
  206.       Logger.error(`${TAG} createPreviewOutput previewProfilesArray error}`)
  207.       previewProfiles = {
  208.         format1,
  209.         size: {
  210.           width: QRCodeScanConst.DEFAULT_WIDTH,
  211.           height: QRCodeScanConst.DEFAULT_HEIGHT
  212.         }
  213.       }
  214.     } else {
  215.       Logger.info(`${TAG} createPreviewOutput previewProfile length ${previewProfilesArray.length}`)
  216.       previewProfiles = previewProfilesArray[0]
  217.     }
  218.     Logger.info(`${TAG} createPreviewOutput previewProfile[0] ${JSON.stringify(previewProfiles)}`)
  219.     try {
  220.       this.mPreviewOutput = this.mCameraManager.createPreviewOutput(previewProfiles, s_urfaceId)
  221.       Logger.info(`${TAG} createPreviewOutput success`)
  222.       // 监听预览帧开始
  223.       this.mPreviewOutput.on('frameStart', () => {
  224.         Logger.info(`${TAG} createPreviewOutput camera frame Start`)
  225.         if (callback) {
  226.           callback.onFrameStart()
  227.         }
  228.       })
  229.       this.mPreviewOutput.on('frameEnd', () => {
  230.         Logger.info(`${TAG} createPreviewOutput camera frame End`)
  231.         if (callback) {
  232.           callback.onFrameEnd()
  233.         }
  234.       })
  235.       this.mPreviewOutput.on('error', (error=> {
  236.         Logger.error(`${TAG} createPreviewOutput error: ${error}`)
  237.       })
  238.     } catch (err) {
  239.       Logger.error(`${TAG} failed to createPreviewOutput ${err}`)
  240.     }
  241.   }
  242.   /**
  243.    *  释放预览输出流
  244.    */
  245.   public async releasePreviewOutput() {
  246.     Logger.info(`${TAG} releaseCamera PreviewOutput`)
  247.     if (this.mPreviewOutput) {
  248.       await this.mPreviewOutput.release()
  249.       Logger.info(`${TAG} releaseCamera PreviewOutput release`)
  250.       this.mPreviewOutput = null
  251.     }
  252.   }
  253.   /**
  254.    * 创建拍照输出流
  255.    */
  256.   public async createPhotoOutput(functionCallback: FunctionCallBack) {
  257.     Logger.info(`${TAG} createPhotoOutput`)
  258.     if (!this.mCameraManager) {
  259.       Logger.error(`${TAG} createPhotoOutput mCameraManager is null`)
  260.       return
  261.     }
  262.     // 通过宽、高、图片格式、容量创建ImageReceiver实例
  263.     const receiver: image.ImageReceiver = image.createImageReceiver(QRCodeScanConst.DEFAULT_WIDTH, QRCodeScanConst.DEFAULT_HEIGHT, image.ImageFormat.JPEG, 8)
  264.     const imageS_urfaceId: string = await receiver.getReceivingS_urfaceId()
  265.     Logger.info(`${TAG} createPhotoOutput imageS_urfaceId: ${imageS_urfaceId}`)
  266.     let cameraOutputCap = this.mCameraManager.getSupportedOutputCapability(this.mCurCameraDevice)
  267.     Logger.info(`${TAG} createPhotoOutput cameraOutputCap ${cameraOutputCap}`)
  268.     if (!cameraOutputCap) {
  269.       Logger.error(`${TAG} createPhotoOutput getSupportedOutputCapability error}`)
  270.       return
  271.     }
  272.  let photoProfilesArray = cameraOutputCap.photoProfiles
  273.     let photoProfiles: camera.Profile
  274.     if (!photoProfilesArray || photoProfilesArray.length <= 0) {
  275.       // 使用自定义的配置
  276.       photoProfiles = {
  277.         format: camera.CameraFormat.CAMERA_FORMAT_JPEG,
  278.         size: {
  279.           width: QRCodeScanConst.DEFAULT_WIDTH,
  280.           height: QRCodeScanConst.DEFAULT_HEIGHT
  281.         }
  282.       }
  283.     } else {
  284.       Logger.info(`${TAG} createPhotoOutput photoProfile length ${photoProfilesArray.length}`)
  285.       photoProfiles = photoProfilesArray[0]
  286.     }
  287.     Logger.info(`${TAG} createPhotoOutput photoProfile ${JSON.stringify(photoProfiles)}`)
  288.     try {
  289.       this.mPhotoOutput = this.mCameraManager.createPhotoOutput(photoProfiles, imageS_urfaceId)
  290.       Logger.info(`${TAG} createPhotoOutput mPhotoOutput success`)
  291.       // 保存图片
  292.       this.mSaveCameraAsset.saveImage(receiver, functionCallback)
  293.     } catch (err) {
  294.       Logger.error(`${TAG} createPhotoOutput failed to createPhotoOutput ${err}`)
  295.     }
  296.   }
  297.   /**
  298.    * 释放拍照输出流
  299.    */
  300.   public async releasePhotoOutput() {
  301.     Logger.info(`${TAG} releaseCamera PhotoOutput`)
  302.     if (this.mPhotoOutput) {
  303.       await this.mPhotoOutput.release()
  304.       Logger.info(`${TAG} releaseCamera PhotoOutput release`)
  305.       this.mPhotoOutput = null
  306.     }
  307.   }
  308.   public async createSession() {
  309.     Logger.info(`${TAG} createSession`)
  310.     this.mCaptureSession = await this.mCameraManager.createCaptureSession()
  311.     Logger.info(`${TAG} createSession mCaptureSession ${this.mCaptureSession}`)
  312.     this.mCaptureSession.on('error', (error=> {
  313.       Logger.error(`${TAG} CaptureSession error ${JSON.stringify(error)}`)
  314.     })
  315.     try {
  316.       this.mCaptureSession?.beginConfig()
  317.       this.mCaptureSession?.addInput(this.mCameraInput)
  318.       if (this.mPreviewOutputByImage != null) {
  319.         Logger.info(`${TAG} createSession addOutput PreviewOutputByImage`)
  320.         this.mCaptureSession?.addOutput(this.mPreviewOutputByImage)
  321.       }
  322.       if (this.mPreviewOutput != null) {
  323.         Logger.info(`${TAG} createSession addOutput PreviewOutput`)
  324.         this.mCaptureSession?.addOutput(this.mPreviewOutput)
  325.       }
  326.       if (this.mPhotoOutput != null) {
  327.         Logger.info(`${TAG} createSession addOutput PhotoOutput`)
  328.         this.mCaptureSession?.addOutput(this.mPhotoOutput)
  329.       }
  330.       if (this.mMetadataOutput != null) {
  331.         Logger.info(`${TAG} createSession addOutput mMetadataOutput`)
  332.   this.mCaptureSession?.addOutput(this.mMetadataOutput)
  333.       }
  334.     } catch (err) {
  335.       if (err) {
  336.         Logger.error(`${TAG} createSession beginConfig fail err:${JSON.stringify(err)}`)
  337.       }
  338.     }
  339.     try {
  340.       await this.mCaptureSession?.commitConfig()
  341.     } catch (err) {
  342.       if (err) {
  343.         Logger.error(`${TAG} createSession commitConfig fail err:${JSON.stringify(err)}`)
  344.       }
  345.     }
  346.     try {
  347.       await this.mCaptureSession?.start()
  348.     } catch (err) {
  349.       if (err) {
  350.         Logger.error(`${TAG} createSession start fail err:${JSON.stringify(err)}`)
  351.       }
  352.     }
  353.     if (this.mMetadataOutput) {
  354.       this.mMetadataOutput.start().then(() => {
  355.         Logger.info(`${TAG} Callback returned with metadataOutput started`)
  356.       }).catch((err) => {
  357.         Logger.error(`${TAG} Failed to metadataOutput start ${err.code}`)
  358.       })
  359.     }
  360.     Logger.info(`${TAG} createSession mCaptureSession start`)
  361.   }
  362.   public async releaseSession() {
  363.     Logger.info(`${TAG} releaseCamera Session`)
  364.     if (this.mCaptureSession) {
  365.       await this.mCaptureSession.release()
  366.       Logger.info(`${TAG} releaseCamera Session release`)
  367.       this.mCaptureSession = null
  368.     }
  369.   }
  370.   /**
  371.    * 拍照
  372.    */
  373.   public async takePicture() {
  374.     Logger.info(`${TAG} takePicture`)
  375.     if (!this.mCaptureSession) {
  376.       Logger.info(`${TAG} takePicture session is release`)
  377.       return
  378.     }
  379.     if (!this.mPhotoOutput) {
  380.       Logger.info(`${TAG} takePicture mPhotoOutput is null`)
  381.       return
  382.     }
  383.     try {
  384.       const photoCaptureSetting: camera.PhotoCaptureSetting = {
  385.         quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
  386.         rotation: camera.ImageRotation.ROTATION_0,
  387.         location: {
  388.           latitude: 0,
  389.           longitude: 0,
  390.           altitude: 0
  391.         },
  392.         mirror: false
  393.       }
  394.       await this.mPhotoOutput.capture(photoCaptureSetting)
  395.     } catch (err) {
  396.       Logger.error(`${TAG} takePicture err:${JSON.stringify(err)}`)
  397.     }
  398.   }
  399.   /**
  400. * 获取设备的相机列表
  401.    * @param deviceId 设备ID
  402.    */
  403.   public getDeviceCameras(deviceId: string): Array<camera.CameraDevice> {
  404.     Logger.info(`${TAG} getDeviceCameras ${deviceId} size ${this.mCameraMap.size}`)
  405.     return this.mCameraMap.get(deviceId)
  406.   }
  407.   public getCameraCount(): number {
  408.     return this.mCameraCount
  409.   }
  410.   /**
  411.    * 释放相机
  412.    */
  413.   public async releaseCamera(): Promise<boolean> {
  414.     Logger.info(`${TAG} releaseCamera`)
  415.     let result: boolean = false
  416.     let tempStartTime: number = new Date().getTime()
  417.     try {
  418.       await this.releaseCameraInput()
  419.       await this.releasePhotoOutput()
  420.       await this.releasePreviewOutput()
  421.       await this.releaseSession()
  422.       result = true
  423.     } catch (err) {
  424.       Logger.error(`${TAG} releaseCamera fail ${JSON.stringify(err)}`)
  425.     }
  426.     let tempTime: number = new Date().getTime() - tempStartTime
  427.     Logger.info(`${TAG} releaseCamera finish time: ${tempTime}`)
  428.     return result
  429.   }
  430.   public async selectPic() {
  431.     Logger.info("getSingleImageFromAlbum start")
  432.     let context = AppStorage.Get('context'as common.UIAbilityContext
  433.     let abilityResult = await context.startAbilityForResult({
  434.       bundleName: 'com.ohos.photos',
  435.       abilityName: 'com.ohos.photos.MainAbility',
  436.       parameters: {
  437.         uri: 'singleselect' // 只选取单个文件
  438.       }
  439.     })
  440.     if (abilityResult.want === null || abilityResult.want === undefined) {
  441.       Logger.info("getSingleImageFromAlbum end. abilityResult.want is null.")
  442.       return null
  443.     }
  444.     if (abilityResult.want.parameters === null || abilityResult.want.parameters === undefined) {
  445.       Logger.info("getSingleImageFromAlbum end. abilityResult.want.parameters is null.")
  446.       return null
  447.     }
  448.     let images = abilityResult.want.parameters['select-item-list']
  449.     let imageUri = images[0]
  450.     Logger.info("getSingleImageFromAlbum end. uri:" + imageUri)
  451.     return imageUri
  452.   }
  453. }
SaveCameraAsset
  1. import image from '@ohos.multimedia.image'
  2. import { FunctionCallBack } from './CameraService'
  3. import { Logger } from '@ohos/common'
  4. import CodeRuleUtil from '../utils/CodeRuleUtil'
  5. const TAG: string = 'SaveCameraAsset'
  6. /**
  7.  * 保存相机拍照的资源
  8.  */
  9. export default class SaveCameraAsset {
  10.   constructor() {
  11.   }
  12.   /**
  13.    *  保存拍照图片
  14.    * @param imageReceiver 图像接收对象
  15.    * @param thumbWidth 宽度
  16.    * @param thumbHeight 高度
  17.    * @param callback 回调
  18.    */
  19.   public saveImage(imageReceiver: image.ImageReceiver, callback: FunctionCallBack) {
  20.     console.info(`${TAG} saveImage`)
  21.     let buffer = new ArrayBuffer(4096)
  22.     const imgWidth: number = imageReceiver.size.width
  23.     const imgHeight: number = imageReceiver.size.height
  24.     Logger.info(`${TAG} saveImage size ${JSON.stringify(imageReceiver.size)}`)
  25.     // 接收图片回调
  26.     imageReceiver.on('imageArrival', async () => {
  27.       console.info(`${TAG} saveImage ImageArrival`)
  28.       // 使用当前时间命名
  29.       imageReceiver.readNextImage((err, imageObj: image.Image) => {
  30.         if (imageObj === undefined) {
  31.           Logger.error(`${TAG} saveImage failed to get valid image error = ${err}`)
  32.           return
  33.         }
  34.         // 根据图像的组件类型从图像中获取组件缓存 4-JPEG类型
  35.         imageObj.getComponent(image.ComponentType.JPEG, async (errMsg, imgComponent) => {
  36.           if (imgComponent === undefined) {
  37.             Logger.error(`${TAG} getComponent failed to get valid buffer error = ${errMsg}`)
  38.             return
  39.           }
  40.           if (imgComponent.byteBuffer) {
  41.             Logger.info(`${TAG} getComponent imgComponent.byteBuffer ${imgComponent.byteBuffer.byteLength}`)
  42.             buffer = imgComponent.byteBuffer
  43.             // todo 内置解码库不开源
  44.             let resultRGB: string = qr.decode(buffer)
  45.             Logger.info(`${TAG} AI uimg result RGB ${resultRGB}`)
  46.             if (callback) {
  47.               callback.aiResult(CodeRuleUtil.getRuleResult(resultRGB))
  48.             }
  49.           } else {
  50.             Logger.info(`${TAG} getComponent imgComponent.byteBuffer is undefined`)
  51.           }
解码

说明:解码使用内部的解码库因为不开源,非常抱歉,当然可以使用开源解码可以,如jsqr、zxing

  1. "dependencies": {
  2.     "jsqr""^1.4.0",
  3.     "@ohos/zxing""^2.0.0"
  4.   }
四、配网协议

说明:处于通用性考虑,需要对配网的二维码解析约定一个协议,也就是约定联网二维码数据的格式:##ssid##pwd##securityType

  • ssid :热点的SSID,编码格式为UTF-8。

  • pwd :热点的密钥

  • securityType :加密类型,这可以参看wifiManager.WifiSecurityType

在项目中也提供了协议解析类AnalyticResult.ts,具体代码如下:

  1. /**
  2.  * 结果解析类
  3.  */
  4. export type ResultType = {
  5.   ssid: string,
  6.   pwd: string,
  7.   securityType : number
  8. }
  9. const SEPARATOR: string = '##'
  10. export class Analytic {
  11.   constructor() {
  12.   }
  13.   getResult(msg: string): ResultType {
  14.     let result: ResultType = null
  15.     if (msg && msg.length > 0 && msg.indexOf(SEPARATOR) >= 0) {
  16.       let resultArr: string[] = msg.split(SEPARATOR)
  17.       if (resultArr.length >= 4) {
  18.         result = {
  19.           ssid: resultArr[1],
  20.           pwd: resultArr[2],
  21.           securityType: parseInt(resultArr[3])
  22.         }
  23.       }
  24.     }
  25.     return result
  26.   }
  27. }
五、配网页面

说明:通过对配网二维码的解析获取到热点的ssid、密钥、加密类型,就可以通过@ohos.wifiManager(WLAN)提供的网络连接接口实现配网。因为网络连接需要调用系统的一些验证流程,需要消耗一些时间,为了优化交互,需要一个网络连接等待界面ConnectPage.ets,界面截图如下:

具体代码如下:

  1. import { WifiConnectStatus } from '../model/Constant'
  2. import router from '@ohos.router';
  3. import { Logger } from '@ohos/common'
  4. import wifi from '@ohos.wifiManager';
  5. import { ResultType } from '../model/AnalyticResult'
  6. import { WifiModel } from '../model/WifiModel'
  7. /**
  8.  * 网络连接页面
  9.  */
  10. const TAG: string = '[ConnectPage]'
  11. const MAX_TIME_OUT: number = 60000 // 最大超时时间
  12. @Entry
  13. @Component
  14. struct ConnectPage {
  15.   @State mConnectSsid: string = ''
  16.   @State mConnectStatus: WifiConnectStatus = WifiConnectStatus.CONNECTING
  17.   @State mConnectingAngle : number = 0
  18.   @State mConnectFailResource : Resource = $r('app.string.connect_wifi_fail')
  19.   private linkedInfo: wifi.WifiLinkedInfo = null
  20.   private mWifiModel: WifiModel = new WifiModel()
  21.   private mTimeOutId: number = -1
  22.   private mAnimationTimeOutId : number = -1
  23.   async aboutToAppear() {
  24.     Logger.info(`${TAG} aboutToAppear`)
  25.     this.showConnecting()
  26.     let wifiResult: ResultType = router.getParams()['wifiResult']
  27.     Logger.info(`${TAG} wifiResult : ${JSON.stringify(wifiResult)}`)
  28.     // 如果wifi是开的,就记录下状态,然后扫描wifi,并获取连接信息
  29.     if (!wifi.isWifiActive()) {
  30.       Logger.info(TAG, 'enableWifi')
  31.       try {
  32.         wifi.enableWifi()
  33.       } catch (error) {
  34.         Logger.error(`${TAG} wifi enable fail, ${JSON.stringify(error)}`)
  35.       }
  36.     }
  37.     await this.getLinkedInfo()
  38.     // 启动监听
  39.     this.addListener()
  40.     if (wifiResult == null) {
  41.       Logger.info(TAG, 'wifiResult is null')
  42.       this.mConnectFailResource = $r('app.string.scan_code_data_error')
  43.       this.mConnectStatus = WifiConnectStatus.FAIL
  44.     } else {
  45.       this.mConnectSsid = wifiResult.ssid
  46.       Logger.info(`${TAG} connect wifi ${this.mConnectSsid}`)
  47.       this.disposeWifiConnect(wifiResult)
  48.     }
  49.   }
  50.   /**
  51.    * 启动超时任务
  52.    */
  53.   startTimeOut(): void {
  54.     Logger.info(TAG, `startTimeOut`)
  55.     this.mTimeOutId = setTimeout(() => {
  56.       // 如果超过1分钟没有连接上网络,则认为网络连接超时
  57.       try {
  58.         this.mConnectFailResource = $r('app.string.connect_wifi_fail')
  59.         this.mConnectStatus = WifiConnectStatus.FAIL
  60.         wifi.disconnect();
  61.       } catch (error) {
  62.         Logger.error(TAG, `failed,code:${JSON.stringify(error.code)},message:${JSON.stringify(error.message)}`)
  63.       }
  64.     }, MAX_TIME_OUT)
  65.   }
  66.   /**
  67.    * 取消超时任务
  68.    */
  69.   cancelTimeOut() {
  70.     Logger.info(TAG, `cancelTimeOut id:${this.mTimeOutId}`)
  71.     if (this.mTimeOutId >= 0) {
  72.       clearTimeout(this.mTimeOutId)
  73.       this.mTimeOutId = -1
  74.     }
  75.   }
  76.   // 监听wifi的变化
  77.   addListener() {
  78.     // 连接状态改变时,修改连接信息
  79.     wifi.on('wifiConnectionChange', async state => {
  80.       Logger.info(TAG, `wifiConnectionChange: ${state}`)
  81.       // 判断网络是否连接 0=断开  1=连接
  82.       if (state === 1) {
  83.         this.mConnectStatus = WifiConnectStatus.SUCCESS
  84.         this.cancelTimeOut()
  85.       }
  86.       await this.getLinkedInfo()
  87.     })
  88.     // wifi状态改变时,先清空wifi列表,然后判断是否是开启状态,如果是就扫描
  89.     wifi.on('wifiStateChange', state => {
  90.       Logger.info(TAG, `wifiStateLisener state: ${state}`)
  91.     })
  92.   }
  93.   // 获取有关Wi-Fi连接的信息,存入linkedInfo
  94.   async getLinkedInfo() {
  95.     try {
  96.       let wifiLinkedInfo = await wifi.getLinkedInfo()
  97.   if (wifiLinkedInfo === null || wifiLinkedInfo.bssid === '') {
  98.         this.linkedInfo = null
  99.         return
  100.       }
  101.       this.linkedInfo = wifiLinkedInfo
  102.     } catch (err) {
  103.       Logger.info(`getLinkedInfo failed err is ${JSON.stringify(err)}`)
  104.     }
  105.   }
  106.   /**
  107.    * 处理wifi连接
  108.    * @param wifiResult
  109.    */
  110.   disposeWifiConnect(wifiResult: ResultType): void {
  111.     this.mConnectStatus = WifiConnectStatus.CONNECTING
  112.     if (this.linkedInfo) {
  113.       // 说明wifi已经连接,需要确认需要连接的wifi和已连接的wifi是否为相同
  114.       let linkedSsid: string = this.linkedInfo.ssid;
  115.       if (linkedSsid === wifiResult.ssid) {
  116.         Logger.info(`${TAG} The same ssid`);
  117.         this.mConnectStatus = WifiConnectStatus.SUCCESS
  118.         return;
  119.       }
  120.       // 如果wifi不同,则先断开网络连接,再重新连接
  121.       try {
  122.         wifi.disconnect();
  123.         this.connectWifi(wifiResult.ssid, wifiResult.pwd, wifiResult.securityType)
  124.       } catch (error) {
  125.         Logger.error(TAG, `failed,code:${JSON.stringify(error.code)},message:${JSON.stringify(error.message)}`)
  126.       }
  127.     } else {
  128.       this.connectWifi(wifiResult.ssid, wifiResult.pwd, wifiResult.securityType)
  129.     }
  130.   }
  131.   private connectWifi(ssid: string, pwd: string, securityType : number) {
  132.     this.startTimeOut()
  133.     this.mWifiModel.connectNetwork(ssid, pwd, securityType)
  134.   }
  135.   async gotoIndex() {
  136.     try {
  137.       let options: router.RouterOptions = {
  138.         url: "pages/Index"
  139.       }
  140.       await router.replaceUrl(options)
  141.     } catch (error) {
  142.       Logger.error(`${TAG} go to index fail, err: ${JSON.stringify(error)}`)
  143.     }
  144.   }
  145.   showConnecting() {
  146.     this.mConnectingAngle = 0
  147.     this.mAnimationTimeOutId = setTimeout(() => {
  148.       this.mConnectingAngle = 360
  149.     }, 500)
  150.   }
  151.   closeConnecting() {
  152.     if (this.mAnimationTimeOutId > -1) {
  153.       clearTimeout(this.mAnimationTimeOutId)
  154.     }
  155.   }
  156.   aboutToDisappear() {
  157.     wifi.off('wifiConnectionChange')
  158.    wifi.off('wifiStateChange')
  159.     this.cancelTimeOut()
  160.     this.closeConnecting()
  161.   }
  162.   build() {
  163.     Column() {
  164.       // back
  165.       Row() {
  166.         Image($r('app.media.icon_back'))
  167.           .width(30)
  168.           .height(30)
  169.           .objectFit(ImageFit.Contain)
  170.           .onClick(() => {
  171.             router.back()
  172.           })
  173.       }
  174.       .width('90%')
  175.       .height('10%')
  176.       .justifyContent(FlexAlign.Start)
  177.       .alignItems(VerticalAlign.Center)
  178.       Stack() {
  179.         // 背景
  180.         Column() {
  181.           Image($r('app.media.bg_connect_wifi'))
  182.             .width('100%')
  183.             .height('100%')
  184.             .objectFit(ImageFit.Contain)
  185.             .rotate({
  186.               x: 0,
  187.               y: 0,
  188.               z: 1,
  189.               centerX: '50%',
  190.               centerY: '49%',
  191.               angle: this.mConnectingAngle
  192.             })
  193.             .animation({
  194.               duration: 2000// 动画时长
  195.               curve: Curve.Linear, // 动画曲线
  196.               delay: 0// 动画延迟
  197.               iterations: -1// 播放次数
  198.               playMode: PlayMode.Normal // 动画模式
  199.             })
  200.         }
  201.         Column({ space20 }) {
  202.           if (this.mConnectStatus === WifiConnectStatus.SUCCESS) {
  203.             // 连接成功
  204.             Image($r('app.media.icon_connect_wifi_success'))
  205.               .width(80)
  206.               .height(80)
  207.               .objectFit(ImageFit.Contain)
  208.             Text($r('app.string.connect_wifi_success'))
  209.               .fontSize(32)
  210.               .fontColor($r('app.color.connect_wifi_text'))
  211.             Text(this.mConnectSsid)
  212.               .fontSize(22)
  213.               .fontColor($r('app.color.connect_wifi_text'))
  214.           } else if (this.mConnectStatus === WifiConnectStatus.FAIL) {
  215.             // 连接失败
  216.             Image($r('app.media.icon_connect_wifi_fail'))
  217.               .width(80)
  218.               .height(80)
  219.               .objectFit(ImageFit.Contain)
  220.             Text(this.mConnectFailResource)
  221.               .fontSize(32)
  222.               .fontColor($r('app.color.connect_wifi_text'))
  223.             Button($r('app.string.reconnect_wifi'))
  224.               .width(260)
  225.               .height(55)
  226.               .backgroundColor($r('app.color.connect_fail_but_bg'))
  227.               .onClick(() => {
  228.                 this.gotoIndex()
  229.               })
  230.           } else {
  231.             // 连接中
  232.    Image($r('app.media.icon_connect_wifi'))
  233.               .width(100)
  234.               .height(100)
  235.               .objectFit(ImageFit.Contain)
  236.             Text($r('app.string.connect_wifi_hint'))
  237.               .fontSize(16)
  238.               .fontColor($r('app.color.connect_wifi_text'))
  239.             Text($r('app.string.connecting_wifi'))
  240.               .fontSize(32)
  241.               .fontColor($r('app.color.connect_wifi_text'))
  242.             Text(this.mConnectSsid)
  243.               .fontSize(22)
  244.               .fontColor($r('app.color.connect_wifi_text'))
  245.           }
  246.         }
  247.         .width('100%')
  248.         .height('100%')
  249.         .justifyContent(FlexAlign.Center)
  250.         .alignItems(HorizontalAlign.Center)
  251.       }
  252.       .width('100%')
  253.       .height('80%')
  254.     }
  255.     .width('100%')
  256.     .height('100%')
  257.     .backgroundColor($r('app.color.connect_bg'))
  258.   }
  259. }

整个界面比较简单,主要显示当前的连接状态:连接中、连接成功、连接超时,特别强调连接超时,计划热点最长连接60s,如果在预定时间未连接成功,则显示超时,超时后可以通过重新配网按钮进行重新扫码连接,根据实际测试,在热点未打开状态下扫码连接耗时平均值12s。

亮点

界面中最大的亮点,增加了一个发光圆形的属性动画animation,圆形在2s内绕着z轴旋从0度转到360度。

  1. Image($r('app.media.bg_connect_wifi'))
  2.             .width('100%')
  3.             .height('100%')
  4.             .objectFit(ImageFit.Contain)
  5.             .rotate({
  6.               x: 0,
  7.               y: 0,
  8.               z: 1,
  9.               centerX: '50%',
  10.               centerY: '49%',
  11.               angle: this.mConnectingAngle
  12.             })
  13.             .animation({
  14.               duration: 2000// 动画时长
  15.               curve: Curve.Linear, // 动画曲线
  16.               delay: 0// 动画延迟
  17.               iterations: -1// 播放次数
  18.               playMode: PlayMode.Normal // 动画模式
  19.             })
六、网络自动连接

说明:网络自动连接主要是通过@ohos.wifiManager(WLAN)提供的连接接口实现,具体代码如下:

  1. import wifi from '@ohos.wifiManager'
  2. import { Logger } from '@ohos/common'
  3. const TAGstring = '[WiFiModel]'
  4. export type WifiType = {
  5.   ssidstring,
  6.   bssidstring,
  7.   securityType: wifi.WifiSecurityType,
  8.   rssinumber,
  9.   bandnumber,
  10.   frequencynumber,
  11.   timestampnumber
  12. }
  13. export class WifiModel {
  14.   async getScanInfos(): Promise<Array<WifiType>> {
  15.     Logger.info(TAG'scanWifi begin')
  16.     let wifiListArray<WifiType> = []
  17.     let resultArray<wifi.WifiScanInfo> = []
  18.     try {
  19.       result = await wifi.getScanResults()
  20.     } catch (err) {
  21.       Logger.info(TAG`scan info err: ${JSON.stringify(err)}`)
  22.       return wifiList
  23.     }
  24.     Logger.info(TAG`scan info call back: ${result.length}`)
  25.     for (var i = 0; i < result.length; ++i) {
  26.       wifiList.push({
  27.         ssid: result[i].ssid,
  28.         bssid: result[i].bssid,
  29.         securityType: result[i].securityType,
  30.         rssi: result[i].rssi,
  31.         band: result[i].band,
  32.         frequency: result[i].frequency,
  33.         timestamp: result[i].timestamp
  34.       })
  35.     }
  36.     return wifiList
  37.   }
  38.   connectNetwork(wifiSsidstringpswstring, securityType : number): void {
  39.     Logger.debug(TAG`connectNetwork bssid=${wifiSsid} securityType:${securityType}`)
  40.     // securityType 加密类型默认:Pre-shared key (PSK)加密类型
  41.     let deviceConfig: wifi.WifiDeviceConfig  = {
  42.       ssid: wifiSsid,
  43.       preSharedKey: psw,
  44.       isHiddenSsidfalse,
  45.       securityType: securityType
  46.     }
  47.     try {
  48.       wifi.connectToDevice(deviceConfig)
  49.       Logger.info(TAG`connectToDevice success`)
  50.     } catch (err) {
  51.       Logger.error(TAG`connectToDevice fail err is ${JSON.stringify(err)}`)
  52.     }
  53.     try {
  54.       wifi.addDeviceConfig(deviceConfig)
  55.     } catch (err) {
  56.       Logger.error(TAG`addDeviceConfig fail err is ${JSON.stringify(err)}`)
  57.     }
  58.   }
  59. }

网络连接主要是通过wifi.connectToDevice(deviceConfig)实现,其中:deviceConfig: wifi.WifiDeviceConfig为WLAN配置信息,在连接网络时必填三个参数ssid、preSharedKey、securityType。

  • ssid:热点的SSID

  • preSharedKey:热点密钥

  • securityType:加密类型

注意:在调用connectToDevice()函数连接网络时,如果网络已经连接,则需要先调用disconnect()接口断开网络后再执行。

至此,你已经完成了扫码即可连接网络的应用。

感谢

如果您能看到最后,还希望您能动动手指点个赞,一个人能走多远关键在于与谁同行,我用跨越山海的一路相伴,希望得到您的点赞。

最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?但是又不知道从哪里下手,而且学习时频繁踩坑,最终浪费大量时间。所以本人整理了一些比较合适的鸿蒙(HarmonyOS NEXT)学习路径和一些资料的整理供小伙伴学习

点击领取→纯血鸿蒙Next全套最新学习资料希望这一份鸿蒙学习资料能够给大家带来帮助,有需要的小伙伴自行领取~~

一、鸿蒙(HarmonyOS NEXT)最新学习路线

有了路线图,怎么能没有学习资料呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)…等技术知识点。

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料

二、HarmonyOS Next 最新全套视频教程

三、《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

四、大厂面试必问面试题

五、鸿蒙南向开发技术

六、鸿蒙APP开发必备


完整鸿蒙HarmonyOS学习资料,请点击→纯血版全套鸿蒙HarmonyOS学习资料

总结
总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变化,不断学习和提升自己,他们才能在这个变革的时代中立于不败之地。 

                        

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/950330
推荐阅读
相关标签
  

闽ICP备14008679号