美文网首页
《Android编程权威指南》之使用intent拍照

《Android编程权威指南》之使用intent拍照

作者: 夜远曦白 | 来源:发表于2021-11-10 23:26 被阅读0次

    转眼就《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

    相关文章

      网友评论

          本文标题:《Android编程权威指南》之使用intent拍照

          本文链接:https://www.haomeiwen.com/subject/dcjenltx.html