Android:调用系统相机实现拍照+裁切(兼容 7.0,11.0)

android.jpg

前言

  • 在平时的工作中,我们会经常遇到这样的需求:比如更改用户头像,我们就需要打开相机或相册,选择照片后进行裁剪,最后上传给后台...
  • 相信这样的功能对小伙伴们来说可能早已司空见惯,但是在实现的过程中,还是有一些地方需要注意的
  • 本文主要演示如何去实现拍照+裁切功能,以及在实现的过程中需要注意的地方
  • 完整代码会在文章底部给出

目录

目录.jpg

一 、拍照

1. 定义相关变量 及 权限申请

 val AUTHORITY = "com.cs.camerademo.fileProvider" //FileProvider的签名(后面会介绍)
 val REQUEST_CODE_CAPTURE_RAW = 6 //startActivityForResult时的请求码 
 var imageFile: File? = null     //拍照后保存的照片
 var imgUri: Uri? = null         //拍照后保存的照片的uri
  <uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.RECORD_AUDIO" />

2.调用系统相机

   //拍照(返回原始图)
    private fun gotoCaptureRaw() {
        imageFile = createImageFile()   
        imageFile?.let {
            val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {  //如果是7.0以上,使用FileProvider,否则会报错
                intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
                imgUri = FileProvider.getUriForFile(this, AUTHORITY, it)
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri) //设置拍照后图片保存的位置
            } else {
                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(it)) //设置拍照后图片保存的位置
            }
            intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) //设置图片保存的格式
            intent.resolveActivity(packageManager)?.let {
                startActivityForResult(intent, REQUEST_CODE_CAPTURE_RAW) //调起系统相机
            }
        }
    }

    //生成一个文件
    fun createImageFile(isCrop: Boolean = false): File? {
        return try {
            var rootFile = File(rootFolderPath + File.separator + "capture")
            if (!rootFile.exists())
                rootFile.mkdirs()
            val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
            val fileName = if (isCrop) "IMG_${timeStamp}_CROP.jpg" else "IMG_$timeStamp.jpg"
            File(rootFile.absolutePath + File.separator + fileName)
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

调用相机之前,intent需要设置两个参数

  • intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) 设置图片保存的格式

  • intent.putExtra(MediaStore.EXTRA_OUTPUT,uri) 设置拍照后图片保存的位置,如果没有这句,相机则会返回一个拍照的缩略图,具体效果及代码请查看完整代码

注意:在设置图片保存的uri的时候,需要对Android7.0以上系统做处理,否则会报FileUriExposedException异常

3. 处理异常,兼容7.0

在7.0以上系统中,Android不再允许在app中把file://Uri暴露给其他app,因为这样做存在一定的风险,如:

  • 文件是私有的,接收file://Uri的app无法访问该文件。
  • 在Android6.0之后引入运行时权限,如果接收file://Uri的app没有申请READ_EXTERNAL_STORAGE权限,在读取文件时会引发崩溃

官方给出的解决方案是使用FileProvider,具体使用步骤如下:
第一步:在AndroidManifest.xml清单文件中注册FileProvider

      <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.cs.camerademo.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
  • name:provider的类名,若使用默认的v4的FileProvide
  • android:authorities 一个签名认证,可以自定义,但在获取uri的时候需要保持一致;
  • grantUriPermissions:使用FileProvider的使用需要我们给流出的URI赋予临时访问权限(READ和WRITE),该设置是允许我们行使该项权力;
  • meta-data: 配置的是我们可以访问的文件的路径配置信息,需要使用xml文件进行配置,FileProvider会通过解析xml文件获取配置项,其中name为固定写法:android.support.FILE_PROVIDER_PATHS,resource为配置路径信息的配置项目。

第二步:在res目录下创建一个名为xml的文件夹,然后在该文件夹下创建名为file_paths的xml文件


xml目录.png
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!--path:需要临时授权访问的路径(.代表所有路径)-->
    <!--name:就是你给这个访问路径起个名字-->
    <external-path name="external-path" path="CameraDemo" />
</paths>

这个xml文件最外层是一对<paths>标签,标签内部是一个或多个表示路径的item.,写法是 <对应的path name="起个名字" path="对应的path目录下的某个文件夹" >

例如 :
<external-path name="external-path" path="CameraDemo" />
external-path 表示 Environment.getExternalStorageDirectory()目录;
name 是我们给它起的名字叫"external-path" (当然你也可以用起个张三李四什么的,好像没啥用...);
path 表示Environment.getExternalStorageDirectory()目录下的CameraDemo文件夹

具体的路径与写法的对应规则如下:

写法 对应目录
<files-path name="name" path="path" > Context.getFilesDir()
<cache-path name="name" path="path"> Context.getCacheDir()
<external-path name="name" path="path"> Environment.getExternalStorageDirectory()
<external-files-path name="name" path="path"> Context.getExternalFilesDir(null)
<external-cache-path name="name" path="path"> Context.getExternalCacheDir()

如果需要使用FileProvider获取某个目录下的文件的uri,按照上表中的对应关系在xml文件中声明就可以了

第一步在清单文件中注册FileProvider的时候, android:resource="@xml/file_paths" 对应的就是我们刚刚创建的这个xml文件

第三步: 在Activity使用FileProvider获取uri

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) //申请临时读取的权限
imgUri = FileProvider.getUriForFile(this, AUTHORITY , imageFile)

