在使用App的时候,我们经常会在一些社交软件中聊天时发一些图片或者文件之类的多媒体文件,那在鸿蒙原生应用中,我们怎么开发这样的功能呢? 本文会给大家对这个功能点进行讲解,我们采用的是拉起系统组件来进行图片、文件的选择,拉起系统相机进行拍照的这样一种实现方式。
在文章开始之前,按照惯例,我们先展示本文Demo效果图:

下面我们正式开始讲解。
我们使用Empty 模板创建一个Demo工程。


在src->main->ets 下面创建bean文件夹,在文件夹下创建MediaBean.ts文件
/**
 * 多媒体数据类
 */
export class MediaBean {
  /**
   * 文件名称
   */
  public fileName: string;
  /**
   * 文件大小
   */
  public fileSize: number;
  /**
   * 文件类型
   */
  public fileType: string;
  /**
   * 本地存储地址
   */
  public localUrl: string;
}
 
在src->main->ets 下面创建helper文件夹,在文件夹下创建MediaHelper.ts文件


/**
 * 多媒体辅助类
 */
export class MediaHelper {
  private readonly TAG: string = 'MediaHelper';
  private mContext: common.Context;
  constructor(context: common.Context) {
    this.mContext = context;
  }
  /**
 * 选择图片
   */
  public selectPicture(): Promise {
  }
  
  /**
 * 选择文件
   */
  public selectFile(): Promise {
  }
  /**
 * 拍照
   */
  public async takePhoto(context: common.UIAbilityContext): Promise {
  }
  /**
 * 封装附件实体类
 *  * @param uri 文件路径
   */
  private async buildMediaBean(uri: string): Promise {
  }
  /**
 * 通过Uri查找所选文件信息,插入到MediaBean中
 * @param mediaBean
 * @param uri
   */
  private async appendFileInfoToMediaBean(mediaBean: MediaBean, uri: string) {
  }
}
     
