最近项目里面有用到裁切功能,没弄多复杂,就是系统自带的,顺便就总结了一下系统拍照、裁切、选取的使用。网上的资料说实话真是没什么营养,但是Android官网上的说明也有点太简单了,真就要实践出真理。
本来拍照是没什么难度的,不就是调用intent去系统相机拍照么,但是由于文件权限问题,Uri这东西就能把人很头疼。下面是代码(onActivityResult见后文):
private fun openCamera() {val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)// 应用外部私有目录:files-Picturesval picFile = createFile("Camera")val photoUri = getUriForFile(picFile)// 保存路径,不要uri,读取bitmap时麻烦picturePath = picFile.absolutePath// 给目标应用一个临时授权intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)//android11以后强制分区存储,外部资源无法访问,所以添加一个输出保存位置,然后取值操作intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)startActivityForResult(intent, REQUEST_CAMERA_CODE)}private fun createFile(type: String): File {// 在相册创建一个临时文件val picFile = File(requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES),"${type}_${System.currentTimeMillis()}.jpg")try {if (picFile.exists()) {picFile.delete()}picFile.createNewFile()} catch (e: IOException) {e.printStackTrace()}// 临时文件,后面会加long型随机数
// return File.createTempFile(
// type,
// ".jpg",
// requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)
// )return picFile}private fun getUriForFile(file: File): Uri {// 转换为urireturn if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//适配Android 7.0文件权限,通过FileProvider创建一个content类型的UriFileProvider.getUriForFile(requireActivity(),"com.xxx.xxx.fileProvider", file)} else {Uri.fromFile(file)}}
这里的file是使用getExternalFilesDir和Environment.DIRECTORY_PICTURES生成的,它的文件保存在应用外部私有目录:files-Pictures里面。这里要注意不能存放在内部的私有目录里面,不然是无法访问的,外部私有目录虽然也是私有的,但是外面是可以访问的,这里拿官网上的说明:
在搭载 Android 9(API 级别 28)或更低版本的设备上,只要您的应用具有适当的存储权限,就可以访问属于其他应用的应用专用文件。为了让用户更好地管理自己的文件并减少混乱,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被授予了对外部存储空间的分区访问权限(即分区存储)。启用分区存储后,应用将无法访问属于其他应用的应用专属目录。
再一个比较麻烦的就是Uri的获取了,网上有一大堆资料,不过我这也贴一下,网上的可能有问题。
manifest.xml
res -> xml -> file_paths.xml
ps. 注意authorities这个最好填自己的包名,不然有两个应用用了同样的authorities,后面的应用会安装不上。
这里打开相册用的是SAF框架,使用intent去选取(onActivityResult见后文)。
private fun openAlbum() {val intent = Intent()intent.type = "image/*"intent.action = "android.intent.action.GET_CONTENT"intent.addCategory("android.intent.category.OPENABLE")startActivityForResult(intent, REQUEST_ALBUM_CODE)}
裁切这里比较麻烦,参数比较多,而且Uri那里有坑,不能使用provider,再一个就是图片传递那因为安卓版本变更,不会传略缩图了,很坑。
private fun cropImage(path: String) {cropImage(getUriForFile(File(path)))}private fun cropImage(uri: Uri) {val intent = Intent("com.android.camera.action.CROP")// Android 7.0需要临时添加读取Url的权限intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)intent.setDataAndType(uri, "image/*")// 使图片处于可裁剪状态intent.putExtra("crop", "true")// 裁剪框的比例(根据需要显示的图片比例进行设置)
// if (Build.MANUFACTURER.contains("HUAWEI")) {
// //硬件厂商为华为的,默认是圆形裁剪框,这里让它无法成圆形
// intent.putExtra("aspectX", 9999)
// intent.putExtra("aspectY", 9998)
// } else {
// //其他手机一般默认为方形
// intent.putExtra("aspectX", 1)
// intent.putExtra("aspectY", 1)
// }// 设置裁剪区域的形状,默认为矩形,也可设置为圆形,可能无效// intent.putExtra("circleCrop", true);// 让裁剪框支持缩放intent.putExtra("scale", true)// 属性控制裁剪完毕,保存的图片的大小格式。太大会OOM(return-data)
// intent.putExtra("outputX", 400)
// intent.putExtra("outputY", 400)// 生成临时文件val cropFile = createFile("Crop")// 裁切图片时不能使用provider的uri,否则无法保存
// val cropUri = getUriForFile(cropFile)val cropUri = Uri.fromFile(cropFile)intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri)// 记录临时位置cropPicPath = cropFile.absolutePath// 设置图片的输出格式intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())// return-data=true传递的为缩略图,小米手机默认传递大图, Android 11以上设置为true会闪退intent.putExtra("return-data", false)startActivityForResult(intent, REQUEST_CROP_CODE)}
下面是对上面三个操作的回调处理,一开始我觉得uri没什么用,还制造麻烦,后面发现可以通过流打开uri,再去获取bitmap,好像又不是那么麻烦了。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)if (resultCode == RESULT_OK) {when(requestCode) {REQUEST_CAMERA_CODE -> {// 通知系统文件更新
// requireContext().sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
// Uri.fromFile(File(picturePath))))if (!enableCrop) {val bitmap = getBitmap(picturePath)bitmap?.let {// 显示图片binding.image.setImageBitmap(it)}}else {cropImage(picturePath)}}REQUEST_ALBUM_CODE -> {data?.data?.let { uri ->if (!enableCrop) {val bitmap = getBitmap("", uri)bitmap?.let {// 显示图片binding.image.setImageBitmap(it)}}else {cropImage(uri)}}}REQUEST_CROP_CODE -> {val bitmap = getBitmap(cropPicPath)bitmap?.let {// 显示图片binding.image.setImageBitmap(it)}}}}}private fun getBitmap(path: String, uri: Uri? = null): Bitmap? {var bitmap: Bitmap?val options = BitmapFactory.Options()// 先不读取,仅获取信息options.inJustDecodeBounds = trueif (uri == null) {BitmapFactory.decodeFile(path, options)}else {val input = requireContext().contentResolver.openInputStream(uri)BitmapFactory.decodeStream(input, null, options)}// 预获取信息,大图压缩后加载val width = options.outWidthval height = options.outHeightLog.d("TAG", "before compress: width = " +options.outWidth + ", height = " + options.outHeight)// 尺寸压缩var size = 1while (width / size >= MAX_WIDTH || height / size >= MAX_HEIGHT) {size *= 2}options.inSampleSize = sizeoptions.inJustDecodeBounds = falsebitmap = if (uri == null) {BitmapFactory.decodeFile(path, options)}else {val input = requireContext().contentResolver.openInputStream(uri)BitmapFactory.decodeStream(input, null, options)}Log.d("TAG", "after compress: width = " +options.outWidth + ", height = " + options.outHeight)// 质量压缩val baos = ByteArrayOutputStream()bitmap!!.compress(Bitmap.CompressFormat.JPEG, 80, baos)val bais = ByteArrayInputStream(baos.toByteArray())options.inSampleSize = 1bitmap = BitmapFactory.decodeStream(bais, null, options)return bitmap}
这里还做了一个图片的质量压缩和采样压缩,需要注意的是采样压缩的采样率只能是2的倍数,如果需要按任意比例采样,需要用到Matrix,不是很难,读者可以研究下。
如果你发现你没有申请权限,那你的去申请一下相机权限;如果你发现你还申请了储存权限,那你可以试一下去掉储存权限,实际还是可以使用的,因为这里并没有用到外部储存,都是应用的私有储存内,具体关于储存的适配,可以看我转载的这几篇文章,我觉得写的非常好:
Android 存储基础
Android 10、11 存储完全适配(上)
Android 10、11 存储完全适配(下)
以上代码都经过我这里实践了,确认了可用,可能写法不是最优,可以避免使用绝对路径,只使用Uri。至于请求码、布局什么的,读者自己改一下加一个就行,核心部分已经在这了。如果需要完整代码,等我下篇文章再加点内容再说喽!
上一篇:使用“Database Configuration Assistant”(数据库配置助手)创建Oracle数据库
下一篇:最新或2023(历届)北京小升初入学信息采集5月启动 2023北京小升初登记入学现场 北京2023年小升初最新政策