转眼就《Android编程权威指南》第16章了,这次会运用到 Android 相机方面的技术了。
一、布置照片
首先要新增两个 view,ImageView 用来装缩略图,ImageButton 用来打开相机。修改下布局文件喽。大致的预览效果如下:
预览图
二、文件存储
Android 是有给我们提供私有存储空间的。如下:
Context 类提供的基本文件和目录处理函数:
- getFilesDir(): File「获取/data/data/<包名>/files目录」
- openFileInput(name: String): FileInputStream「打开现有文件进行读取」
- openFileOutput(name: String, mode: Int): FileOutputStream 「打开文件进行写入,如果不存在就创建它」
- getDir(name: String, mode: Int): File 「获取/data/data/<包名>/目录的子目录(如果不存在就先创建它)」
- fileList(...): Array<String> 「获取主文件目录下的文件列表。可与其他函数配合使用,比如openFileInput(String)」
- getCacheDir(): File 「获取/data/data/<包名>/cache目录,应注意及时清理该目录,并节约使用」
不过现在的情况是,外部的相机应用需要在我们的应用里面保存拍摄的照片,那么需要使用到 ContentProvider,ContentProvider Android 提供给我们的组件,它允许我们暴露内容 URI 给其他应用,这样,这些应用就可以从内容 URI 下载或向其中写入文件。实现了内容共享功能。
使用FileProvider
- 1、在 AndroidManifest.xml 中添加 FileProvider 并声明为 ContentProvider,给予一个指定的权限「文件保存地」。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pyn.criminalintent">
......
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.pyn.criminalintent.fileprovider"
android:exported="false"
android:grantUriPermissions="true" />
......
</manifest>
android:authorities 属性值在整个系统里要有唯一性。「所以常用包名」
把 FileProvider 和指定的位置关联起来,就相当于给发出请求的其他应用提供一个目标地。
android:exported="false" 表示除了你自己以及你授权的人,其他任何人都不允许使用你的 FileProvider。
grantUriPermissions 属性用来给其他应用授权,允许它们向你指定位置的 URI。
- 2、配置 FileProvider,让它知道该暴露哪些文件,打开 app/res 目录,New 出 files.xml,这是一个描述性 XML 文件,意思是把私有存储空间的根路径映射为crime_photos。这个名字仅供FileProvider内部使用,你不应去用它。如图:
配置 fileProvider
- 3、在AndroidManifest.xml文件中,添加一个meta-data标签,让FileProvider能找到files.xml文件。
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.pyn.criminalintent.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/files" />
</provider>
指定照片存放位置
- 在Crime.kt中添加一个计算属性获取图片文件名
@Entity
data class Crime(
@PrimaryKey val id: UUID = UUID.randomUUID(),
var title: String = "",
var date: Date = Date(),
var isSolved: Boolean = false,
var requiresPolice: Boolean = false,
var suspect: String = ""
) {
val photoFileName
get() = "IMG_$id.jpg"
}
- 接下来,找到要保存文件的目录,在CrimeRepository类里添加getPhotoFile(Crime)函数。
class CrimeRepository private constructor(context: Context) {
...
private val filesDir = context.applicationContext.filesDir
...
/**
* 返回指向某个具体位置的File对象
*/
fun getPhotoFile(crime: Crime) : File = File(filesDir,crime.photoFileName)
}
- 最后,在CrimeDetailViewModel类里添加一个函数,把文件信息告诉CrimeFragment。
class CrimeDetailViewModel : ViewModel() {
...
fun getPhotoFile(crime: Crime): File {
return crimeRepository.getPhotoFile(crime)
}
}
三、使用相机intent
- 1、保存照片文件存储位置
class CrimeFragment : Fragment(), DatePickerFragment.Callbacks {
...
private lateinit var photoFile: File
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
crimeDetailViewModel.crimeLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {
it?.let {
this.mCrime = it
photoFile = crimeDetailViewModel.getPhotoFile(it)
updateUI()
}
})
...
}
...
}
- 2、创建一个新属性保存图片URI,然后使用引用到的 photoFile 初始化它。
class CrimeFragment : Fragment(), DatePickerFragment.Callbacks {
...
private lateinit var photoUri: Uri
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
crimeDetailViewModel.crimeLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {
it?.let {
this.mCrime = it
photoFile = crimeDetailViewModel.getPhotoFile(it)
photoUri = FileProvider.getUriForFile(
requireActivity(),
"com.pyn.criminalintent.fileprovider",
photoFile
)
updateUI()
}
})
...
}
...
}
FileProvider.getUriForFile(...) 会把本地文件路径转换为相机能使用的Uri形式。这部分代码通常在公司项目中都不会写在 Fragment 或者 Activity 中,会以工具类的形式提取出来。
- 3、编写用于拍照的隐式 intent。
mBinding.imgCrimePhoto.apply {
val packageManager:PackageManager = requireActivity().packageManager
val captureImageIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val resolvedActivity:ResolveInfo? = packageManager.resolveActivity(captureImageIntent,PackageManager.MATCH_DEFAULT_ONLY)
if (resolvedActivity == null){
isEnabled = false
}
setOnClickListener { captureImageIntent.putExtra(MediaStore.EXTRA_OUTPUT,photoUri)
val cameraActivities :List<ResolveInfo> = packageManager.queryIntentActivities(captureImageIntent,PackageManager.MATCH_DEFAULT_ONLY)
for (cameraActivity in cameraActivities){
requireActivity().grantUriPermission(cameraActivity.activityInfo.packageName,photoUri,Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
startForResult2.launch(captureImageIntent)
}
}
运行起来可以打开相机,这里写入文件,还需要给相机应用权限。这里授予了 Intent.FLAG_GRANT_WRITE_URI_PERMISSION 给所有 cameraImage intent的目标 activity,以此允许它们在 Uri 指定的位置写文件。
四、缩放和显示位图
要展示照片,那么就需要加载照片到大小合适的Bitmap对象中,Bitmap是个对象,它只存储实际像素数据,即使原始照片已压缩过,但存入Bitmap对象时,文件并不会同样压缩,比如一张1600万像素24位的相机照片(存为JPG格式大约5 MB),一旦载入Bitmap对象,就会立即膨胀至48 MB。这样我们应用的内存可能就受不了了,那么就需要手动缩放位图照片。
- 1、新建 PictureUtils.kt 文件。
object PictureUtil {
/**
* 先确认屏幕的尺寸,然后按此缩放图像
*/
fun getScaledBitmap(path: String, activity: Activity):Bitmap{
val size = Point()
val outMetrics = DisplayMetrics()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
val display = activity.display
display?.getRealMetrics(outMetrics)
} else {
@Suppress("DEPRECATION")
val display = activity.windowManager.defaultDisplay
@Suppress("DEPRECATION")
display.getMetrics(outMetrics)
}
return getScaledBitmap(path,size.x,size.y)
}
fun getScaledBitmap(path: String, destWidth: Int, destHeigth: Int): Bitmap {
var options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, options)
val srcWidth = options.outWidth.toFloat()
val srcHeight = options.outHeight.toFloat()
var inSampleSize = 1
if (srcHeight > destHeigth || srcWidth > destWidth) {
val heightScale = srcHeight / destHeigth
val widthScale = srcWidth / destWidth
val sampleScale = if (heightScale > widthScale) {
heightScale
} else {
widthScale
}
inSampleSize = Math.round(sampleScale)
}
options = BitmapFactory.Options()
options.inSampleSize = inSampleSize
return BitmapFactory.decodeFile(path, options)
}
}
inSampleSize 决定着缩略图像素的大小,1 表示缩略图和原始照片的水平像素大小一样,2 缩略图的像素数就是原始文件的1/4。
编写更新 photoView 的函数,然后在要更新UI的时候调用它,即 updateUI() 中和 选择了照片回调中。
private fun updatePhotoView(){
if(photoFile.exists()){
val bitmap = PictureUtil.getScaledBitmap(photoFile.path, requireActivity())
mBinding.imgCrimePhoto.setImageBitmap(bitmap)
}else{
mBinding.imgCrimePhoto.setImageDrawable(null)
}
}
五、功能声明
有时候上架应用市场,市场商店是要求应用声明好自己使用到的功能的(相机、NFC等),否则可能拒绝上架。
声明应用要使用相机,在AndroidManifest.xml中加入<uses-feature>标签,
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pyn.criminalintent">
...
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
...
</manifest>
这里 android:required 属性为 false 表示尽管不带相机的设备会导致应用功能缺失,但应用仍然可以正常安装和使用。默认这个属性为 true。声明为 false,我们就应该在代码里处理好设备没有相机功能的逻辑。此应用是有检测是否有相机的,因此应该声明为 false。
六、挑战练习:优化照片显示
创建能显示放大版照片的DialogFragment。只要点击缩略图,就会弹出这个DialogFragment,让用户查看放大版的照片。这种功能在很多 App 里面都是会有的功能。嗯嗯,好好实现一下。
七、挑战练习:优化缩略图加载
Android 有个 ViewTreeObserver 的 API 工具。可以从Activity层级结构中获取任何视图的 ViewTreeObserver 对象,为 ViewTreeObserver 对象设置包括 OnGlobalLayoutListener 在内的各种监听器。使用 OnGlobalLayoutListener 监听器,可以监听任何布局的传递,控制事件的发生。
题目:使用有效的 photoView 尺寸,等到有布局切换时再调用updatePhotoView() 函数。
核心代码如下
mBinding.imgCrimePhoto.viewTreeObserver.addOnGlobalLayoutListener {
imgPhotoWidth = mBinding.imgCrimePhoto.measuredWidth
imgPhotoHeight = mBinding.imgCrimePhoto.measuredHeight
updatePhotoView(imgPhotoWidth, imgPhotoHeight)
}
最终效果:
效果
八、其他
CriminalIntent 项目 Demo 地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/CriminalIntent
网友评论