《Android 编程权威指南》学习笔记 : 第16章 使用 intent 拍照
第16章 使用 intent 拍照
参考资料
https://www.jc2182.com/andriod/android-camera.html
方案分析
文件存储
相机拍摄的照片动辄几MB大小,保存在SQLite数据库中肯定不现实。显然,它们需要在设备文件系统的某个地方保存。
设备上就有这么一个地方:私有存储空间,像照片这样的文件也可以这么保存。
因为要处理的照片都是私有文件,只有你自己的应用能读写,
如果其他应用要读写你的文件,虽然Context.MODE_WORLD_READABLE可以传入openFileOutput(String, Int)函数,但这个flag已经废弃了。即使强制使用,在新系统设备上也不是很可靠。以前,还可以通过公共外部存储转存,但出于安全考虑,这条路在新版本系统上也被堵住了。
ContentProvider
如果想共享文件给其他应用,或是接收其他应用的文件(比如相机应用拍摄的照片),可以通过ContentProvider把要共享的文件暴露出来。
ContentProvider允许你暴露内容URI给其他应用。这样,这些应用就可以从内容URI下载或向其中写入文件。当然,主动权在你手上,你可以控制读或写。
如果只想从其他应用接收一个文件,自己实现ContentProvider简直是费力不讨好的事。
Google早已想到这点,因此提供了一个名为FileProvider的便利类。这个类能帮你搞定一切,而你只要做做参数配置就行了。
使用 FileProvider
声明FileProvider为ContentProvider,并给予一个指定的权限。在AndroidManifest.xml中添加一个FileProvider声明
代码清单: 添加FileProvider声明(manifests/AndroidManifest.xml)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.criminalintent">
<application .../>
<activity
.../>
...
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.criminalintent.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/files"/>
</provider>
</application>
</manifest>
其中:
- com.example.criminalintent.fileprovider:是授权字符串,后面会用到。
- exported="false": 处了自己和授权的人,其它任何人不允许使用你的 FileProvider
- android:grantUriPermissions="true":授权其它应用,允许它们向你指定位置的URI写入文件
- resource="@xml/files":指定要暴露的文件:执行一个xml资源文件。
右键点击 app/res 目录,New -> Android resource file, 资源类型选择xml,文件名输入 files,确定创建文件,替换为如下内容
代码清单: res/xm/files.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="crime_photos" path="."/>
</paths>
照片存放位置
代码清单:Crime.kt
@Entity
data class Crime(...) {
val photoFileName get() = "IMG_$id.jpg"
}
代码清单:CrimeRepository.kt
class CrimeRepository private constructor(context: Context) {
...
private var filesDir = context.applicationContext.filesDir
fun getPhotoFile(crime: Crime):File = File(filesDir, crime.photoFileName)
...
}
- filesDir:返回的目录:/data/user/0/com.example.criminalintent/files
在 Device File Exploer 的位置见下图:
直接在真机的文件管理中是无法查看的,真机的操作系统已经将其隐藏,只能在 Android Studio 的 Device File Exploer 查看。
- getPhotoFile返回图片地址,比如:/data/user/0/com.example.criminalintent/files/IMG_454f516f-4812-42dc-a06a-479ceb5c359b.jpg
代码清单:CrimeRepository.kt
class CrimeDetailViewModel : ViewModel() {
...
fun getPhotoFile(crime: Crime): File {
return crimeRepository.getPhotoFile(crime)
}
}
使用相机 intent
布局文件,添加 拍照按钮的ImageButton 和显示照片的ImageView
代码清单:res/layout/fragment_crime.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/crime_photo"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="centerInside"
android:cropToPadding="true"
android:background="@android:color/darker_gray"/>
<ImageButton
android:id="@+id/crime_camera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_camera"/>
</LinearLayout>
<LinearLayout
...
</LinearLayout>
...
</LinearLayout>
代码清单:CrimeFragment.kt
class CrimeFragment : Fragment()
...
private lateinit var photoFile: File
private lateinit var photoUri: Uri
private lateinit var photoButton: ImageButton
private lateinit var photoView: ImageView
// 启动照相机 activity 的intent
private val captureImageIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
// 启动照相机 activity 的启动器
private lateinit var captureActivityResultLauncher: ActivityResultLauncher<Intent>
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
// 照相机 Activity启动器
captureActivityResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK
&& result.data != null) {
updatePhotoView()
//撤销权限
requireActivity().revokeUriPermission(photoUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
photoButton = view.findViewById(R.id.crime_camera) as ImageButton
photoView = view.findViewById(R.id.crime_photo) as ImageView
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
crimeDetailViewModel.crimeLiveData.observe(
viewLifecycleOwner,
Observer { crime ->
crime?.let {
this.crime = crime
// 照片文件:路径 + 文件名
photoFile = crimeDetailViewModel.getPhotoFile(crime)
Log.d(TAG,"图片文件:$photoFile")
// 把照片文件封装成 Uri
photoUri = FileProvider.getUriForFile(
requireActivity(), // 当前的Activity
"com.example.criminalintent.fileprovider", // 授权字符串,与 AndroidManifest.xml 文件里的一致
photoFile //照片文件
)
updateUI()
}
}
)
}
override fun onStart() {
...
photoButton.apply {
val packageManager: PackageManager = requireActivity().packageManager
// 检查是否有相机应用
var resolvedActivity: ResolveInfo? =
packageManager.resolveActivity(captureImageIntent, PackageManager.MATCH_DEFAULT_ONLY)
if (resolvedActivity == null) {
isEnabled = false
}
setOnClickListener {
//设置照片存储的Uri
captureImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
val cameraActivities: List<ResolveInfo> =
packageManager.queryIntentActivities(
captureImageIntent,
PackageManager.MATCH_DEFAULT_ONLY)
// 授予所有的照相机在photoUri指定的位置写入文件的权限
for (cameraActivity in cameraActivities) {
requireActivity().grantUriPermission(
cameraActivity.activityInfo.packageName,
photoUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
// 启动照相机 activity
captureActivityResultLauncher.launch(captureImageIntent)
}
}
}
private fun updatePhotoView() {
if (photoFile.exists()) {
val bitmap = getScaledBitmap(photoFile.path, requireActivity() )
photoView.setImageBitmap(bitmap)
}else {
photoView.setImageBitmap(null)
}
}
private fun updateUI() {
...
updatePhotoView()
}
override fun onDetach() {
super.onDetach()
//撤销权限
requireActivity().revokeUriPermission(photoUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
}
-
Intent(MediaStore.ACTION_IMAGE_CAPTURE): 启动照相机 activity 的intent,action名称:MediaStore.ACTION_IMAGE_CAPTURE
-
captureActivityResultLauncher : 启动照相机 activity 的启动器
-
设置照片存储的Uri:captureImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri):
设置照相机的拍照后照片的存储位置Uri:photoUri,并使用授权字符进行标记 ,见如下代码:// 把照片文件封装成 Uri photoUri = FileProvider.getUriForFile( requireActivity(), // 当前的Activity "com.example.criminalintent.fileprovider", // 授权字符串,与 AndroidManifest.xml 文件里的一致 photoFile //照片文件:路径+文件名 )
-
Uri授权:授予所有的照相机在photoUri指定的位置写入文件的权限:
requireActivity().grantUriPermission( cameraActivity.activityInfo.packageName, photoUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION )
-
写入授权:Intent.FLAG_GRANT_WRITE_URI_PERMISSION :
-
启动照相机 activity: captureActivityResultLauncher.launch(captureImageIntent)
-
相机存储完照片,返回后调用 updatePhotoView() 更新 imageView。
-
并且用完记得在相机回调函数,和 onDetach()撤销权限 Intent.FLAG_GRANT_WRITE_URI_PERMISSION
-
日志:
- 图片位置的日志:
图片文件:/data/user/0/com.example.criminalintent/files/IMG_454f516f-4812-42dc-a06a-479ceb5c359b.jpg
- 图片原始宽高和转换后的宽高:
2022-06-03 17:29:42.474 27366-27366/com.example.criminalintent D/PictureUtils: destWidth:1080, srcWidth:4000.0 2022-06-03 17:29:42.475 27366-27366/com.example.criminalintent D/PictureUtils: destHeight:2276, srcHeight:3000.0
- 图片位置的日志:
-
直接在真机的文件管理中是无法查看的,真机的操作系统已经将其隐藏,只能在 Android Studio 的 Device File Exploer 查看。
- 因为每个 crime的图片都是 IMG+crime.id 组成,每次拍照得到的文件名相同,保存会覆盖掉旧的照片,故,每一个crime即使是拍照多次, 只有对应的一张图片
缩放和显示位图
Bitmap是个简单对象,它只存储实际像素数据。也就是说,即使原始照片已压缩过,但存入Bitmap对象时,文件并不会同样压缩。因此,一张1600万像素24位的相机照片(存为JPG格式大约5 MB),一旦载入Bitmap对象,就会立即膨胀至48 MB!
这个问题可以设法解决,但需要手动缩放位图照片。具体做法是,首先确认文件到底有多大,然后考虑按照给定区域大小合理缩放文件。最后,重新读取缩放后的文件,创建Bitmap对象
代码清单:PictureUtils.kt
package com.example.criminalintent
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
private const val TAG = "PictureUtils"
fun getScaledBitmap(path: String, activity: Activity): Bitmap {
val metrics = activity.resources.displayMetrics
return getScaledBitmap(path, metrics.widthPixels, metrics.heightPixels)
}
/**
* 缩放位图照片
* */
fun getScaledBitmap(path: String, destWidth: Int, destHeight: Int): Bitmap {
// 读取目标文件
var options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, options)
val srcWidth = options.outWidth.toFloat()
val srcHeight = options.outHeight.toFloat()
Log.d(TAG, "destWidth:$destWidth, srcWidth:$srcWidth")
Log.d(TAG, "destHeight:$destHeight, srcHeight:$srcHeight")
/**计算缩放比例
* 1:缩略图与原始图标的水平像素一样
* 2:表示水平像素比为 1:2,即:缩略图的像素是原始文件的 1/4
*/
var inSampleSize = 1
if (srcHeight > destHeight || srcWidth > destWidth) {
val heightScale = srcHeight / destHeight
val widthScale = srcWidth / destWidth
val sampleScale = if (heightScale > widthScale) {
heightScale
}else{
widthScale
}
inSampleSize = Math.round(sampleScale)
}
options = BitmapFactory.Options()
options.inSampleSize = inSampleSize
// 读取和创建最终的BitMap
return BitmapFactory.decodeFile(path, options)
}
功能声明
应用的拍照功能用起来不错,但还有一件事情要做:告诉潜在用户应用有拍照功能。
假如应用要用到诸如相机、NFC,或者任何其他的随设备走的功能时,都应该让Android系统知道。这样,假如设备缺少这样的功能,类似Google Play商店的安装程序就会拒绝安装应用。
为了声明应用要使用相机,在AndroidManifest.xml中加入
代码清单:添加
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.criminalintent">
...
<uses-feature android:name="android.hardware.camera"
android:required="false"/>
</manifest>
注意,我们在代码中使用了android:required属性。默认情况下,声明要使用某个设备功能后,应用就无法支持那些无此功能的设备了,但这不适用于CriminalIntent应用。这是因为,resolveActivity(...)函数可以判断设备是否支持拍照。如果不支持,就直接禁用拍照按钮。
无论如何,这里设置android:required属性为false,Android系统因此就知道,尽管不带相机的设备会导致应用功能缺失,但应用仍然可以正常安装和使用
真机运行
运行 CriminalIntent,