摄影圈子 hqmana.com

【Android进阶】16、使用 intent 拍照

16.1 布局照片和按钮

新增2个View对象,显示照片缩略图的ImageView,和拍照按钮。UI 如下图所示:

在这里插入图片描述

首先在 res/layout/fragment_crime.xml 中添加 ImageView 和 ImageButton,完整的布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="16dp">

    <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 android:orientation="vertical" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1">

            <TextView style="?android:listSeparatorTextViewStyle" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/crime_title_label"/>

            <EditText android:id="@+id/crime_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/crime_title_hint"/>
        </LinearLayout>
    </LinearLayout>

    <TextView style="?android:listSeparatorTextViewStyle" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/crime_details_label"/>

    <Button android:id="@+id/crime_date" android:layout_width="match_parent" android:layout_height="wrap_content" tools:text="Wed Nov 14 11:56 EST 2018"/>

    <CheckBox android:id="@+id/crime_solved" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/crime_solved_label"/>

    <Button android:id="@+id/crime_suspect" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/crime_suspect_text"/>

    <Button android:id="@+id/crime_report" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/crime_report_text"/>
</LinearLayout>

效果如下:

在这里插入图片描述

在 CrimeFragment.kt 中添加新属性,代码如下:

class CrimeFragment : Fragment(), DatePickerFragment.Callbacks {
    
    private lateinit var photoButton: ImageButton
    private lateinit var photoView: ImageView
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View? {
    
        val view = inflater.inflate(R.layout.fragment_crime, container, false)
        titleField = view.findViewById(R.id.crime_title) as EditText
        dateButton = view.findViewById(R.id.crime_date) as Button
        solvedCheckBox = view.findViewById(R.id.crime_solved) as CheckBox
        reportButton = view.findViewById(R.id.crime_report) as Button
        suspectButton = view.findViewById(R.id.crime_suspect) as Button
        photoButton = view.findViewById(R.id.crime_camera) as ImageButton
        photoView = view.findViewById(R.id.crime_photo) as ImageView
        return view
    }
}

16.2 文件存储

照片存储较大,需要放置在私有存储空间中,Context类有如下几个API:

  • 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,获取主文件目录下的文件列表。可与其他函数配合使用,比如openFileInput(String)。
  • getCacheDir(): File,获取/data/data/<包名>/cache目录。应注意及时清理该目录,并节约使用。

如果只有你自己 的应用能读写。不过,如果存储的文件仅供应用内部使用,上述各类函数也够用了。

但如果其他应用要读写你的文件,事情就没那么简单了。CriminalIntent应用就是这个情况:外部相机应用需要在你的应用里保存拍摄的照片。

如果想共享文件给其他应用,或是接收其他应用的文件(比如相机应用拍摄的照片),可以通过ContentProvider把要共享的文件暴露出来。ContentProvider允许你暴露内容URI给其他应用。这样,这些应用就可以从内容URI下载或向其中写入文件。当然,主动权在你手上,你可以控制读或写。

16.2.1 使用FileProvider

首先,声明FileProvider为ContentProvider,并给予一个指定的权限。在AndroidManifest.xml中添加一个FileProvider声明,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.bignerdranch.android.criminalintent">

    <application android:name=".CriminalIntentApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme">
        <activity android:name=".MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <provider android:name="androidx.core.content.FileProvider" android:authorities="com.bignerdranch.android.criminalintent.fileprovider" android:exported="false" android:grantUriPermissions="true" />
    </application>

</manifest>

其中,各字段含义如下:

  • android:authorities属性值在整个系统里要有唯一性。为了做到这点,一个习惯做法是在权限字符串里加上应用包名。
  • exported = "false"属性就意味着,除了你自己以及你授权的人,其他任何人都不允许使用你的FileProvider。
  • 而grantUriPermissions属性用来给其他应用授权,允许它们向你指定位置的URI(稍后你会看到,这个位置信息放在intent中对外发出)写入文件。

在这里插入图片描述
res/xml/files.xml 设置私有存储空间的根路径映射为 crime_photos,这个名字仅供 FileProvider 内部使用,我们不要用它,代码如下:

