鸿蒙实战开发:【相机和媒体库】

介绍

在ArkTS中调用相机拍照和录像,以及如何使用媒体库接口进行媒体文件的增、删、改、查操作。本示例用到了

  • 权限管理能力
  • 相机模块能力接口
  • 图片处理接口
  • 音视频相关媒体业务能力接口
  • 媒体库管理接口
  • 设备信息能力接口
  • 文件存储管理能力接口
  • 弹窗能力接口

效果预览

首页

使用说明

1.启动应用,在权限弹窗中授权后返回应用,首页显示当前设备的相册信息,首页监听相册变化会刷新相册列表。

2.点击 + 按钮,弹出相机、录音、文本文件三个图标。

3.安装相机应用[Camera]应用后,点击相机 图标,进入相机界面,默认是拍照模式,点击底部拍照按钮可以拍照,拍照完成会在底部左侧显示照片预览图。点击录像 切换到录像模式,点击底部按钮开始录像,点击结束按钮结束录像,结束录像后底部左侧显示视频图标。点击系统Back 键或界面顶部返回按钮返回首页。

4.点击录音 图标进入录音界面,点击右侧开始 按钮开始录音,按钮变为暂停按钮,点击可以暂停和继续录音,点击左侧结束按钮结束录音返回首页。

5.点击文本 图标进入文本编辑界面,输入文本内容后点击Save按钮,会创建并写入文本文件,完成后返回首页。

6.点击相册进入文件列表界面,展示相册内的文件,列表中有删除重命名按钮,点击可以删除文件和重命名文件。

7.安装视频播放[VideoPlayer]应用后,点击视频文件可以调起视频播放界面播放该视频。

相关概念

媒体库管理:媒体库管理提供接口对公共媒体资源文件进行管理,包括文件的增、删、改、查等。 相机:相机模块支持相机相关基础功能的开发,主要包括预览、拍照、录像等。

工程目录

entry/src/main/ets/
|---MainAbility
|   |---MainAbility.ts                      // 主程序入口,应用启动时获取相应权限
|---pages
|   |---index.ets                           // 首页
|   |---AlbumPage.ets                       // 相册页面
|   |---CameraPage.ets                      // 相机页面
|   |---RecordPage.ets                      // 录音页面
|   |---DocumentPage.ets                    // 存储文件页面
|---model                                  
|   |---CameraService.ts                    // 相机模块(拍照录像模式)
|   |---DateTimeUtil.ts                     // 日期工具包
|   |---MediaUtils.ts                       // 媒体工具模块
|   |---RecordModel.ts                      // 录音模块(底层能力实现)
|   |---TimeUtils.ts                        // 时间工具包
|---view                                    
|   |---BasicDataSource.ets                 // 初始化媒体服务数组
|   |---MediaItem.ets                       // 定义具体的某一媒体模块页面 
|   |---MediaView.ets                       // 媒体模块的前置模块(判断是否有展示的媒体内容)
|   |---RenameDialog.ets                    // 重命名文件模块 
|   |---TitleBar.ets                        // 标题栏                                                           

