前言
- 在平時的工作中,我們會經常遇到這樣的需求:比如更改用戶頭像,我們就需要打開相機或相冊,選擇照片后進行裁剪,最后上傳給后臺...
- 相信這樣的功能對小伙伴們來說可能早已司空見慣,但是在實現的過程中,還是有一些地方需要注意的
- 本文主要演示如何去實現拍照+裁切功能,以及在實現的過程中需要注意的地方
- 完整代碼會在文章底部給出
目錄
一 、拍照
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 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中看到對應的圖片信息
由于現在手機像素都特別高,拍出來的照片非常大,如果不進行壓縮處理直接展示在屏幕上的話不僅會耗費內存而且可能引起OutOfMemary(內存溢出)
好,到這里就完成了調用系統相機拍照并顯示的功能了。貼一下效果圖:
二、拍照 + 裁剪
第一部分已經介紹了如何調用系統相機進行拍照,但是有時候,我們不需要整張圖片,而是只需要采取圖片的某一部分就行,那就要用到裁剪功能了
要實現拍照 + 裁剪功能,步驟如下:
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))
}
}
到這里就完成了調用系統相機拍照+裁剪的功能了。貼一下效果圖:
三、相冊選擇 + 裁剪
從相冊中選擇一張照片,然后裁剪并顯示,流程如下:
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))
}
}
效果圖如下:
四、錄制視頻
調用系統的錄制功能錄制一段視頻,并播放
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,指向視頻所保存的位置
效果圖如下:
五、小結
- Android7.0系統以上,不再允許直接使用file://URI,解決的辦法是FileProvider類生成ContentUri
- Android7.0系統以上,調用相機時,設置輸出位置必須使用ContentUri;
在裁剪的時,設置圖片源的時候必須使用ContentUri,設置輸出位置的時候繼續使用 file://URI
完整的示例代碼:
https://github.com/smashinggit/Study
注:此工程包含多個module,本文所用代碼均在camerademo文件夾下