以下内容摘自郭霖《第一行代码》第三版
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/takePhoneBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Take Photo" />
<Button
android:id="@+id/fromAlbumBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="From Album" />
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
</LinearLayout>
布局文件中只有两个控件:一个Button和一个ImageView。Button是用于打开摄像头进行拍照的,而ImageView则是用于将拍到的图片显示出来。
MainActivity
package com.example.cameraalbumtest
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.ImageView
import androidx.core.content.FileProvider
import java.io.File
class MainActivity : AppCompatActivity() {
val takePhoto = 1
val fromAlbum = 2
lateinit var imageUri: Uri
lateinit var outputImage: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val takePhoneBtn: Button = findViewById(R.id.takePhoneBtn)
takePhoneBtn.setOnClickListener {
// 创建File对象,用于存储拍照后的图片,存放在手机SD卡的应用关联缓存目录下,具体的路径是/sdcard/Android/data/<package name>/cache
outputImage = File(externalCacheDir, "output_image.jpg")
if(outputImage.exists()){
outputImage.delete()
}
outputImage.createNewFile()
imageUri = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
// 如果运行设备的系统版本高于Android 7.0,就调用FileProvider的getUriForFile()方法将File对象转换成一个封装过的Uri对象。
FileProvider.getUriForFile(this, "com.example.cameraalbumtest.fileprovider", outputImage)
}else{
// 如果运行设备的系统版本低于Android 7.0,就调用Uri的fromFile() 方法将File对象转换成Uri对象,这个Uri对象标识着output_image.jpg这张图片的本地真实路径。
Uri.fromFile(outputImage)
}
// 启动相机程序
val intent = Intent("android.media.action.IMAGE_CAPTURE")
// 指定图片的输出地址
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
// 启动Activity
startActivityForResult(intent, takePhoto)
}
val fromAlbumBtn: Button = findViewById(R.id.fromAlbumBtn)
fromAlbumBtn.setOnClickListener {
// 打开文件选择器
// 构建了一个Intent对象,并将它的action指定为Intent.ACTION_OPEN_DOCUMENT,表示打开系统的文件选择器。
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
// 指定只显示图片
// 注意:要coyp代码需要把/后面的那个空格去掉,这里有个MD文档的书写bug
intent.type = "image/ *"
startActivityForResult(intent, fromAlbum)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val imageView: ImageView = findViewById(R.id.imageView)
when(requestCode){
takePhoto -> {
if(resultCode == Activity.RESULT_OK){
// 将拍摄的照片显示出来
// 如果发现拍照成功,就可以调用BitmapFactory的decodeStream()方法将output_image.jpg这张照片解析成Bitmap对象,然后把它设置到ImageView中显示出来。
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
imageView.setImageBitmap(rotateIfRequired(bitmap))
}
}
fromAlbum -> {
if(resultCode == Activity.RESULT_OK && data != null){
// 调用了返回Intent的getData()方法来获取选中图片的Uri,
data.data?.let {uri ->
// 将选择的图片显示出来
//然后再调用getBitmapFromUri()方法将Uri转换成Bitmap对象
val bitmap = getBitmapFromUri(uri)
imageView.setImageBitmap(bitmap)
}
}
}
}
}
private fun getBitmapFromUri(uri: Uri) = contentResolver
.openFileDescriptor(uri, "r")?.use{
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}
private fun rotateIfRequired(bitmap: Bitmap): Bitmap{
val exif = ExifInterface(outputImage.path)
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
return when(orientation){
ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270)
else -> bitmap
}
}
private fun rotateBitmap(bitmap: Bitmap, degeree: Int): Bitmap{
val matrix = Matrix()
matrix.postRotate(degeree.toFloat())
val rotatedBitMap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
bitmap.recycle()
return rotatedBitMap
}
}
先来看用摄像头拍照展示的逻辑:
首先这里创建了一个File对象,用于存放摄像头拍下的图片,这里我们把图片命名为output_image.jpg,并存放在手机SD卡的应用关联缓存目录下。应用关联缓存目录就是指SD卡中专门用于存放当前应用缓存数据的位置,调用getExternalCacheDir()方法可以得到这个目录,具体的路径是/sdcard/Android/data/<package name>/cache
。使用应用关联缓存目录来存放图片是因为从Android 6.0系统开始,读写SD卡被列为了危险权限,如果将图片存放在SD卡的任何其他目录,都要进行运行时权限处理才行,而使用应用关联目录则可以跳过这一步。从Android 10.0系统开始,公有的SD卡目录已经不再允许被应用程序直接访问了,而是要使用作用域存储才行。
接着会进行一个判断,如果运行设备的系统版本低于Android 7.0,就调用Uri的fromFile()方法将File对象转换成Uri对象,这个Uri对象标识着output_image.jpg这张图片的本地真实路径。否则,就调用FileProvider的getUriForFile()方法将File对象转换成一个封装过的Uri对象。getUriForFile()方法接收3个参数:
- 第一个参数要求传入Context对象
- 第二个参数可以是任意唯一的字符串
- 第三个参数是我们刚刚创建的File对象。
之所以要进行这样一层转换,是因为从Android 7.0系统开始,直接使用本地真实路径的Uri被认为是不安全的,会抛出一个FileUriExposedException异常。而FileProvider则是一种特殊的ContentProvider,它使用了和ContentProvider类似的机制来对数据进行保护,可以选择性地将封装过的Uri共享给外部,从而提高了应用的安全性。
接下来构建了一个Intent对象,并将这个Intent的action指定为android.media.action.IMAGE_CAPTURE,再调用Intent的putExtra()方法指定图片的输出地址,这里填入刚刚得到的Uri对象,最后调用startActivityForResult()启动Activity。由于我们使用的是一个隐式Intent,系统会找出能够响应这个Intent的Activity去启动,这样照相机程序就会被打开,拍下的照片将会输出到output_image.jpg中。
由于刚才是使用startActivityForResult()启动Activity的,因此拍完照后会有结果返回到onActivityResult()方法中。如果发现拍照成功,就可以调用BitmapFactory的decodeStream()方法将output_image.jpg这张照片解析成Bitmap对象,然后把它设置到ImageView中显示出来。
注意:调用照相机程序去拍照有可能会在一些手机上发生照片旋转的情况。这是因为这些手机认为打开摄像头进行拍摄时手机就应该是横屏的,因此回到竖屏的情况下就会发生90度的旋转。为此,这里我们又加上了判断图片方向的代码,如果发现图片需要进行旋转,那么就先将图片旋转相应的角度,然后再显示到界面上。
再来看从相册中选择图片的逻辑:
在“From Album”按钮的点击事件里,我们先构建了一个Intent对象,并将它的action指定为Intent.ACTION_OPEN_DOCUMENT,表示打开系统的文件选择器。接着给这个Intent对象设置一些条件过滤,只允许可打开的图片文件显示出来,然后调用startActivityForResult()方法即可。注意,在调用startActivityForResult()方法的时候,我们给第二个参数传入的值变成了fromAlbum,这样当选择完图片回到
onActivityResult()方法时,就会进入fromAlbum的条件下处理图片。
接下来的部分就很简单了,我们调用了返回Intent的getData()方法来获取选中图片的Uri,然后再调用getBitmapFromUri()方法将Uri转换成Bitmap对象,最终将图片显示到界面上。
在AndroidManifest.xml中对ContentProvider进行注册:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
...
>
<provider
android:authorities="com.example.cameraalbumtest.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
...
</application>
</manifest>
android:name属性的值是固定的,而android:authorities属性的值必须和刚才FileProvider.getUriForFile()方法中的第二个参数一致。另外,这里还在<provider>
标签的内部使用<meta-data>
指定Uri的共享路径,并引用了一个@xml/file_paths资源。
file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="my_images"
path="/" />
</paths>
external-path就是用来指定Uri共享路径的,name属性的值可以随便填,path属性的值表示共享的具体路径。这里使用一个单斜线表示将整个SD卡进行共享,当然也可以仅共享存放output_image.jpg这张图片的路径。
最后来看真机效果图截图: