系列文章目录
HarmonyOS Next 系列之省市区弹窗选择器实现(一)
HarmonyOS Next 系列之验证码输入组件实现(二)
HarmonyOS Next 系列之底部标签栏TabBar实现(三)
HarmonyOS Next 系列之HTTP请求封装和Token持久化存储(四)
HarmonyOS Next 系列之从手机选择图片或拍照上传功能实现(五)
文章目录
- 系列文章目录
- 前言
- 一、实现步骤总结
- 二、代码实现
-
- 1.媒体读写权限检查和申请
- 2.从手机存储选择图片或拍照
- 3.复制图片到缓存目录下
- [4. 接口请求上传图片](#4. 接口请求上传图片)
- 三、完整代码
前言
HarmonyOS Next(基于API11)实现从手机选择图片或拍照上传功能,常用于头像上传等操作
一、实现步骤总结
1、媒体读写权限检查和申请
2、从手机存储选择图片或拍照
3、把图片复制到缓存目录
4、接口请求上传图片
分析说明:
图片上传使用API request.uploadFile 而该api上传文件的本地路径只支持internal协议
所以选择完图片/或拍照后需要把图片从内部存储复制到cache目录下,该操作需要外部存储设备媒体读写权限,且是用户级别的权限,因此每次复制图片前需要检查权限如果没权限需弹窗口让用户授权,最后在通过该api实现上传
二、代码实现
1.媒体读写权限检查和申请
(1)检查权限
工具类文件:
javascript
import bundleManager from '@ohos.bundle.bundleManager';
import abilityAccessCtrl, { Context, Permissions } from '@ohos.abilityAccessCtrl';
//校验应用是否授予权限
//@params permissions:权限名称数组
//@return permissionabilityAccessCtrl:权限名称
async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
let atManager = abilityAccessCtrl.createAtManager();
let grantStatus: abilityAccessCtrl.GrantStatus = 0;
// 获取应用程序的accessTokenID
let tokenId: number = 0;
try {
let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
tokenId = appInfo.accessTokenId;
} catch (err) {
console.error(`getBundleInfoForSelf failed, code is ${err.code}, message is ${err.message}`);
}
// 校验应用是否被授予权限
try {
grantStatus = await atManager.checkAccessToken(tokenId, permission);
} catch (err) {
console.error(`checkAccessToken failed, code is ${err.code}, message is ${err.message}`);
}
return grantStatus;
}
//检查用户权限
//@params permissions:权限名称数组
export async function checkPermissions(permissions: Permissions): Promise<boolean> {
try {
let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permissions);
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}
catch (e) {
return Promise.reject(e)
}
}
调用:
javascript
const READ_MEDIA_PERMISSION: Permissions = 'ohos.permission.READ_MEDIA' //媒体读取权限
const WRITE_MEDIA_PERMISSION: Permissions = 'ohos.permission.WRITE_MEDIA' //媒体写入权限
let permissionList: Permissions[] = []; //需要申请选项列表
let readPermission = await checkPermissions(READ_MEDIA_PERMISSION)//检查是否有媒体读取权限
!readPermission && permissionList.push(READ_MEDIA_PERMISSION)
let writePermission = await checkPermissions(WRITE_MEDIA_PERMISSION)//检查是否有媒体写入权限
!writePermission && permissionList.push(READ_MEDIA_PERMISSION)
(2)申请权限
工具类文件:
javascript
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import common from '@ohos.app.ability.common'
interface rejectObj {
code: number
message: string
}
/**
* 申请权限
* @params context:AblitiyContext
* @params permissions:权限名称数组
* @returns Promise<boolean>:是否授权成功
*/
export async function applyPermission(context: common.UIAbilityContext, permissions: Array<Permissions>): Promise<boolean> {
let atManager = abilityAccessCtrl.createAtManager();
return new Promise((resolve: (res: boolean) => void, reject: (e: rejectObj) => void) => {
atManager.requestPermissionsFromUser(context, permissions).then((data) => {
let grantStatus: Array<number> = data.authResults;
resolve(grantStatus.every(item => item === 0))
}).catch((err: rejectObj) => {
reject(err)
})
})
}
调用:
javascript
private context = getContext(this) as common.UIAbilityContext; //UIAbilityContext
.....
.....
.....
//申请权限
let res: boolean = await applyPermission(this.context, permissionList)
if (!res) {//用户未同意授权
AlertDialog.show({
title: "提示",
message: "无权限读写用户外部存储中的媒体文件信息,请前往系统设置开启",
alignment: DialogAlignment.Center,
secondaryButton: {
value: '关闭',
action: () => {
}
}
})
}
2.从手机存储选择图片或拍照
(1)从手机存储选择图片
javascript
import picker from '@ohos.file.picker';
....
....
//从相册选择
let PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
let photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then(async (PhotoSelectResult) => {
if (PhotoSelectResult.photoUris.length) {
console.log(`图片本地路径:${PhotoSelectResult.photoUris[0]}`)
}
})
(2)拍照
javascript
import camera from '@ohos.multimedia.camera';
import camerapicker from '@ohos.multimedia.cameraPicker';
import { BusinessError } from '@ohos.base';
....
....
try{
let pickerProfile: camerapicker.PickerProfile = {
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
};
let pickerResult: camerapicker.PickerResult = await camerapicker.pick(this.context,
[camerapicker.PickerMediaType.PHOTO, camerapicker.PickerMediaType.PHOTO], pickerProfile);
} catch (error) {
let err = error as BusinessError;
console.error(`the pick call failed. error code: ${err.code}`);
}
3.复制图片到缓存目录下
默认复制图片到缓存目录cache根路径下,移动后文件名前面加上当前时间戳区分:timestamep+原name.格式
javascript
import fs from '@ohos.file.fs';
/**
* 复制文件到缓存目录下
* @param path :文件路径
* @param context :Context
* @returns Promise<string> 移动后文件路径
*/
export async function copyFileToCache(path: string,context:Context): Promise<string> {
try {
let file = fs.openSync(path, fs.OpenMode.READ_WRITE)
if (file) {
let fileDir: string = `${context.cacheDir}` //临时文件目录
//时间戳生成随机文件名
let newPath: string = `${new Date().getTime()}_${path.split("/")[path.split("/").length-1]}`
let targetPath: string = `${fileDir}/${newPath}`
fs.copyFileSync(file.fd, targetPath)
return newPath
}
else {
return ''
}
} catch (e) {
return Promise.resolve('')
}
}
4. 接口请求上传图片
javascript
//开始上传图片 path:图片路径后缀(图片名称)
async uploadImage(path: string) {
let uri=`internal://cache/${path}` //上传图片全路径
let uploadConfig: request.UploadConfig = {
url:"http://xxxxxxx",
header:{},
method: "POST",
files: [{ filename: path, name: "file", uri, type: path.split('.')[path.split('.').length-1] }],
data: [],
};
try {
let uploadTask:request.UploadTask=await request.uploadFile(this.context, uploadConfig)
//上传中回调
uploadTask.on('progress', (size,total) => {
console.log(size.toString(),total.toString(),'上传进度')
})
//上传完成回调
uploadTask.on('complete', (taskStates: request.TaskState[]) => {
console.info("upOnComplete complete taskState:" + JSON.stringify(taskStates));
})
//上传失败回调
uploadTask.on('fail', (taskStates: request.TaskState[]) => {
console.info("upOnComplete fail taskState:" + JSON.stringify(taskStates));
})
}catch (e){
console.log( JSON.stringify(e),'e')
}
}
需要注意的是我们在复制图片步骤中通过context.cacheDir获取到的缓存目录路径如下所示:
javascript
"path":"/data/storage/el2/base/haps/entry/cache/1717854801890_IMG_20240603_170235.jpg"
需转换成internal协议路径,
前面 "/data/storage/el2/base/haps/entry/cache"实际等价于"internal://cache"
所以在上传接口拼接uri参数时候只需要知道图片名称+格式即可,最终上传参数拼接后路径为internal://cache/1717854801890_IMG_20240603_170235.jpg
三、完整代码
完整代码将封装一个完整的组件,自定义底部弹窗菜单选择拍照或从手机相册选择,选完自动上传。
代码目录结构
utils/index.ets(工具类):
javascript
import fs from '@ohos.file.fs';
import bundleManager from '@ohos.bundle.bundleManager';
import abilityAccessCtrl, { Context, Permissions } from '@ohos.abilityAccessCtrl';
import common from '@ohos.app.ability.common'
/**
* 复制文件到缓存目录下
* @param path :文件路径
* @param context :Context
* @returns Promise<string> 移动后文件路径
*/
export async function copyFileToCache(path: string,context:Context): Promise<string> {
try {
let file = fs.openSync(path, fs.OpenMode.READ_WRITE)
if (file) {
let fileDir: string = `${context.cacheDir}` //临时文件目录
//时间戳生成随机文件名
let newPath: string = `${new Date().getTime()}_${path.split("/")[path.split("/").length-1]}`
let targetPath: string = `${fileDir}/${newPath}`
fs.copyFileSync(file.fd, targetPath)
return newPath
}
else {
return ''
}
} catch (e) {
return Promise.resolve('')
}
}
//校验应用是否授予权限
//@params permissions:权限名称数组
//@return permissionabilityAccessCtrl:权限名称
async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
let atManager = abilityAccessCtrl.createAtManager();
let grantStatus: abilityAccessCtrl.GrantStatus = 0;
// 获取应用程序的accessTokenID
let tokenId: number = 0;
try {
let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
tokenId = appInfo.accessTokenId;
} catch (err) {
console.error(`getBundleInfoForSelf failed, code is ${err.code}, message is ${err.message}`);
}
// 校验应用是否被授予权限
try {
grantStatus = await atManager.checkAccessToken(tokenId, permission);
} catch (err) {
console.error(`checkAccessToken failed, code is ${err.code}, message is ${err.message}`);
}
return grantStatus;
}
//检查用户权限
//@params permissions:权限名称数组
export async function checkPermissions(permissions: Permissions): Promise<boolean> {
try {
let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permissions);
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}
catch (e) {
return Promise.reject(e)
}
}
interface rejectObj {
code: number
message: string
}
/**
* 申请权限
* @params context:AblitiyContext
* @params permissions:权限名称数组
* @returns Promise<boolean>:是否授权成功
*/
export async function applyPermission(context: common.UIAbilityContext, permissions: Array<Permissions>): Promise<boolean> {
let atManager = abilityAccessCtrl.createAtManager();
return new Promise((resolve: (res: boolean) => void, reject: (e: rejectObj) => void) => {
atManager.requestPermissionsFromUser(context, permissions).then((data) => {
let grantStatus: Array<number> = data.authResults;
resolve(grantStatus.every(item => item === 0))
}).catch((err: rejectObj) => {
reject(err)
})
})
}
ImageUploadDialog.ets(图片上传弹窗菜单选择组件):
javascript
import picker from '@ohos.file.picker';
import { checkPermissions, applyPermission, copyFileToCache } from '../../utils/index'
import { request } from '@kit.BasicServicesKit';
import { Permissions } from '@ohos.abilityAccessCtrl';
import camera from '@ohos.multimedia.camera';
import camerapicker from '@ohos.multimedia.cameraPicker';
import { BusinessError } from '@ohos.base';
import { common } from '@kit.AbilityKit';
@Extend(Text)
function custText() {
.width('100%')
.height('48')
.fontColor('#39364D')
.textAlign(TextAlign.Center)
}
@CustomDialog
export default struct ImageUploadDialog {
dialogController: CustomDialogController
@Prop uploadURL:string='';//上传接口地址
private context = getContext(this) as common.UIAbilityContext; //UIAbilityContext
private success:(res: request.TaskState[])=>void=()=>{}//上传成功回调
private fail:(res: request.TaskState[])=>void=()=>{} //上传失败回调
//检查权限
async checkAppPermission(): Promise<boolean> {
try {
const READ_MEDIA_PERMISSION: Permissions = 'ohos.permission.READ_MEDIA' //媒体读取权限
const WRITE_MEDIA_PERMISSION: Permissions = 'ohos.permission.WRITE_MEDIA' //媒体写入权限
let permissionList: Permissions[] = []; //需要申请选项列表
let readPermission = await checkPermissions(READ_MEDIA_PERMISSION)//检查是否有媒体读取权限
!readPermission && permissionList.push(READ_MEDIA_PERMISSION)
let writePermission = await checkPermissions(WRITE_MEDIA_PERMISSION)//检查是否有媒体写入权限
!writePermission && permissionList.push(READ_MEDIA_PERMISSION)
if (permissionList.length) {
//申请权限
let res: boolean = await applyPermission(this.context, permissionList)
if (!res) {//用户未同意授权
AlertDialog.show({
title: "提示",
message: "无权限读写用户外部存储中的媒体文件信息,请前往系统设置开启",
alignment: DialogAlignment.Center,
secondaryButton: {
value: '关闭',
action: () => {
}
}
})
}
return res
}
return true
}
catch (e) {
return Promise.reject(e)
}
}
//开始上传图片 path:图片路径后缀(图片名称)
async uploadImage(path: string) {
console.log(path, 'path')
let uri=`internal://cache/${path}` //上传图片全路径
let uploadConfig: request.UploadConfig = {
url:this.uploadURL,
header:{},
method: "POST",
files: [{ filename: path, name: "file", uri, type: path.split('.')[path.split('.').length-1] }],
data: [],
};
try {
let uploadTask:request.UploadTask=await request.uploadFile(this.context, uploadConfig)
//上传中回调
uploadTask.on('progress', (size,total) => {
console.log(size.toString(),total.toString(),'上传进度')
})
//上传完成回调
uploadTask.on('complete', (taskStates: request.TaskState[]) => {
console.info("upOnComplete complete taskState:" + JSON.stringify(taskStates));
if(taskStates&&taskStates.length&& taskStates[0].responseCode===0){
this.success&&this.success(taskStates)
}
})
//上传失败回调
uploadTask.on('fail', (taskStates: request.TaskState[]) => {
console.info("upOnComplete fail taskState:" + JSON.stringify(taskStates));
this.fail&&this.fail(taskStates)
})
}catch (e){
console.log( JSON.stringify(e),'e')
}
}
build() {
Column() {
//拍照
Text('拍照').custText().onClick(async()=>{
//检查是否有读写外部媒体权限
let res: boolean = await this.checkAppPermission()
//无权限返回
if (!res) return
try {
let pickerProfile: camerapicker.PickerProfile = {
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
};
let pickerResult: camerapicker.PickerResult = await camerapicker.pick(this.context,
[camerapicker.PickerMediaType.PHOTO, camerapicker.PickerMediaType.PHOTO], pickerProfile);
if(pickerResult?.resultUri){
//关闭弹窗
this.dialogController.close()
//复制图片到缓存目录(缓存目录才有读写权限)
let filePath = await copyFileToCache(pickerResult.resultUri, this.context)
if (filePath) {
//上传头像并设置
this.uploadImage(filePath)
}
}
} catch (error) {
let err = error as BusinessError;
console.error(`the pick call failed. error code: ${err.code}`);
}
})
Divider().color('#F7F9FA').width('100%').strokeWidth(1)
//从手机相册选择
Text('从手机相册选择').custText().onClick(async () => {
//检查是否有读写外部媒体权限
let res: boolean = await this.checkAppPermission()
//无权限返回
if (!res) return
//关闭弹窗
this.dialogController.close()
//从相册选择
let PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
let photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then(async (PhotoSelectResult) => {
if (PhotoSelectResult.photoUris.length) {
//复制图片到缓存目录(缓存目录才有读写权限)
let filePath = await copyFileToCache(PhotoSelectResult.photoUris[0],this.context)
if (filePath) {
this.uploadImage(filePath)
}
}
})
})
Button('取消', { type: ButtonType.Capsule })
.backgroundColor('#F7F7F7')
.fontSize('16fp')
.fontColor('#333333')
.width('100%')
.margin({ top: '30' })
.onClick(() => {
this.dialogController.close()
})
}.width('100%').padding({ left: '16', top: '11', right: '16', bottom: '16' })
.backgroundColor(Color.White)
.borderRadius({
topLeft: '24',
topRight: '24'
})
}
}
组件入参 说明:
javascript
uploadURL:上传接口url
success:(res: request.TaskState[])=>void 上传成功回调函数
fail:(res: request.TaskState[])=>void=()=>{} //上传失败回调
成功或失败回调参数说明
javascript
request.TaskState[]: {
path:string //图片在本地路径
message:string //上传结果信息
responseCode //上传结果状态码 0:成功,其他值失败
}[]
从 request.TaskState字段描述可以看出request.uploadFile无法返回接口返回的数据,这也是最大的坑,期待官方解决,如果需要获取上传成功后返回的url,可以在设计个接口上传完再调用该接口获取图片url,如果像头像设置这种功能也可以把图片上传和头像设置整合成一个接口,上传完也即设置完成。
页面调用:
pages/Index
javascript
import ImageUploadDialog from '../components/ImageUploadDialog/ImageUploadDialog'
import { promptAction } from '@kit.ArkUI'
@Entry
@Component
struct Index {
@State dialogController: CustomDialogController | null = null //选择上传类型弹窗控制器
aboutToAppear(): void {
this.dialogController= new CustomDialogController({
builder: ImageUploadDialog({
uploadURL: 'http://xxxxxxxxx',//上传地址
success:e=>{//上传成功回调
console.log(JSON.stringify(e))
promptAction.showToast({message:'上传成功'})
},
fail:e=>{//上传失败回调
console.log(JSON.stringify(e))
promptAction.showToast({message:'上传失败'})
}
}),
alignment: DialogAlignment.Bottom,//弹窗居于底部
customStyle: true,//自定义样式
})
}
build() {
Column(){
Button('上传').onClick(()=>{
this.dialogController?.open()
})
}.width('100%')
}
}
最后不要忘记添加权限
三个:
javascript
"ohos.permission.INTERNET":网访问权限
"ohos.permission.READ_MEDIA":外部存储设备媒体读取权限
"ohos.permission.WRITE_MEDIA":外部存储设备媒体写入权限
module.json5:
javascript
//权限
requestPermissions: [{
"name": "ohos.permission.INTERNET",
},{
"name": "ohos.permission.READ_MEDIA",
"reason": "$string:reasonReadWriteMedia",//使用权限原因
"usedScene": {
"abilities": [//使用的该权限的EntryAbility名称数组
"EntryAbility"
],
"when": "inuse"
}
},{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "$string:reasonReadWriteMedia",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}]
entry\src\main\resources\base\element\string.json
javascript
{
"string": [
,{
"name":"reasonReadWriteMedia",
"value": "上传头像"
}
]
}
效果: