Android:呼叫系統相機實現拍照+裁切(相容7.0以上系統)

arraiy發表於2020-07-07

 

前言

  • 在平時的工作中,我們會經常遇到這樣的需求:比如更改使用者頭像,我們就需要開啟相機或相簿,選擇照片後進行裁剪,最後上傳給後臺...
  • 相信這樣的功能對小夥伴們來說可能早已司空見慣,但是在實現的過程中,還是有一些地方需要注意的
  • 本文主要演示如何去實現拍照+裁切功能,以及在實現的過程中需要注意的地方
  • 完整程式碼會在文章底部給出

目錄

目錄.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)
    private fun gotoCrop(sourceUri: Uri) {
        imageCropFile = FileUtil.createImageFile(true) //建立一個儲存裁剪後照片的File
        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中返回圖片
            intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) //設定輸出圖片的格式

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) //新增這一句表示對目標應用臨時授權該Uri所代表的檔案
                intent.setDataAndType(sourceUri, "image/*")  //設定資料來源,必須是由FileProvider建立的ContentUri

                var imgCropUri = Uri.fromFile(it)
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imgCropUri) //設定輸出  不需要ContentUri,否則失敗
                Log.d("tag", "輸入 $sourceUri")
                Log.d("tag", "輸出 ${Uri.fromFile(it)}")
            } else {
                intent.setDataAndType(Uri.fromFile(imageFile!!), "image/*")
                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(it))
            }
            startActivityForResult(intent, REQUEST_CODE_CAPTURE_CROP)
        }
    }

注意:
Android7.0以上,相機呼叫時intent.putExtra(MediaStore.EXTRA_OUTPUT, uri), Uri必須使用ContentUri,不能用Uri.fromFile(file)
在裁剪的時候就不一樣,設定圖片源的時候必須使用ContentUri,設定輸出位置的時候繼續使用 Uri.fromFile(file)。

疑問:
我對這個地方一直有疑問,7.0以上,在呼叫裁剪的時候,設定輸入的時候不是也應該用ContentUri麼???但實踐後發現用ContentUri無法得到裁剪的圖片。如果有知道原因的小夥們可以留言。想看原始碼,也沒找到系統裁剪的原始碼在哪裡-_-|| 小夥們可以留言告訴我一下

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資料夾下



轉自:https://www.jianshu.com/p/eca7335602c1

相關文章