getUriForFile是FileProvider的一个静态方法,第一个参数是Context对象,第二个参数是在清单文件中定义的authorities,第三个参数是一个File对象。 返回值为传入文件的content uri对象。

当我们拿到返回的ContentUri对象后,就解决了Android7.0报FileUriExposedException的问题

4. 接收相机返回的结果

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_CAPTURE_RAW -> {   //拍照成功后,压缩图片,显示结果
                    imageFile?.let {
                        compressImage(it)       //照片生成后,压缩图片并显示     
                    }
                }
            }
        } else {
            Log.d("tag", "错误码 $resultCode")
        }
    }

    //压缩图片并显示
    fun compressImage(file:File) {
        thread {
            var temp = System.currentTimeMillis()
            var bitmap = BitmapFactory.decodeFile(file.absolutePath)
            var compressBitmap = BitmapUtils.decodeBitmap(bitmap, 720, 1080)
            runOnUiThread {
                ivImage.setImageBitmap(compressBitmap)   //ivImage是xml布局文件中的一个ImageView
                Log.d("tag", "原始图片大小  ${bitmap.width} * ${bitmap.height}")
                Log.d("tag", "压缩后图片大小  ${compressBitmap.width} * ${compressBitmap.height}")
                Log.d("tag", "加载图片耗时 ${System.currentTimeMillis() - temp}")
            }
        }
    }

在onActivityResult中成功收到返回结果后,压缩图片,并显示在ImageView中。
由于压缩图片是耗时操作,所以开启了一个子线程进行压缩处理,并在Log中输入的原始图片大小,压缩后图片大小和压缩所耗时间,小伙伴们可以在logcat中看到对应的图片信息

图片信息.png

由于现在手机像素都特别高,拍出来的照片非常大,如果不进行压缩处理直接展示在屏幕上的话不仅会耗费内存而且可能引起OutOfMemary(内存溢出)

好,到这里就完成了调用系统相机拍照并显示的功能了。贴一下效果图:


拍照.gif

二、拍照 + 裁剪

第一部分已经介绍了如何调用系统相机进行拍照,但是有时候,我们不需要整张图片,而是只需要采取图片的某一部分就行,那就要用到裁剪功能了

要实现拍照 + 裁剪功能,步骤如下:

1. 进行调用系统相机拍照,并接收返回结果。

这一步在上面已经介绍介绍过,所以不在赘述。唯一一个地方与上面不同的是,在onActivityResult中收到成功的返回结果后不在是去压缩并显示图片,而是去调用系统的裁剪功能


    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_CAPTURE -> { //拍照成功的返回结果
                    val sourceUri = FileProvider.getUriForFile(this, AUTHORITY, imageFile) //通过FileProvider创建一个content类型的Uri
                    gotoCrop(sourceUri)  //裁剪图片
                }
                REQUEST_CODE_CAPTURE_CROP -> { //裁剪成功得返回结果
                    imageCropFile?.let { //显示图片
                        ivResult.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))
                    }
                }
            }
        } else {
            Log.d("tag", "错误码 $resultCode")
        }
    }

