英才App中拍照/相册组件实践(一)-本地文件获取

66 阅读4分钟

一、前言

在日常的手机应用开发过程中,经常会遇到上传图片的需求,像上传头像、上传作品集、投诉上传图片等功能。在英才App开发中,native侧需要用到头像选择和上传功能,同时FE侧也需要用到简历作品集和投诉图片上报功能,因此为了方便各端使用,需要对该处业务抽象出一个功能模块,并提供通用的调用方式。

本章将重点介绍拍照功能的实现。

二、实现

拍照/相册组件提供了两种方式来获取图片:

  1. 打开相机拍照
  2. 从图库中选择图片

2.1 相机拍照模块

使用系统相机实现,主要实现方式为:

1、权限申请

2、打开相机摄像头

打开摄像头可以通过设置intent来实现,主要代码如下:

private fun startCamera() {        /**         * 设置拍照得到的照片的储存目录,因为我们访问应用的缓存路径并不需要读写内存卡的申请权限,         * 因此,这里为了方便,将拍照得到的照片存在这个缓存目录中         */        try {            tempFile = File.createTempFile(                "IMG_${Date().time}",                ".jpg",                getExternalFilesDir(Environment.DIRECTORY_PICTURES)            )        } catch (e: IOException) {            e.printStackTrace()        }        /**fileProvider         * 因 Android 7.0 开始,不能使用 file:// 类型的 Uri 访问跨应用文件,否则报异常,         * 因此我们这里需要使用内容提供器,FileProvider 是 ContentProvider 的一个子类,         * 我们可以轻松的使用 FileProvider 来在不同程序之间分享数据(相对于 ContentProvider 来说)         */        // 打开系统相机的 Action,等同于:"android.media.action.IMAGE_CAPTURE"        val takePhotoIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {            photoUri = FileProvider.getUriForFile(                this,                this.application.getPackageName() + ".fileProvider",                tempFile            )            takePhotoIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)        } else {            photoUri = Uri.fromFile(tempFile) // Android 7.0 以前使用原来的方法来获取文件的 Uri        }        // 设置拍照所得照片的输出目录        takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)        startActivityForResult(takePhotoIntent, TAKE_PHOTO_REQUEST_CODE)    }.

同时需要在AndroidManifest.xml清单文件中对FileProvider进行注册

<provider            android:name="androidx.core.content.FileProvider"            android:authorities="${applicationId}.fileProvider"            android:exported="false"            android:grantUriPermissions="true"            tools:replace="android:authorities">            <meta-data                android:name="android.support.FILE_PROVIDER_PATHS"                android:resource="@xml/file_path"                tools:replace="android:resource"></meta-data>        </provider>

使用FileProvider获取某个目录下文件的uri,还需要在res下创建对应xml文件,声明目录范围

<?xml version="1.0" encoding="utf-8"?><paths><!--    name 属性可以随便填,path 属性代表FileProvider 共享的文件路径,空字符串代表共享 sd 卡上的所有文件-->    <external-path name="image" path="." /></paths>

3、拍照后传回数据处理

在拍照结束后,我们可以在OnActivityResult回调中对回传的intent及携带的数据进行处理,主要代码如下:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {        super.onActivityResult(requestCode, resultCode, data)        if (resultCode == Activity.RESULT_OK) {            // 通过返回码判断是哪个应用返回的数据            when (requestCode) {                TAKE_PHOTO_REQUEST_CODE -> {                    if (needCrop) {                        cropPhoto(photoUri!!)                    } else {                        dealWithChooseFile(tempFile)                    }                }                CROP_PHOTO_REQUEST_CODE -> {                    dealWithChooseFile()                }            }        }    }

根据业务需要,提供了2种图片处理方式:裁剪和非裁剪。

裁剪方式实现为:在拿到相机拍照完成后的图片地址后,调用系统的裁剪功能,根据产品需要设置裁剪框的比例和缩放等,以及设置裁剪后的输出目录,主要实现代码如下

private fun cropPhoto(inputUri: Uri) {        // 调用系统裁剪图片的 Action        val cropPhotoIntent = Intent("com.android.camera.action.CROP")        // 设置数据Uri 和类型        cropPhotoIntent.setDataAndType(inputUri, "image/*")        // 授权应用读取 Uri,这一步要有,不然裁剪程序会崩溃        cropPhotoIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)        cropPhotoIntent.putExtra("circleCrop", true)        // 裁剪框的比例,1:1        cropPhotoIntent.putExtra("aspectX", 1)        cropPhotoIntent.putExtra("aspectY", 1)        // 让裁剪框支持缩放        cropPhotoIntent.putExtra("scale", true)        // 设置图片的最终输出目录        cropPhotoIntent.putExtra(            MediaStore.EXTRA_OUTPUT,            Uri.parse("file:////sdcard/" + "${Date().time}.jpg").also {                photoOutputUri = it            })        startActivityForResult(cropPhotoIntent, CROP_PHOTO_REQUEST_CODE)    }

裁剪完成后,会在OnActivityResult回调方法中通知裁剪结果

private fun dealWithChooseFile() {        val file = File(photoOutputUri?.path ?: "")    }

非裁剪方式注意不要使用PhotoUri来获取文件,会报找不到文件错误。直接使用我们定义的图片存储文件tempFile即可。

2.2 自定义相册模块

1)系统相册实现

在介绍自定义相册实现之前,先介绍下系统相册的调用

1、权限申请

2、调起系统相册

private void choiceFromAlbum() {        // 打开系统图库选择图片        Intent picture = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);        startActivityForResult(picture, ALBUM_REQUEST_CODE);    }

3、OnActivityResult回调方法中接收系统相册返回的数据

public void onActivityResult(int requestCode, int resultCode, Intent data) {        if (resultCode == RESULT_OK) {            switch (requestCode) {                case CHOICE_FROM_ALBUM_REQUEST_CODE:                    Uri uri = data.getData();                    break;        }    }
}

getData()返回的就是相册中照片存放的Uri

2)自定义相册实现

由于系统相册只能选取单张,在英才简历上传多作品集图片时交互体验很不好,因此需要开发一个自定义相册功能,以满足同时选择多张图片上传的需求。自定义相册实现主要分以下几个步骤:

1、权限申请

2、自定义相册布局及数据获取

自定义相册选择中会创建一个相册Activity来展示,图片列表使用RecyclerView来加载数据,通过Glide或者fresco来展示图片,具体代码不难实现,这里不展现了。

3、相册Adapter数据集获取

核心代码如下:

private val QUERY_URI = MediaStore.Files.getContentUri("external")private val PROJECTION = arrayOf(            COLUMN_ID,            COLUMN_NAME,            COLUMN_MIME_TYPE,            COLUMN_PATH,            COLUMN_SIZE,            COLUMN_DURATION        )
try {            cursor = ContentResolverCompat.query(                context.contentResolver,                QUERY_URI,                PROJECTION,                selectionStr                ORDER_BY,                null            )            if (cursor != null) {                while (cursor.moveToNext()) {                    val fileId = cursor.getLong(cursor.getColumnIndex(COLUMN_ID))                    val filePath = cursor.getString(cursor.getColumnIndex(COLUMN_PATH))                    val mimeType = cursor.getString(cursor.getColumnIndex(COLUMN_MIME_TYPE))                    val size = cursor.getLong(cursor.getColumnIndex(COLUMN_SIZE))                    val duration = cursor.getLong(cursor.getColumnIndex(COLUMN_DURATION))                    val mediaModel = AlbumMediaModel(fileId, filePath, mimeType, size, duration)                    mediaList.add(mediaModel)                }            }        } catch (e: Exception) {        } finally {            cursor?.close()        }

主要思想就是查图片数据库中对应fold的数据表,获取表中的所有图片数据,包括图片的uri、path、name等我们需要的信息。

三、总结

本文主要介绍了如何通过系统方式调用相机和相册以及自定义相册的实现思路,以帮助对这块功能不熟悉的同学提供解决思路。如果对某部分实现不理解的同学,欢迎一起探讨。