<paths>
    <files-path name="crime_photos" path="." />
</paths>

在AndroidManifest.xml文件中,添加一个meta-data标签,让FileProvider能找到files.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.bignerdranch.android.criminalintent">

    <application android:name=".CriminalIntentApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme">
        <activity android:name=".MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <provider android:name="androidx.core.content.FileProvider" android:authorities="com.bignerdranch.android.criminalintent.fileprovider" android:exported="false" android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/files" />
        </provider>
    </application>

</manifest>

16.2.2 指定照片存放位置

在Crime.kt中添加一个计算属性获取图片文件名,代码如下:

@Entity
data class Crime(
    @PrimaryKey val id: UUID = UUID.randomUUID(),
    var title: String = "",
    var date: Date = Date(),
    var isSolved: Boolean = false,
    var suspect: String = ""
) {
    
    val photoFileName get() = "IMG_$id.jpg"
}

在CrimeRepository类里添加getPhotoFile(Crime)函数,用于返回某个具体位置的File对象,代码如下:

class CrimeRepository private constructor(context: Context) {
    
    private val filesDir = context.applicationContext.filesDir
    fun getPhotoFile(crime: Crime): File = File(filesDir, crime.photoFileName)
}

最后,在CrimeDetailViewModel类里添加一个函数,把文件信息告诉CrimeFragment,代码如下:

class CrimeDetailViewModel() : ViewModel() {
    
    private val crimeRepository = CrimeRepository.get()
    private val crimeIdLiveData = MutableLiveData<UUID>()
    var crimeLiveData: LiveData<Crime?> =
        Transformations.switchMap(crimeIdLiveData) {
     crimeId ->
            crimeRepository.getCrime(crimeId)
        }

    fun loadCrime(crimeId: UUID) {
    
        crimeIdLiveData.value = crimeId
    }

    fun saveCrime(crime: Crime) {
    
        crimeRepository.updateCrime(crime)
    }

    fun getPhotoFile(crime: Crime): File {
    
        return crimeRepository.getPhotoFile(crime)
    }
}

16.3 使用相机 intent

    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)
                    updateUI()
                }
            }
        )
    }

16.3.1 触发拍照

我们需要的intent操作是定义在MediaStore类中的ACTION_IMAGE_CAPTURE。

MediaStore类定义了一些公共接口,可用于处理图像、视频以及音乐这些常见的多媒体任务。当然,这也包括触发相机应用的拍照intent。

ACTION_IMAGE_CAPTURE打开相机应用,默认只能拍摄缩略图这样的低分辨率照片,而且照片会保存在onActivityResult(…)返回的Intent对象里。

要想获得全尺寸照片,就要让它使用文件系统存储照片。这可以通过传入保存在MediaStore.EXTRA_OUTPUT中的指向存储路径的Uri来完成。这个Uri会指向FileProvider提供的位置。

首先,创建一个新属性保存图片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,
            Observer {
     crime ->
                crime?.let {
    
                    this.crime = crime
                    photoFile = crimeDetailViewModel.getPhotoFile(crime)
                    photoUri =
                        FileProvider.getUriForFile(
                            requireActivity(),
                            "com.bignerdranch.android.criminalintent.fileprovider",
                            photoFile
                        )
                    updateUI()
                }
            }
        )
    }
}

接下来是编写用于拍照的隐式intent,首先检查设备是否有相机,是否有空间存照片,设置将照片保存在 photoUri 的路径中,代码如下:

class CrimeFragment : Fragment(), DatePickerFragment.Callbacks {
    
    override fun onStart() {
    
        super.onStart()
        photoButton.apply {
    
            val packageManager: PackageManager = requireActivity().packageManager
            val captureImage = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            val resolvedActivity: ResolveInfo? =
                packageManager.resolveActivity(
                    captureImage,
                    PackageManager.MATCH_DEFAULT_ONLY
                )
            if (resolvedActivity == null) {
    
                isEnabled = false
            }
            setOnClickListener {
    
                captureImage.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
                val cameraActivities: List<ResolveInfo> =
                    packageManager.queryIntentActivities(
                        captureImage,
                        PackageManager.MATCH_DEFAULT_ONLY
                    )
                for (cameraActivity in cameraActivities) {
    
                    requireActivity().grantUriPermission(
                        cameraActivity.activityInfo.packageName,
                        photoUri,
                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    )
                }
                startActivityForResult(captureImage, REQUEST_PHOTO)
            }
        }
    }
}