具体实现

  • 布局原理:定义@ObjectLink 装饰的数组变量album存放资源文件,使用list()组件中ListItem()循环数组展示,加号Button(),点击后触发 animateTo({ duration: 500, curve: Curve.Ease })控制动画展示,[源码参考]。

    /*

    • Copyright (c) 2022-2023 Huawei Device Co., Ltd.

    • Licensed under the Apache License, Version 2.0 (the "License");

    • you may not use this file except in compliance with the License.

    • You may obtain a copy of the License at

    • http://www.apache.org/licenses/LICENSE-2.0
      
    • Unless required by applicable law or agreed to in writing, software

    • distributed under the License is distributed on an "AS IS" BASIS,

    • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

    • See the License for the specific language governing permissions and

    • limitations under the License.

    */

    import mediaLibrary from '@ohos.multimedia.mediaLibrary';

    import common from '@ohos.app.ability.common';

    import Want from '@ohos.app.ability.Want';

    import router from '@ohos.router';

    import TitleBar from '../view/TitleBar';

    import MediaUtils from '../model/MediaUtils';

    import { MediaView } from '../view/MediaView';

    import Logger from '../model/Logger';

    @Observed

    export default class Album {

    constructor(public albumName: string, public count: number, public mediaType?: mediaLibrary.MediaType) {
    
      this.albumName = albumName;
    
      this.count = count;
    
      this.mediaType = mediaType;
    
    }
    

    }

    @Entry

    @Component

    struct Index {

    private mediaUtils: MediaUtils = MediaUtils.getInstance(getContext(this))
    
    @State albums: Array<Album> = []
    
    @State selectIndex: number = 0
    
    @State operateVisible: boolean = false
    
    
    
    async onPageShow() {
    
      this.albums = [];
    
      this.albums = await this.mediaUtils.getAlbums();
    
    }
    
    
    
    @Builder OperateBtn(src, zIndex, translate, handleClick) {
    
      Button() {
    
        Image(src)
    
          .width('70%')
    
          .height('70%')
    
      }
    
      .type(ButtonType.Circle)
    
      .width('40%')
    
      .height('40%')
    
      .backgroundColor('#0D9FFB')
    
      .zIndex(zIndex)
    
      .translate({ x: translate.x, y: translate.y })
    
      .transition({ type: TransitionType.Insert, translate: { x: 0, y: 0 } })
    
      .transition({ type: TransitionType.Delete, opacity: 0 })
    
      .onClick(handleClick)
    
    }
    
    
    
    build() {
    
      Stack({ alignContent: Alignment.BottomEnd }) {
    
        Column() {
    
          TitleBar()
    
          List() {
    
            ForEach(this.albums, (item: Album, index) => {
    
              ListItem() {
    
                MediaView({ album: item })
    
                  .id(`mediaType${index}`)
    
              }
    
            }, item => item.albumName)
    
          }
    
          .divider({ strokeWidth: 1, color: Color.Gray, startMargin: 16, endMargin: 16 })
    
          .layoutWeight(1)
    
        }
    
        .width('100%')
    
        .height('100%')
    
    
    
        Stack({ alignContent: Alignment.Center }) {
    
          Button() {
    
            Image($r('app.media.add'))
    
              .width('100%')
    
              .height('100%')
    
          }
    
          .width(60)
    
          .height(60)
    
          .padding(10)
    
          .id('addBtn')
    
          .type(ButtonType.Circle)
    
          .backgroundColor('#0D9FFB')
    
          .onClick(() => {
    
            animateTo({ duration: 500, curve: Curve.Ease }, () => {
    
              this.operateVisible = !this.operateVisible
    
            })
    
          })
    
    
    
          Button() {
    
            Image($r('app.media.icon_camera'))
    
              .id('camera')
    
              .width('100%')
    
              .height('100%')
    
          }
    
          .width(60)
    
          .height(60)
    
          .padding(10)
    
          .type(ButtonType.Circle)
    
          .backgroundColor('#0D9FFB')
    
          .translate({ x: 0, y: -80 })
    
          .visibility(this.operateVisible ? Visibility.Visible : Visibility.None)
    
          .onClick(() => {
    
            this.operateVisible = !this.operateVisible;
    
            let context: common.UIAbilityContext | undefined = AppStorage.Get('context');
    
            let want: Want = {
    
              bundleName: "com.samples.camera_page",
    
              abilityName: "EntryAbility",
    
            };
    
            context && context.startAbility(want,  (err) => {
    
              if (err.code) {
    
                Logger.error('StartAbility', `Failed to startAbility. Code: ${err.code}, message: ${err.message}`);
    
              }
    
            });
    
          })
    
    
    
          Button() {
    
            Image($r('app.media.icon_record'))
    
              .id('record')
    
              .width('100%')
    
              .height('100%')
    
          }
    
          .width(60)
    
          .height(60)
    
          .padding(10)
    
          .type(ButtonType.Circle)
    
          .backgroundColor('#0D9FFB')
    
          .translate({ x: -80, y: 0 })
    
          .visibility(this.operateVisible ? Visibility.Visible : Visibility.None)
    
          .onClick(() => {
    
            this.operateVisible = !this.operateVisible
    
            router.push({ url: 'pages/RecordPage' })
    
          })
    
    
    
          Button() {
    
            Image($r('app.media.icon_document'))
    
              .width('100%')
    
              .height('100%')
    
          }
    
          .width(60)
    
          .height(60)
    
          .padding(10)
    
          .id('document')
    
          .type(ButtonType.Circle)
    
          .backgroundColor('#0D9FFB')
    
          .translate({ x: 0, y: 80 })
    
          .visibility(this.operateVisible ? Visibility.Visible : Visibility.None)
    
          .onClick(() => {
    
            this.operateVisible = !this.operateVisible
    
            router.pushUrl({ url: 'pages/DocumentPage' })
    
          })
    
        }
    
        .width(180)
    
        .height(220)
    
        .margin({ right: 40, bottom: 120 })
    
      }
    
      .width('100%')
    
      .height('100%')
    
    }
    
    
    
    aboutToDisappear() {
    
      this.mediaUtils.offDateChange()
    
    }
    

    }

  • 获取资源文件:通过引入媒体库实例(入口)接口@ohos.multimedia.medialibrary,例如通过this.getFileAssetsFromType(mediaLibrary.MediaType.FILE)获取FILE类型的文件资源,并通过albums.push()添加至album数组中。

  • 展示系统资源文件:当album内的值被修改时,只会让用 @ObjectLink 装饰的变量album所在的组件被刷新,当前组件不会刷新。

  • 录音功能:通过引入音视频接口@ohos.multimedia.media,例如通过media.createAudioRecorder()创建音频录制的实例来控制音频的录制,通过this.audioRecorder.on('prepare', () => {this.audioRecorder.start()})异步方式开始音频录制,[源码参考]

    /*

    • Copyright (c) 2022 Huawei Device Co., Ltd.

    • Licensed under the Apache License, Version 2.0 (the "License");

    • you may not use this file except in compliance with the License.

    • You may obtain a copy of the License at

    • http://www.apache.org/licenses/LICENSE-2.0
      
    • Unless required by applicable law or agreed to in writing, software

    • distributed under the License is distributed on an "AS IS" BASIS,

    • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

    • See the License for the specific language governing permissions and

    • limitations under the License.

    */

    import media from '@ohos.multimedia.media'

    import Logger from '../model/Logger'

    let audioConfig = {

    audioSourceType: 1,
    
    audioEncoder: 3,
    
    audioEncodeBitRate: 22050,
    
    audioSampleRate: 22050,
    
    numberOfChannels: 2,
    
    format: 6,
    
    uri: ''
    

    }

    export default class RecordModel {

    private tag: string = 'RecordModel'
    
    private audioRecorder: media.AudioRecorder = undefined
    
    
    
    initAudioRecorder(handleStateChange: () => void) {
    
      this.release();
    
      this.audioRecorder = media.createAudioRecorder()
    
      Logger.info(this.tag, 'create audioRecorder success')
    
      this.audioRecorder.on('prepare', () => {
    
        Logger.info(this.tag, 'setCallback  prepare case callback is called')
    
        this.audioRecorder.start()
    
      })
    
      this.audioRecorder.on('start', () => {
    
        Logger.info(this.tag, 'setCallback start case callback is called')
    
        handleStateChange()
    
      })
    
      this.audioRecorder.on('stop', () => {
    
        Logger.info(this.tag, 'audioRecorder stop called')
    
        this.audioRecorder.release()
    
      })
    
      this.audioRecorder.on('pause', () => {
    
        Logger.info(this.tag, 'audioRecorder pause finish')
    
        handleStateChange()
    
      })
    
      this.audioRecorder.on('resume', () => {
    
        Logger.info(this.tag, 'audioRecorder resume finish')
    
        handleStateChange()
    
      })
    
    }
    
    
    
    release() {
    
      if (typeof (this.audioRecorder) !== `undefined`) {
    
        Logger.info(this.tag, 'audioRecorder  release')
    
        this.audioRecorder.release()
    
        this.audioRecorder = undefined
    
      }
    
    }
    
    
    
    startRecorder(pathName: string) {
    
      Logger.info(this.tag, `startRecorder, pathName = ${pathName}`)
    
      if (typeof (this.audioRecorder) !== 'undefined') {
    
        Logger.info(this.tag, 'start prepare')
    
        audioConfig.uri = pathName
    
        this.audioRecorder.prepare(audioConfig)
    
      } else {
    
        Logger.error(this.tag, 'case failed, audioRecorder is null')
    
      }
    
    }
    
    
    
    pause() {
    
      Logger.info(this.tag, 'audioRecorder pause called')
    
      if (typeof (this.audioRecorder) !== `undefined`) {
    
        this.audioRecorder.pause()
    
      }
    
    }
    
    
    
    resume() {
    
      Logger.info(this.tag, 'audioRecorder resume called')
    
      if (typeof (this.audioRecorder) !== `undefined`) {
    
        this.audioRecorder.resume()
    
      }
    
    }
    
    
    
    finish() {
    
      if (typeof (this.audioRecorder) !== `undefined`) {
    
        this.audioRecorder.stop()
    
      }
    
    }
    

    }

  • 拍照录像功能:通过引入相机模块接口@ohos.multimedia.camera,例如通过this.cameraManager.createCaptureSession()创建相机入口的实例来控制拍照和录像,通过this.captureSession.start()开始会话工作,[源码参考]

    /*

    • Copyright (c) 2022 Huawei Device Co., Ltd.

    • Licensed under the Apache License, Version 2.0 (the "License");

    • you may not use this file except in compliance with the License.

    • You may obtain a copy of the License at

    • http://www.apache.org/licenses/LICENSE-2.0
      
    • Unless required by applicable law or agreed to in writing, software

    • distributed under the License is distributed on an "AS IS" BASIS,

    • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

    • See the License for the specific language governing permissions and

    • limitations under the License.

    */

    import camera from '@ohos.multimedia.camera'

    import deviceInfo from '@ohos.deviceInfo'

    import fileio from '@ohos.fileio'

    import image from '@ohos.multimedia.image'

    import media from '@ohos.multimedia.media'

    import mediaLibrary from '@ohos.multimedia.mediaLibrary'

    import Logger from '../model/Logger'

    import MediaUtils from '../model/MediaUtils'

    const CameraMode = {

    MODE_PHOTO: 0, // 拍照模式
    
    MODE_VIDEO: 1 // 录像模式
    

    }

    const CameraSize = {

    WIDTH: 1920,
    
    HEIGHT: 1080
    

    }

    export default class CameraService {

    private tag: string = 'CameraService'
    
    private context: any = undefined
    
    private mediaUtil: MediaUtils = undefined
    
    private cameraManager: camera.CameraManager = undefined
    
    private cameras: Array<camera.CameraDevice> = undefined
    
    private cameraId: string = ''
    
    private cameraInput: camera.CameraInput = undefined
    
    private previewOutput: camera.PreviewOutput = undefined
    
    private photoOutPut: camera.PhotoOutput = undefined
    
    private captureSession: camera.CaptureSession = undefined
    
    private mReceiver: image.ImageReceiver = undefined
    
    private photoUri: string = ''
    
    private fileAsset: mediaLibrary.FileAsset = undefined
    
    private fd: number = -1
    
    private curMode = CameraMode.MODE_PHOTO
    
    private videoRecorder: media.VideoRecorder = undefined
    
    private videoOutput: camera.VideoOutput = undefined
    
    private handleTakePicture: (photoUri: string) => void = undefined
    
    private cameraOutputCapability: camera.CameraOutputCapability = undefined
    
    private videoConfig: any = {
    
      audioSourceType: 1,
    
      videoSourceType: 0,
    
      profile: {
    
        audioBitrate: 48000,
    
        audioChannels: 2,
    
        audioCodec: 'audio/mp4v-es',
    
        audioSampleRate: 48000,
    
        durationTime: 1000,
    
        fileFormat: 'mp4',
    
        videoBitrate: 48000,
    
        videoCodec: 'video/mp4v-es',
    
        videoFrameWidth: 640,
    
        videoFrameHeight: 480,
    
        videoFrameRate: 30
    
      },
    
      url: '',
    
      orientationHint: 0,
    
      location: {
    
        latitude: 30, longitude: 130
    
      },
    
      maxSize: 10000,
    
      maxDuration: 10000
    
    }
    
    
    
    constructor(context: any) {
    
      this.context = context
    
      this.mediaUtil = MediaUtils.getInstance(context)
    
      this.mReceiver = image.createImageReceiver(CameraSize.WIDTH, CameraSize.HEIGHT, 4, 8)
    
      Logger.info(this.tag, 'createImageReceiver')
    
      this.mReceiver.on('imageArrival', () => {
    
        Logger.info(this.tag, 'imageArrival')
    
        this.mReceiver.readNextImage((err, image) => {
    
          Logger.info(this.tag, 'readNextImage')
    
          if (err || image === undefined) {
    
            Logger.error(this.tag, 'failed to get valid image')
    
            return
    
          }
    
          image.getComponent(4, (errMsg, img) => {
    
            Logger.info(this.tag, 'getComponent')
    
            if (errMsg || img === undefined) {
    
              Logger.info(this.tag, 'failed to get valid buffer')
    
              return
    
            }
    
            let buffer = new ArrayBuffer(4096)
    
            if (img.byteBuffer) {
    
              buffer = img.byteBuffer
    
            } else {
    
              Logger.error(this.tag, 'img.byteBuffer is undefined')
    
            }
    
            this.savePicture(buffer, image)
    
          })
    
        })
    
      })
    
    }
    
    
    
    async savePicture(buffer: ArrayBuffer, img: image.Image) {
    
      Logger.info(this.tag, 'savePicture')
    
      this.fileAsset = await this.mediaUtil.createAndGetUri(mediaLibrary.MediaType.IMAGE)
    
      this.photoUri = this.fileAsset.uri
    
      Logger.info(this.tag, `this.photoUri = ${this.photoUri}`)
    
      this.fd = await this.mediaUtil.getFdPath(this.fileAsset)
    
      Logger.info(this.tag, `this.fd = ${this.fd}`)
    
      await fileio.write(this.fd, buffer)
    
      await this.fileAsset.close(this.fd)
    
      await img.release()
    
      Logger.info(this.tag, 'save image done')
    
      if (this.handleTakePicture) {
    
        this.handleTakePicture(this.photoUri)
    
      }
    
    }
    
    
    
    async initCamera(surfaceId: string) {
    
      Logger.info(this.tag, 'initCamera')
    
      await this.releaseCamera()
    
      Logger.info(this.tag, `deviceInfo.deviceType = ${deviceInfo.deviceType}`)
    
      if (deviceInfo.deviceType === 'default') {
    
        this.videoConfig.videoSourceType = 1
    
      } else {
    
        this.videoConfig.videoSourceType = 0
    
      }
    
      this.cameraManager = await camera.getCameraManager(this.context)
    
      Logger.info(this.tag, 'getCameraManager')
    
      this.cameras = await this.cameraManager.getSupportedCameras()
    
      Logger.info(this.tag, `get cameras ${this.cameras.length}`)
    
      if (this.cameras.length === 0) {
    
        Logger.info(this.tag, 'cannot get cameras')
    
        return
    
      }
    
    
    
      let cameraDevice = this.cameras[0]
    
      this.cameraInput = await this.cameraManager.createCameraInput(cameraDevice)
    
      this.cameraInput.open()
    
      Logger.info(this.tag, 'createCameraInput')
    
      this.cameraOutputCapability = await this.cameraManager.getSupportedOutputCapability(cameraDevice)
    
      let previewProfile = this.cameraOutputCapability.previewProfiles[0]
    
      this.previewOutput = await this.cameraManager.createPreviewOutput(previewProfile, surfaceId)
    
      Logger.info(this.tag, 'createPreviewOutput')
    
      let mSurfaceId = await this.mReceiver.getReceivingSurfaceId()
    
      let photoProfile = this.cameraOutputCapability.photoProfiles[0]
    
      this.photoOutPut = await this.cameraManager.createPhotoOutput(photoProfile, mSurfaceId)
    
      this.captureSession = await this.cameraManager.createCaptureSession()
    
      Logger.info(this.tag, 'createCaptureSession')
    
      await this.captureSession.beginConfig()
    
      Logger.info(this.tag, 'beginConfig')
    
      await this.captureSession.addInput(this.cameraInput)
    
      await this.captureSession.addOutput(this.previewOutput)
    
      await this.captureSession.addOutput(this.photoOutPut)
    
      await this.captureSession.commitConfig()
    
      await this.captureSession.start()
    
      Logger.info(this.tag, 'captureSession start')
    
    }
    
    
    
    setTakePictureCallback(callback) {
    
      this.handleTakePicture = callback
    
    }
    
    
    
    async takePicture() {
    
      Logger.info(this.tag, 'takePicture')
    
      if (this.curMode === CameraMode.MODE_VIDEO) {
    
        this.curMode = CameraMode.MODE_PHOTO
    
      }
    
      let photoSettings = {
    
        rotation: camera.ImageRotation.ROTATION_0,
    
        quality: camera.QualityLevel.QUALITY_LEVEL_MEDIUM,
    
        location: { // 位置信息,经纬度
    
          latitude: 12.9698,
    
          longitude: 77.7500,
    
          altitude: 1000
    
        },
    
        mirror: false
    
      }
    
      await this.photoOutPut.capture(photoSettings)
    
      Logger.info(this.tag, 'takePicture done')
    
      AppStorage.Set('isRefresh', true)
    
    }
    
    
    
    async startVideo() {
    
      Logger.info(this.tag, 'startVideo begin')
    
      await this.captureSession.stop()
    
      await this.captureSession.beginConfig()
    
      if (this.curMode === CameraMode.MODE_PHOTO) {
    
        this.curMode = CameraMode.MODE_VIDEO
    
        if (this.photoOutPut) {
    
          await this.captureSession.removeOutput(this.photoOutPut)
    
          this.photoOutPut.release()
    
        }
    
      } else {
    
        if (this.videoOutput) {
    
          await this.captureSession.removeOutput(this.videoOutput)
    
        }
    
      }
    
      if (this.videoOutput) {
    
        await this.captureSession.removeOutput(this.videoOutput)
    
        await this.videoOutput.release()
    
      }
    
      this.fileAsset = await this.mediaUtil.createAndGetUri(mediaLibrary.MediaType.VIDEO)
    
      this.fd = await this.mediaUtil.getFdPath(this.fileAsset)
    
      this.videoRecorder = await media.createVideoRecorder()
    
      this.videoConfig.url = `fd://${this.fd}`
    
      await this.videoRecorder.prepare(this.videoConfig)
    
      let videoId = await this.videoRecorder.getInputSurface()
    
      let videoProfile = this.cameraOutputCapability.videoProfiles[0];
    
      this.videoOutput = await this.cameraManager.createVideoOutput(videoProfile, videoId)
    
      await this.captureSession.addOutput(this.videoOutput)
    
      await this.captureSession.commitConfig()
    
      await this.captureSession.start()
    
      await this.videoOutput.start()
    
      await this.videoRecorder.start()
    
      Logger.info(this.tag, 'startVideo end')
    
    }
    
    
    
    async stopVideo() {
    
      Logger.info(this.tag, 'stopVideo called')
    
      await this.videoRecorder.stop()
    
      await this.videoOutput.stop()
    
      await this.videoRecorder.release()
    
      await this.fileAsset.close(this.fd)
    
    }
    
    
    
    async releaseCamera() {
    
      Logger.info(this.tag, 'releaseCamera')
    
      if (this.cameraInput) {
    
        await this.cameraInput.close()
    
      }
    
      if (this.previewOutput) {
    
        await this.previewOutput.release()
    
      }
    
      if (this.photoOutPut) {
    
        await this.photoOutPut.release()
    
      }
    
      if (this.videoOutput) {
    
        await this.videoOutput.release()
    
      }
    
      if (this.captureSession) {
    
        await this.captureSession.release()
    
      }
    
    }
    

    }