2. 调用系统的裁剪功能,并接收返回结果。

    //裁剪(参数是需要裁剪的图片的uri)
  imageCropFile = FileUtil.createImageFile(true)
        imageCropFile?.let {

            val intent = Intent("com.android.camera.action.CROP")
            intent.putExtra("crop", "true")
            intent.putExtra("aspectX", 1)    //X方向上的比例
            intent.putExtra("aspectY", 1)    //Y方向上的比例
            intent.putExtra("outputX", 500)  //裁剪区的宽
            intent.putExtra("outputY", 500)  //裁剪区的高
            intent.putExtra("scale ", true)  //是否保留比例
            intent.putExtra("return-data", false)
            intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
            intent.setDataAndType(sourceUri, "image/*")

            // 7.0 使用 FileProvider 并赋予临时权限
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            }

            // 11.0无法访问私有域,所以这里要确保裁剪后的文件保存在公有目录中
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                // 这里要保证输出的文件是在公有目录
                // 由于此demo中默认保存的就是公有目录,所以这里不做任何操作,如果是自己的项目,请根据具体情况修改
            } else {
            }

            val outputUri = Uri.fromFile(it)
            intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri)            //设置输出

            Log.d("tag", "输入 $sourceUri")
            Log.d("tag", "输出 $outputUri")
            startActivityForResult(intent, REQUEST_CODE_CAPTURE_CROP)
        }

注意:
Android7.0以上,相机调用时intent.putExtra(MediaStore.EXTRA_OUTPUT, uri), Uri必须使用ContentUri,不能用Uri.fromFile(file)
在裁剪的时候就不一样,设置图片源的时候必须使用ContentUri,设置输出位置的时候继续使用 Uri.fromFile(file)。

注意:
在 Android 11 以后强制分区存储,外部资源无法访问。所以在调用裁剪的时候,需要保证输出的图片存储在公有目录中。否则无法保存裁剪的图片

3. 接收裁剪结果并显示

 REQUEST_CODE_CAPTURE_CROP -> {   //裁剪成功后,显示结果
       imageCropFile?.let {
                      ivResult.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))
          }
    }

到这里就完成了调用系统相机拍照+裁剪的功能了。贴一下效果图:


拍照+裁剪.gif

三、相册选择 + 裁剪

从相册中选择一张照片,然后裁剪并显示,流程如下:

1. 打开系统相册

 //打开系统相册
 private fun gotoGallery() {
     var intent = Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
     startActivityForResult(intent, REQUEST_CODE_ALBUM)
 }

2. 在onActivityResult接收返回的url,调用裁剪方法

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_ALBUM -> { //从相册选择照片后,裁剪
                    data?.let {
                        gotoCrop(it.data)
                    }
                }
                REQUEST_CODE_CAPTURE_CROP -> {   //裁剪成功后,显示结果
                    imageCropFile?.let {
                        ivResult.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))
                    }
                }
            }
        } else {
            Log.d("tag", "错误码 $resultCode")
        }
    }

注意:
打开系统相册选择照片后,返回结果在Intent数据中的data字段,是一个uri,表示选择的那张照片的位置

具体的裁剪相关代码在上面已有,此处不在展示

3. 接收裁剪结果并显示

 REQUEST_CODE_CAPTURE_CROP -> {   //裁剪成功后,显示结果
       imageCropFile?.let {
                      ivResult.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))
          }
    }

效果图如下:


相册+裁剪.gif

四、录制视频

调用系统的录制功能录制一段视频,并播放

1. 调用录制视频功能

    //录制视频
    private fun gotoCaptureVideo() {
        var intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
        if (intent.resolveActivity(packageManager) != null)
            startActivityForResult(intent, REQUEST_CODE_VIDEO)
    }

2. 接收返回结果并使用VideoView播放

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_VIDEO -> {   //录制视频成功后播放
                    data?.let {
                        var uri = it.data
                        videoView.visibility = View.VISIBLE
                        videoView.setVideoURI(uri)
                        videoView.start()
                        Log.d("tag", "视频uri $uri")
                    }
                }
            }
        } else {
            Log.d("tag", "错误码 $resultCode")
        }
    }

录制视频后返回的结果在 Intent的data字段中,是一个uri,指向视频所保存的位置

效果图如下:


录制视频.gif

五、小结

  1. Android7.0系统以上,不再允许直接使用file://URI,解决的办法是FileProvider类生成ContentUri
  2. Android7.0系统以上,调用相机时,设置输出位置必须使用ContentUri;
    在裁剪的时,设置图片源的时候必须使用ContentUri,设置输出位置的时候继续使用 file://URI

完整的示例代码:

https://github.com/smashinggit/Study

注:此工程包含多个module,本文所用代码均在camerademo文件夹下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容