MediaHelper 类定义了5个方法,
通过系统组件选择图片、文件或者拍照之后,系统只是简单的返回一个文件的Uri,如果我们需要展示文件的名称、文件大小、文件类型,需要通过appendFileInfoToMediaBean 方法另外去获取。
下面我们针对这几个方法,增加具体的实现代码:
/** * 选择图片 */ public selectPicture(): Promise{ try { let photoSelectOptions = new picker.PhotoSelectOptions(); photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; photoSelectOptions.maxSelectNumber = 1; let photoPicker = new picker.PhotoViewPicker(); return photoPicker.select(photoSelectOptions) .then((photoSelectResult) => { Log.info(this.TAG, 'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(photoSelectResult)); if (photoSelectResult && photoSelectResult.photoUris && photoSelectResult.photoUris.length > 0) { let filePath = photoSelectResult.photoUris[0]; Log.info(this.TAG, 'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + filePath); return filePath; } }).catch((err) => { Log.error(this.TAG, 'PhotoViewPicker.select failed with err: ' + err); return err; }).then(async (filePath) => { const mediaBean = await this.buildMediaBean(filePath); return mediaBean; }); } catch (err) { Log.error(this.TAG, 'PhotoViewPicker failed with err: ' + err); return Promise.reject(err); } } 
选择图片的功能,我们通过系统组件 picker.PhotoViewPicker 来进行图片选择,通过配置PhotoSelectOptions,指定选择的MIMEType类型(这里PhotoViewMIMETypes.IMAGE_TYPE 图片类型) 、选择的图片最大数量 maxSelectNumber ,这里我们实现单选功能,数值设置为1即可。
使用photoPicker.select 拉起系统组件进行选择,然后在回调中获取图片的uri。
/** * 选择文件 */ public selectFile(): Promise{ try { let documentSelectOptions = new picker.DocumentSelectOptions(); let documentPicker = new picker.DocumentViewPicker(); return documentPicker.select(documentSelectOptions) .then((documentSelectResult) => { Log.info(this.TAG, 'DocumentViewPicker.select successfully, DocumentSelectResult uri: ' + JSON.stringify(documentSelectResult)); if (documentSelectResult && documentSelectResult.length > 0) { let filePath = documentSelectResult[0]; Log.info(this.TAG, 'DocumentViewPicker.select successfully, DocumentSelectResult uri: ' + filePath); return filePath; } }).catch((err) => { Log.error(this.TAG, 'PhotoViewPicker.select failed with err: ' + err); return err; }).then(async (filePath) => { const mediaBean = await this.buildMediaBean(filePath); return mediaBean; }); } catch (err) { Log.error(this.TAG, 'PhotoViewPicker failed with err: ' + err); return Promise.reject(err); } } 
选择文件的功能,我们通过系统组件 picker.DocumentViewPicker来进行文件选择,代码基本是跟图片选择是一样的,区别在于DocumentSelectOptions,目前api9并没有配置项提供,具体关注后续的api版本情况。
/** * 拍照 */ public async takePhoto(context: common.UIAbilityContext): Promise{ let want = { 'uri': '', 'action': wantConstant.Action.ACTION_IMAGE_CAPTURE, 'parameters': {}, }; return context.startAbilityForResult(want) .then((result) => { Log.info(this.TAG, `startAbility call back , ${JSON.stringify(result)}`); if (result.resultCode === 0 && result.want && StringUtils.isNotNullOrEmpty(result.want.uri)) { //拍照成功 Log.info(this.TAG, 'takePhoto successfully, takePhotoResult uri: ' + result.want.uri); return result.want.uri; } }).catch((error) => { Log.info(this.TAG, `startAbility error , ${JSON.stringify(error)}`); return error; }).then(async (uri: string) => { const mediaBean = await this.buildMediaBean(uri); return mediaBean; }); } 
拍照的功能,我们也是拉起相机来进行拍照的,我们使用 startAbilityForResult 方法 + 配置拉起action (wantConstant.Action.ACTION_IMAGE_CAPTURE)的方式拉起系统相机,拍照结束后,在then中接收返回的数据,我们通过返回码result.resultCode 来判断是否进行了拍照,如果状态值===0,说明进行了拍照,我们再使用result.want.uri获取拍照后的照片uri。
/** * 封装多媒体实体类 * * @param uri 文件路径 */ private async buildMediaBean(uri: string): Promise{ if (StringUtils.isNullOrEmpty(uri)) { return null; } const mediaBean: MediaBean = new MediaBean(); mediaBean.localUrl = uri; await this.appendFileInfoToMediaBean(mediaBean, uri); return mediaBean; } 
这个方法的作用主要是封装一个多媒体实体类,并触发appendFileInfoToMediaBean 获取Uri对应文件的一些文件信息。代码很简单,相信大家一目了然。
/**
 * 通过Uri查找所选文件信息,插入到MediaBean中
 * @param mediaBean
 * @param uri
   */
  private async appendFileInfoToMediaBean(mediaBean: MediaBean, uri: string) {
    if (StringUtils.isNullOrEmpty(uri)) {
      return;
    }
    let fileList: Array = [];
    const parts: string[] = uri.split('/');
    const id: string = parts.length > 0 ? parts[parts.length - 1] : '-1';
    try {
      let media = mediaLibrary.getMediaLibrary(this.mContext);
      let mediaFetchOptions: mediaLibrary.MediaFetchOptions = {
        selections: mediaLibrary.FileKey.ID + '= ?',
        selectionArgs: [id],
        uri: uri
      };
      let fetchFileResult = await media.getFileAssets(mediaFetchOptions);
      Log.info(this.TAG, `fileList getFileAssetsFromType fetchFileResult.count = ${fetchFileResult.getCount()}`);
      fileList = await fetchFileResult.getAllObject();
      fetchFileResult.close();
      await media.release();
    } catch (e) {
      Log.error(this.TAG, "query: file data  exception ");
    }
    if (fileList && fileList.length > 0) {
      let fileInfoObj = fileList[0];
      Log.info(this.TAG, `file id = ${JSON.stringify(fileInfoObj.id)} , uri = ${JSON.stringify(fileInfoObj.uri)}`);
      Log.info(this.TAG, `file fileList displayName = ${fileInfoObj.displayName} ,size = ${fileInfoObj.size} ,mimeType = ${fileInfoObj.mimeType}`);
      mediaBean.fileName = fileInfoObj.displayName;
      mediaBean.fileSize = fileInfoObj.size;
      mediaBean.fileType = fileInfoObj.mimeType;
    }
  }
  
这个方法的作用主要是通过uri查询文件的详细信息,包括文件名称、文件大小、文件类型。
这里列下FileAsset的一些字段:
| 名称 | 类型 | 可读 | 可写 | 说明 | 
|---|---|---|---|---|
| id | number | 是 | 否 | 文件资源编号 | 
| uri | string | 是 | 否 | 文件资源uri(如:datashare:///media/image/2) | 
| mimeType | string | 是 | 否 | 文件扩展属性 | 
| mediaType8+ | MediaType | 是 | 否 | 媒体类型 | 
| displayName | string | 是 | 是 | 显示文件名,包含后缀名 | 
| title | string | 是 | 是 | 文件标题 | 
| relativePath8+ | string | 是 | 是 | 相对公共目录路径 | 
| parent8+ | number | 是 | 否 | 父目录id | 
| size | number | 是 | 否 | 文件大小(单位:字节) | 
| dateAdded | number | 是 | 否 | 添加日期(添加文件时间到1970年1月1日的秒数值) | 
| dateModified | number | 是 | 否 | 修改日期(修改文件时间到1970年1月1日的秒数值,修改文件名不会改变此值,当文件内容发生修改时才会更新) | 
| dateTaken | number | 是 | 否 | 拍摄日期(文件拍照时间到1970年1月1日的秒数值) | 
| artist8+ | string | 是 | 否 | 作者 | 
| audioAlbum8+ | string | 是 | 否 | 专辑 | 
| width | number | 是 | 否 | 图片宽度(单位:像素) | 
| height | number | 是 | 否 | 图片高度(单位:像素) | 
| orientation | number | 是 | 是 | 图片显示方向(顺时针旋转角度,如0,90,180 单位:度) | 
| duration8+ | number | 是 | 否 | 持续时间(单位:毫秒) | 
| albumId | number | 是 | 否 | 文件所归属的相册编号 | 
| albumUri8+ | string | 是 | 否 | 文件所归属相册uri | 
| albumName | string | 是 | 否 | 文件所归属相册名称 | 
这里贴下MediaHelper.ts的完整代码
import common from '@ohos.app.ability.common';
import picker from '@ohos.file.picker';
import mediaLibrary from '@ohos.multimedia.mediaLibrary';
import wantConstant from '@ohos.ability.wantConstant';
import { MediaBean } from '../bean/MediaBean';
import { StringUtils } from '../utils/StringUtils';
import { Log } from '../utils/Log';
/**
 * 多媒体辅助类
 */
export class MediaHelper {
  private readonly TAG: string = 'MediaHelper';
  private mContext: common.Context;
  constructor(context: common.Context) {
    this.mContext = context;
  }
  /**
   * 选择图片
   */
  public selectPicture(): Promise {
    try {
      let photoSelectOptions = new picker.PhotoSelectOptions();
      photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
      photoSelectOptions.maxSelectNumber = 1;
      let photoPicker = new picker.PhotoViewPicker();
      return photoPicker.select(photoSelectOptions)
        .then((photoSelectResult) => {
          Log.info(this.TAG, 'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(photoSelectResult));
          if (photoSelectResult && photoSelectResult.photoUris && photoSelectResult.photoUris.length > 0) {
            let filePath = photoSelectResult.photoUris[0];
            Log.info(this.TAG, 'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + filePath);
            return filePath;
          }
        }).catch((err) => {
          Log.error(this.TAG, 'PhotoViewPicker.select failed with err: ' + err);
          return err;
        }).then(async (filePath) => {
          const mediaBean = await this.buildMediaBean(filePath);
          return mediaBean;
        });
    } catch (err) {
      Log.error(this.TAG, 'PhotoViewPicker failed with err: ' + err);
      return Promise.reject(err);
    }
  }
  /**
   * 选择文件
   */
  public selectFile(): Promise {
    try {
      let documentSelectOptions = new picker.DocumentSelectOptions();
      let documentPicker = new picker.DocumentViewPicker();
      return documentPicker.select(documentSelectOptions)
        .then((documentSelectResult) => {
          Log.info(this.TAG, 'DocumentViewPicker.select successfully, DocumentSelectResult uri: ' + JSON.stringify(documentSelectResult));
          if (documentSelectResult && documentSelectResult.length > 0) {
            let filePath = documentSelectResult[0];
            Log.info(this.TAG, 'DocumentViewPicker.select successfully, DocumentSelectResult uri: ' + filePath);
            return filePath;
          }
        }).catch((err) => {
          Log.error(this.TAG, 'PhotoViewPicker.select failed with err: ' + err);
          return err;
        }).then(async (filePath) => {
          const mediaBean = await this.buildMediaBean(filePath);
          return mediaBean;
        });
    } catch (err) {
      Log.error(this.TAG, 'PhotoViewPicker failed with err: ' + err);
      return Promise.reject(err);
    }
  }
  /**
   * 拍照
   */
  public async takePhoto(context: common.UIAbilityContext): Promise {
    let want = {
      'uri': '',
      'action': wantConstant.Action.ACTION_IMAGE_CAPTURE,
      'parameters': {},
    };
    return context.startAbilityForResult(want)
      .then((result) => {
        Log.info(this.TAG, `startAbility call back , ${JSON.stringify(result)}`);
        if (result.resultCode === 0 && result.want && StringUtils.isNotNullOrEmpty(result.want.uri)) {
          //拍照成功
          Log.info(this.TAG, 'takePhoto successfully, takePhotoResult uri: ' + result.want.uri);
          return result.want.uri;
        }
      }).catch((error) => {
        Log.info(this.TAG, `startAbility error , ${JSON.stringify(error)}`);
        return error;
      }).then(async (uri: string) => {
        const mediaBean = await this.buildMediaBean(uri);
        return mediaBean;
      });
  }
  /**
   * 封装多媒体实体类
   *
   * @param uri 文件路径
   */
  private async buildMediaBean(uri: string): Promise {
    if (StringUtils.isNullOrEmpty(uri)) {
      return null;
    }
    const mediaBean: MediaBean = new MediaBean();
    mediaBean.localUrl = uri;
    await this.appendFileInfoToMediaBean(mediaBean, uri);
    return mediaBean;
  }
  /**
   * 通过Uri查找所选文件信息,插入到MediaBean中
   * @param mediaBean
   * @param uri
   */
  private async appendFileInfoToMediaBean(mediaBean: MediaBean, uri: string) {
    if (StringUtils.isNullOrEmpty(uri)) {
      return;
    }
    let fileList: Array = [];
    const parts: string[] = uri.split('/');
    const id: string = parts.length > 0 ? parts[parts.length - 1] : '-1';
    try {
      let media = mediaLibrary.getMediaLibrary(this.mContext);
      let mediaFetchOptions: mediaLibrary.MediaFetchOptions = {
        selections: mediaLibrary.FileKey.ID + '= ?',
        selectionArgs: [id],
        uri: uri
      };
      let fetchFileResult = await media.getFileAssets(mediaFetchOptions);
      Log.info(this.TAG, `fileList getFileAssetsFromType fetchFileResult.count = ${fetchFileResult.getCount()}`);
      fileList = await fetchFileResult.getAllObject();
      fetchFileResult.close();
      await media.release();
    } catch (e) {
      Log.error(this.TAG, "query: file data  exception ");
    }
    if (fileList && fileList.length > 0) {
      let fileInfoObj = fileList[0];
      Log.info(this.TAG, `file id = ${JSON.stringify(fileInfoObj.id)} , uri = ${JSON.stringify(fileInfoObj.uri)}`);
      Log.info(this.TAG, `file fileList displayName = ${fileInfoObj.displayName} ,size = ${fileInfoObj.size} ,mimeType = ${fileInfoObj.mimeType}`);
      mediaBean.fileName = fileInfoObj.displayName;
      mediaBean.fileSize = fileInfoObj.size;
      mediaBean.fileType = fileInfoObj.mimeType;
    }
  }
}
      
上面的代码,在api9实测是可以正常使用的,但是有一些API被标记为过期,有一些在官方文档注明即将停用,但是我没有找到可以平替的API,如果有读者知道的,麻烦评论区告诉我一声,谢谢。
官方提示让我们切换到 ohos.app.ability.wantConstant这个类下,可是我们用到wantConstant.Action,这个Action在 ohos.app.ability.wantConstant中没有定义,我在SDK中也没有找到Action在哪一个类中定义;
我们需要使用getMediaLibrary获取多媒体对象,调用getFileAssets查询文件的多媒体信息,官方提示让我们使用ohos.file.picker,可笑的是picker中没有getFileAssets 相关的方法,那我们通过picker只能拿到一个文件的Uri,文件名称、文件大小这些常规的文件相关的数据都拿不到,那功能都无法开发,这也是我之前的一个疑问。
我们读取文件的多媒体信息需要申请一个多媒体的读取权限 ohos.permission.READ_MEDIA,这个权限需要在
module.json5中添加配置requestPermissions,在该节点下配置READ_MEDIA权限,具体如下图:

由于这个READ_MEDIA权限需要进行动态权限申请,因为还需要我们进行动态权限申请代码逻辑开发,这里由于篇幅原因,我就不过多赘述,后续如果对这块动态权限申请有不明白的地方,我再重新写一篇文章介绍,讲下动态申请权限,跳转系统权限设置页配置权限这些功能具体如何实现。
这次的Demo,我们直接安装后,在系统设置中找到应用,把对应的权限开启即可(绕过权限动态申请)。
下面我们编写UI页面,使用我们上面的MediaHelper工具类选择图片、拍照,并将图片显示出来。

我们在Index.ets文件中放三个按钮,以及显示文件名称、大小、文件类型以及文件路径、显示图片的控件。
完整的代码如下:
import common from '@ohos.app.ability.common';
import { MediaBean } from '../bean/MediaBean';
import { MediaHelper } from '../helper/MediaHelper';
@Entry
@Component
struct Index {
  @State mediaBean: MediaBean = new MediaBean();
  private mediaHelper: MediaHelper = new MediaHelper(getContext());
  build() {
    Row() {
      Column() {
        Text('选择图片')
          .textAlign(TextAlign.Center)
          .width(200)
          .fontSize(16)
          .padding(10)
          .margin(20)
          .border({ width: 0.5, color: '#ff38f84b', radius: 15 })
          .onClick(() => {
            this.handleClick(MediaOption.Picture)
          })
        Text('选择文件')
          .textAlign(TextAlign.Center)
          .width(200)
          .fontSize(16)
          .padding(10)
          .margin(20)
          .border({ width: 0.5, color: '#ff38f84b', radius: 15 })
          .onClick(() => {
            this.handleClick(MediaOption.File)
          })
        Text('拍照')
          .textAlign(TextAlign.Center)
          .width(200)
          .fontSize(16)
          .padding(10)
          .margin(20)
          .border({ width: 0.5, color: '#ff38f84b', radius: 15 })
          .onClick(() => {
            this.handleClick(MediaOption.TakePhoto)
          })
        Divider()
          .width('100%')
          .height(0.5)
          .color('#ff99f6a2')
          .margin({ top: 20 })
          .padding({ left: 20, right: 20 })
        Text(`文件名称: ${this.mediaBean.fileName ? this.mediaBean.fileName : ''}`)
          .textAlign(TextAlign.Center)
          .width('100%')
          .fontSize(16)
          .margin(10)
        Text(`文件大小: ${this.mediaBean.fileSize ? this.mediaBean.fileSize : ''}`)
          .textAlign(TextAlign.Center)
          .width('100%')
          .fontSize(16)
          .margin(10)
        Text(`文件类型: ${this.mediaBean.fileType ? this.mediaBean.fileType : ''}`)
          .textAlign(TextAlign.Center)
          .width('100%')
          .fontSize(16)
          .margin(10)
        Text(`文件Uri: ${this.mediaBean.localUrl ? this.mediaBean.localUrl : ''}`)
          .textAlign(TextAlign.Center)
          .width('100%')
          .fontSize(16)
          .margin(10)
        Image(this.mediaBean.localUrl)
          .width(300)
          .height(300)
          .backgroundColor(Color.Grey)
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
  async handleClick(option: MediaOption) {
    let mediaBean: MediaBean;
    switch (option) {
      case MediaOption.Picture:
        mediaBean = await this.mediaHelper.selectPicture();
        break;
      case MediaOption.File:
        mediaBean = await this.mediaHelper.selectFile();
        break;
      case MediaOption.TakePhoto:
        mediaBean = await this.mediaHelper.takePhoto(getContext() as common.UIAbilityContext);
        break;
      default:
        break;
    }
    if (mediaBean) {
      this.mediaBean = mediaBean;
    }
  }
}
enum MediaOption {
  Picture = 0,
  File = 1,
  TakePhoto = 2
}
 
打包安装到真机上,需要我们给项目配置签名信息。我们点击File -> Project Structure ->Project ,选择 Signing Configs面板,勾选 Support HarmonyOS 跟Automatically generate signature,自动生成调试签名,生成完毕后,运行安装到手机上。
使用拍照功能时,请使用真机运行,如果使用的本地模拟器运行,拍照后返回,uri可能会返回“”。
注意:由于我们没有实现多媒体读取权限动态申请权限,因此需要在手机系统设置-应用中找到该应用,开启多媒体权限,该权限默认是禁止的,开启后再打开应用操作即可。运行的具体的效果如文章开头贴图展示一般。
文本到此完毕,有疑问的请在评论区留言交流,谢谢阅读。