鸿蒙NEXT开发知识已更新 gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md可参考学习更多

约束与限制

1.rk3568底层录像功能有问题,暂不支持录像功能,当前拍照功能仅支持部分机型。

2.本示例仅支持标准系统上运行。

3.本示例为Stage模型,已适配API version 9版本SDK,版本号:3.2.11.9;

4.本示例需要使用DevEco Studio 3.1 Beta2 (Build Version: 3.1.0.400, built on April 7, 2023)及以上版本才可编译运行。

相关推荐
试行1 小时前
Android实现自定义下拉列表绑定数据
android·java
Dingdangr6 小时前
Android中的Intent的作用
android
技术无疆6 小时前
快速开发与维护:探索 AndroidAnnotations
android·java·android studio·android-studio·androidx·代码注入
GEEKVIP6 小时前
Android 恢复挑战和解决方案:如何从 Android 设备恢复删除的文件
android·笔记·安全·macos·智能手机·电脑·笔记本电脑
逢生博客11 小时前
Mac 搭建仓颉语言开发环境(Cangjie SDK)
macos·华为·鸿蒙
青柠_项目管理11 小时前
PMP证书持有者,在华为、腾讯一般能拿多少薪资?
华为·pmp
Jouzzy13 小时前
【Android安全】Ubuntu 16.04安装GDB和GEF
android·ubuntu·gdb
极客先躯13 小时前
java和kotlin 可以同时运行吗
android·java·开发语言·kotlin·同时运行
小强在此15 小时前
【基于开源鸿蒙(OpenHarmony)的智慧农业综合应用系统】
华为·开源·团队开发·智慧农业·harmonyos·开源鸿蒙