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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容