要实际写入文件,还需给相机授予权限。

我们授予Intent.FLAG_GRANT_WRITE_URI_PERMISSION 给所有cameraImage intent的目标activity,以此允许它们在Uri指定的位置写文件。当然,还有个前提条件:在声明FileProvider的时候添加过android:grantUriPermissions属性。

打开设备的相机应用,效果如下:

在这里插入图片描述

16.4 缩放和显示位图

有了照片,接下来就是找到并加载它,然后展示给用户看。在技术实现上,这需要加载照片到大小合适的Bitmap对象中。

要从文件生成Bitmap对象,我们需要val bitmap = BitmapFactory.decodeFile(photoFile.getPath())

通常Bitmap图像都很大,所以可读取+缩放+存储,再重新读取缩放后的文件。

创建 PictureUtils.kt 文件,添加 getScaledBitmap(String, Int, Int) 缩放函数,其确定了 inSampleScale,并依此缩放图片,代码如下:

fun getScaledBitmap(path: String, destWidth: Int, destHeight: Int): Bitmap {
    
    // Read in the dimensions of the image on disk
    var options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    BitmapFactory.decodeFile(path, options)
    val srcWidth = options.outWidth.toFloat()
    val srcHeight = options.outHeight.toFloat()
    // Figure out how much to scale down by
    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
    // Read in and create final bitmap
    return BitmapFactory.decodeFile(path, options)
}

我们还需要添加一个getScaledBitmap(String, Activity) 来计算屏幕尺寸大小,防止 ImageView 过大而放不下到屏幕中的问题,代码如下:

fun getScaledBitmap(path: String, activity: Activity): Bitmap {
    
	val size = Point()
	activity.windowManager.defaultDisplay.getSize(size)
	return getScaledBitmap(path, size.x, size.y)
}

封装 updatePhotoView()函数 将 Bitmap 载入 ImageView,并在updateUI()和onActivityResult(…)函数中调用updatePhotoView()函数;在onActivityResult()中添加写文件权限,并在onDetach()中撤回写文件权限,代码如下:

class CrimeFragment : Fragment(), DatePickerFragment.Callbacks {
    
    private fun updateUI() {
    
        titleField.setText(crime.title)
        dateButton.text = crime.date.toString()
        solvedCheckBox.apply {
    
            isChecked = crime.isSolved
            jumpDrawablesToCurrentState()
        }
        if (crime.suspect.isNotEmpty()) {
    
            suspectButton.text = crime.suspect
        }
        updatePhotoView()
    }

    private fun updatePhotoView() {
    
        if (photoFile.exists()) {
    
            val bitmap = getScaledBitmap(photoFile.path, requireActivity())
            photoView.setImageBitmap(bitmap)
        } else {
    
            photoView.setImageDrawable(null)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    
        when {
    
            requestCode == REQUEST_PHOTO -> {
    
                requireActivity().revokeUriPermission(photoUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
                updatePhotoView()
            }
        }
    }
    override fun onDetach() {
    
        super.onDetach()
        requireActivity().revokeUriPermission(photoUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    }
}

点击拍照按钮,即可将图片防止在缩略图上,效果如下:

在这里插入图片描述

16.5 功能说明

我们的APP若需依赖某些功能,则需向os声明,这样安装应用时就会仅当依赖都具备时才安装,否则不安装。

在AndroidManifest.xml中加入 <uses-feature> 标签,代码如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.bignerdranch.android.criminalintent" >
	<uses-feature android:name="android.hardware.camera" android:required="false"/>
	...
</manifest>

版权声明
本文为[呆呆的猫]所创,转载请带上原文链接,感谢
https://blog.csdn.net/jiaoyangwm/article/details/126848326

Scroll to Top