一、需求背景
在常规的App开发中,很多时候需要用户上传图片来进行一些业务上的实现,例如用户反馈,图片凭证等。
二、实现功能
1.选择弹窗(即选择拍照或者相册)
2.申请权限(相机权限)
3.相机拍照回调,图片处理
4.相册选择回调,图片处理
5.图片压缩,上传服务器
三、实现步骤
1.选择弹窗
1.1 弹窗选择
PhotoSelectDialog.Builder(this)
.setCallClickListener(object : PhotoSelectDialog.PhotoSelectListener {
override fun clickPhoto(dialog: Dialog) {
toCheckPermission()
dialog.dismiss()
}
override fun clickAlbum(dialog: Dialog) {
dispatchChoosePictureIntent()
dialog.dismiss()
}
}).create().show()
1.2 弹窗的代码
class PhotoSelectDialog(context: Context, themeStyle: Int) : Dialog(context, themeStyle) {
init {
initView()
}
private fun initView() {
setContentView(R.layout.iamge_select_dialog)
}
class Builder(private val context: Context) {
private var photoSelectListener: PhotoSelectListener? = null
fun setCallClickListener(photoSelectListener: PhotoSelectListener): Builder {
this.photoSelectListener = photoSelectListener
return this
}
fun create(): PhotoSelectDialog {
val dialog = PhotoSelectDialog(context, R.style.CustomDialogStyle)
dialog.setCancelable(true)
dialog.setCanceledOnTouchOutside(true)
if (photoSelectListener != null) {
dialog.findViewById<AppCompatImageView>(R.id.toSelectImage).setOnClickListener {
photoSelectListener?.clickAlbum(dialog)
}
dialog.findViewById<AppCompatImageView>(R.id.toTakePhoto).setOnClickListener {
photoSelectListener?.clickPhoto(dialog)
}
dialog.findViewById<AppCompatImageView>(R.id.dialog_close).setOnClickListener {
dialog.dismiss()
}
}
return dialog
}
}
interface PhotoSelectListener {
fun clickPhoto(dialog: Dialog)
fun clickAlbum(dialog: Dialog)
}
}
1.3 弹窗的展示
2.申请相机权限
众所周知,像调用相机是必须要有权限。
2.1在manifest进行声明
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
2.2 申请权限
我使用的是EasyPermission框架,也可以自己写。
private fun toCheckPermission() {
if (!EasyPermissions.hasPermissions( this,android.Manifest.permission.CAMERA,) ) {
EasyPermissions.requestPermissions(
this,
getString(R.string.permission_tips),
AppConstant.PER_CAMERA,
android.Manifest.permission.CAMERA,
)
} else {
dispatchTakePictureIntent()//打开相机
}
}
相关权限回调处理,可以去看我的另一篇博客
Android Permission 权限申请,EasyPermission和其他三方库-CSDN博客
3.相机拍照回调,图片处理
3.1 调起相机
private val REQUEST_IMAGE_CAPTURE = 1//请求码
// 启动相机并捕获照片的函数
private fun dispatchTakePictureIntent() {
// 创建一个用于调用系统相机的 Intent
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
// 检查是否有相机应用能够处理该 Intent
takePictureIntent.resolveActivity(packageManager)?.also {
// 尝试创建用于存储照片的文件
val photoFile: File? = try {
createImageFile() // 创建图片文件
} catch (ex: IOException) {
null // 处理文件创建过程中可能出现的异常
}
// 如果创建成功,继续执行
photoFile?.also {
// 获取文件的 URI,使用 FileProvider 来确保文件访问权限正确
photoURI = FileProvider.getUriForFile(this, "com.uz.cashloanuzi.fileprovider", it)
// 将照片文件的 URI 传递给相机应用,确保照片被保存到正确的位置
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
// 启动相机应用并等待结果,结果会回调到 onActivityResult
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
}
}
}
}
// 创建用于保存图片的文件
@Throws(IOException::class)
private fun createImageFile(): File {
// 生成一个带有时间戳的文件名
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
// 获取用于存储图片的目录路径,使用应用专属的外部存储
val storageDir: File? = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
// 创建临时文件,文件名以 "JPEG_时间戳_" 开头,扩展名为 .jpg
return File.createTempFile(
"JPEG_${timeStamp}_", /* 文件前缀 */
".jpg", /* 文件后缀 */
storageDir /* 存储目录 */
).apply {
// 保存文件的绝对路径,供其他用途使用(这儿最开始我在用,后面没用也没注释,可以不管)
currentPhotoPath = absolutePath
}
}
流程请看代码注释,其中的fileprovider,需要自己在manifest中声明
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.uz.cashloanuzi.fileprovider" <!-- 指定与代码中的 `FileProvider.getUriForFile` 相同的 authorities -->
android:exported="false" <!-- 防止其他应用直接访问你的 FileProvider,增加安全性 -->
android:grantUriPermissions="true"> <!-- 允许你临时授予其他应用对文件的访问权限 -->
<!-- FileProvider 的路径配置 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS" <!-- 配置文件的路径信息 -->
android:resource="@xml/file_paths" /> <!-- 引用 XML 文件,定义哪些目录可以被共享 -->
</provider>
file_paths文件
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 共享外部存储图片目录 -->
<external-files-path
name="my_images"
path="Pictures/" />
</paths>
tips:@xml/file_paths
指的是一个 XML 文件,你需要在res/xml
目录下创建这个文件,告诉FileProvider
你允许分享的文件路径。通常这个文件包含一个<paths>
标签,并定义了可共享的目录。
3.2 图片回调处理
@SuppressLint("SuspiciousIndentation")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_IMAGE_CAPTURE -> {//拍照
val bitmap: Bitmap =
MediaStore.Images.Media.getBitmap(this.contentResolver, photoURI)
val file = createFileFromBitmap(bitmap)//创建一个文件,且压缩
val fileSizeInKB = file.length().div(1024)
val fileSizeInMB = fileSizeInKB / 1024
LogUtils.e("file Size", fileSizeInMB.toString())
uploadImage(file) { isSus, result ->//接口上传
if (isSus) {
toShowPic(bitmap)//展示
} else {
ToastUtils.makeText(getString(R.string.upload_failed))
}
}
}
REQUEST_IMAGE_PICK -> {//相册
data?.data?.let { uri ->
val bitmap: Bitmap =
MediaStore.Images.Media.getBitmap(this.contentResolver, uri)
val file = createFileFromBitmap(bitmap)//创建一个文件,且压缩
uploadImage(file) { isSus, result ->//接口上传
if (isSus) {
toShowPic(bitmap)//展示到UI上
} else {
ToastUtils.makeText(getString(R.string.upload_failed))
}
}
}
}
}
} else if (resultCode == Activity.RESULT_CANCELED) {
if (requestCode == REQUEST_IMAGE_CAPTURE) {
ToastUtils.makeText("拍照取消"))
} else if (requestCode == REQUEST_IMAGE_PICK) {
ToastUtils.makeText("相册获取图片取消")
}
}
}
4.相册选择回调,图片处理
4.1 打开相册
private val REQUEST_IMAGE_PICK = 2
private fun dispatchChoosePictureIntent() {//ACTION_PICK 相册选择
Intent(
Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI
).also { choosePictureIntent ->
choosePictureIntent.type = "image/*"
startActivityForResult(choosePictureIntent, REQUEST_IMAGE_PICK)
}
}
4.2 相册回调
即上面onActivityResult里的方法
5.图片压缩,上传服务器
5.1 图片压缩(如果后端需要对图片有要求,得压缩)
// 将 Bitmap 转换为文件并进行压缩,保证文件大小不超过 2MB
private fun createFileFromBitmap(bitmap: Bitmap): File {
// 使用当前时间戳生成唯一的文件名
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
// 获取应用的外部图片存储目录
val storageDir: File? = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
// 创建文件对象,文件名为 JPEG_加上时间戳,并且以 .jpg 结尾
val file = File(storageDir, "JPEG_${timeStamp}_.jpg")
// 创建一个文件输出流,准备将字节数组写入文件
val fos = FileOutputStream(file)
// 初始化图片的质量为 100(最高质量)
var quality = 100
// 将 Bitmap 转换为字节数组,并指定初始的质量
var byteArray = convertBitmapToByteArray(bitmap, quality)
// 输出初始的图片大小
LogUtils.e("image size", byteArray.size.toString())
// 如果图片大小超过 2MB,并且质量大于 10,就继续压缩
while (byteArray.size > 2 * 1024 * 1024 && quality > 10) {
quality -= 10 // 每次减少 10% 的质量
byteArray = convertBitmapToByteArray(bitmap, quality) // 重新生成字节数组
}
// 输出压缩后的图片大小
LogUtils.e("deal image size", byteArray.size.toString())
// 将最终的字节数组写入文件
fos.write(byteArray)
// 刷新并关闭文件输出流,确保数据写入完成
fos.flush()
fos.close()
// 返回创建的文件对象
return file
}
5.2 图片上传
我的Retrofit请求自己又封了一下,这儿就不粘贴。传文件和普通接口会有些不同,注意一下就好了