《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中加入标签,
代码清单:添加标签(manifest/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,

posted @ 2022-06-03 16:50  easy5  阅读(176)  评论(0编辑  收藏  举报