如何在Android中使用Kotlin实现CameraX API

811 阅读7分钟

在靠近用户的地方部署容器

本工程教育(EngEd)计划由科支持。

在全球范围内即时部署容器。Section是经济实惠、简单而强大的。

免费入门

如何在Android中使用Kotlin实现CameraX API

10月28日, 2021

相机是移动设备中最重要的部件之一。它负责捕捉周围环境的光学图像。

当你想在你的Android应用中拍照时,一般有两条路可走。

  • 调用一个隐含的意图Intent(MediaStore.ACTION_IMAGE_CAPTURE) ,重定向到正常的相机应用程序。
  • 使用一个Camera API 来捕获图像。

在本教程中,我们将处理第二条路径。相机API更好,因为。

  • 它们对图像的拍摄和处理方式有更多的控制。
  • 它们能产生更高质量的图像。

我们将使用CameraX API。这是目前推荐用于拍照的Android API,因为。

  • 它已经过安卓团队的全面测试,以确保不同设备之间的一致性。
  • 与其他API相比,它更容易使用。

目标

在本教程中,我们将开发一个基本的相机应用。该应用程序将具有以下功能。

  • 在其上举行相机预览(即通过相机看到的屏幕)。
  • 在后置和前置摄像头之间切换。
  • 拍照并将其保存在一个本地化的存储位置。
  • 在一个可滚动的图库中查看拍摄的图片。

前提条件

要继续学习本教程,你需要对用Kotlin开发Android应用有基本的了解。

目录

项目设置

在Android Studio中创建一个新的空活动项目,并在build.gradle(app) 文件中添加以下依赖关系。

buildFeatures{
    //enable view binding
    viewBinding true
}
dependencies{
...
    //Check for the latest versions
    def camerax_version = "1.0.1"

    // CameraX core library using camera2 implementation
    implementation "androidx.camera:camera-camera2:$camerax_version"

    // CameraX Lifecycle Library
    implementation "androidx.camera:camera-lifecycle:$camerax_version"

    // CameraX View class
    implementation "androidx.camera:camera-view:1.0.0-alpha27"
}

你还需要在AndroidManifest.xml 文件中声明以下权限,在<application> 标签的上方。

<uses-feature android:name="android.hardware.camera.any"/>
<uses-permission android:name="android.permission.CAMERA"/>

相机预览

这是显示摄像机所看到的画面。

activity_main.xml 中用PreviewView 替换TextView

<androidx.camera.view.PreviewView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/preview"/>

在你开始之前,你的MainActivity.kt 文件应该看起来像这样。

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    companion object {
        val TAG = "MainActivity"
    }
}

添加以下属性。

private lateinit var cameraProviderFuture:ListenableFuture<ProcessCameraProvider>

ListenableFuture 是一个轻量级的接口,主动监听主线程之外发生的操作(异步操作)。在这种情况下,被监听的操作是 。ProcessCameraProvider

这个过程将被用来把摄像机的生命周期与应用程序的生命周期绑定起来。

添加一个cameraSelector 属性,这将有助于决定是使用前置还是后置摄像头。

private lateinit var cameraSelector:CameraSelector

onCreate 方法中,初始化这些变量。

cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

MainActivity.kt ,创建一个startCamera() ,该函数将在onCreate 方法中被调用。

这个函数将监听来自摄像头的数据。

然后我们将把Preview 用例连接到我们创建的xml 文件中的预览。

用例是开发人员可以访问摄像机功能的一种方式。

之后,在连接用例之前,重新初始化摄像机提供者。

private fun startCamera(){
    // listening for data from the camera
    cameraProviderFuture.addListener({
        val cameraProvider = cameraProviderFuture.get()

        // connecting a preview use case to the preview in the xml file.
        val preview = Preview.Builder().build().also{
            it.setSurfaceProvider(binding.preview.surfaceProvider)
        }
        try{
            // clear all the previous use cases first.
            cameraProvider.unbindAll()
            // binding the lifecycle of the camera to the lifecycle of the application.
            cameraProvider.bindToLifecycle(this,cameraSelector,preview)
        } catch (e: Exception) {
                Log.d(TAG, "Use case binding failed")
        }

    },ContextCompat.getMainExecutor(this))
}

ContextCompat.getMainExecutor(this) 是用来运行被 侦听的异步操作的。它的上下文是在应用程序内。cameraProviderFuture

onCreate 方法中,调用上述函数。

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    startCamera()
    ...
}

运行你的应用程序后,看起来你有一个被封锁的摄像头。为什么呢?嗯,这是因为摄像头的权限还没有被授予。

通过在onCreate 方法上面添加以下属性,向用户询问使用相机的权限。

private val cameraProviderResult = registerForActivityResult(ActivityResultContracts.RequestPermission()){ permissionGranted->
        if(permissionGranted){
            // cut and paste the previous startCamera() call here.
            startCamera()
        }else {
            Snackbar.make(binding.root,"The camera permission is required", Snackbar.LENGTH_INDEFINITE).show()
        }
    }

registerForActivityResult 是安卓的新API,从应用程序外部获取数据。它可以防止出现这样的情况,即应用程序的进程在从其他应用程序获得结果之前就被杀死。

关于ActivityResult APIs的更多信息,请参考文档。从一个活动中获取结果

onCreate ,执行合同。

cameraProviderResult.launch(android.Manifest.permission.CAMERA)

应用程序现在应该提示你允许摄像头的权限,之后摄像头就被激活了。

图像的捕捉和存储

activity_main.xml ,创建一个按钮,当点击时将用于拍摄照片。

<Button
    android:id="@+id/img_capture_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    android:text="Take a photo"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"/>

创建以下属性。

private var imageCapture: ImageCapture? = null
private lateinit var imgCaptureExecutor: ExecutorService
  • imageCapture 是一个用例,就像 用例一样。它用于捕获图像。Preview
  • imgCaptureExecutor 是一个扩展了 的接口。它的工作是提供一个将用于捕获图像的线程。Executor

onCreate 方法中实例化imgCaptureExecutor

imgCaptureExecutor = Executors.newSingleThreadExecutor()

startCamera() 函数中实例化imageCapture,并在将cameraProvider绑定到生命周期中时包括它。

private fun startCamera(){
    ...
    cameraProviderFuture.addListener({
        ...
        imageCapture = ImageCapture.Builder().build()

        try{
            ...
            cameraProvider.bindToLifecycle(this,cameraSelector,preview,imageCapture)
        }....
    )ContextCompat.getMainExecutor(this)}
}

创建一个takePhoto() 函数,只有在点击imgCaptureBtn 的时候才会被调用。

private fun takePhoto(){
    imageCapture?.let{
        //Create a storage location whose fileName is timestamped in milliseconds.
        val fileName = "JPEG_${System.currentTimeMillis()}"
        val file = File(externalMediaDirs[0],fileName)

        // Save the image in the above file
        val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build()

        /* pass in the details of where and how the image is taken.(arguments 1 and 2 of takePicture)
        pass in the details of what to do after an image is taken.(argument 3 of takePicture) */

        it.takePicture(
            outputFileOptions,
            imgCaptureExecutor,
            object : ImageCapture.OnImageSavedCallback {
                    override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults){
                        Log.i(TAG,"The image has been saved in ${file.toUri()}")
                    }

                    override fun onError(exception: ImageCaptureException) {
                        Toast.makeText(
                            binding.root.context,
                            "Error taking photo",
                            Toast.LENGTH_LONG
                        ).show()
                        Log.d(TAG, "Error taking photo:$exception")
                    }

            })
    }
}

接下来,创建一个animateFlash() 函数,在拍摄图像时使屏幕闪动。

@RequiresApi(Build.VERSION_CODES.M)
private fun animateFlash() {
    binding.root.postDelayed({
        binding.root.foreground = ColorDrawable(Color.WHITE)
        binding.root.postDelayed({
            binding.root.foreground = null
        }, 50)
    }, 100)
}

延迟100毫秒后,前景变为白色。然后在延迟50毫秒后恢复正常。

这种改变前景属性的功能只适用于Android M 及以上设备。

onCreate 方法中的imgCaptureBtn onClickListener中触发这些功能。

binding.imgCaptureBtn.setOnClickListener{
    takePhoto()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            animateFlash()
    }
}

运行你的应用程序并拍摄一张照片,然后打开logCat并搜索MainActivity 。你会看到图片被保存的位置。

切换相机

要切换相机,无论是从前面到后面还是从后面到前面,请做以下工作。

activity_main.xml 中创建一个切换按钮。

<Button
    android:id="@+id/switch_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    android:text="Switch"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toStartOf="@id/img_capture_btn"
    app:layout_constraintStart_toStartOf="parent" />

在MainActivity的onCreate 方法中,设置该按钮的onClickListener。

binding.switchBtn.setOnClickListener {
    //change the cameraSelector
    cameraSelector = if(cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA){
        CameraSelector.DEFAULT_FRONT_CAMERA
    }else {
        CameraSelector.DEFAULT_BACK_CAMERA
    }
    // restart the camera
    startCamera()
}

运行你的应用程序并点击Switch 按钮来切换摄像头。

本地画廊

为了查看照片,你需要创建一个可滚动的屏幕来显示这些图片。这可以通过一个连接到ViewPager 的RecyclerView适配器来实现。

build.gradle(app) 中添加以下依赖关系。

// Glide library for image management and loading.
implementation 'com.github.bumptech.glide:glide:4.12.0'

activity_main.xml 文件中添加另一个按钮,用于导航到一个新的活动。

<Button
    android:id="@+id/gallery_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    android:text="Gallery"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toEndOf="@id/img_capture_btn" />

接下来,到File -> New -> Activity -> EmptyActivity ,创建一个GalleryActivity

回到MainActivity.kt ,为画廊按钮设置点击监听器。

binding.galleryBtn.setOnClickListener {
    val intent = Intent(this, GalleryActivity::class.java)
    startActivity(intent)
}

运行你的应用程序并点击Gallery 按钮以导航到一个新的活动。

更新GalleryActivity.kt ,使用viewBinding。

class GalleryActivity : AppCompatActivity() {
    private lateinit var binding:ActivityGalleryBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityGalleryBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

activity_gallery.xml 中创建视图传呼机。

<androidx.viewpager2.widget.ViewPager2
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:id="@+id/view_pager"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"/>

创建一个新的布局资源文件list_item_img.xml ,其中将包含一个imageView。

<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:src="@tools:sample/backgrounds/scenic"
    android:id="@+id/local_img"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

创建一个新的Kotlin类GalleryAdapter 。这个适配器将在其构造函数中接受一个文件列表。这个列表将被glide和recyclerView用来渲染图片到imageViews。

class GalleryAdapter(private val fileArray: Array<File>) :
    RecyclerView.Adapter<GalleryAdapter.ViewHolder>() {
    class ViewHolder(private val binding: ListItemImgBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(file: File) {
            Glide.with(binding.root).load(file).into(binding.localImg)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return ViewHolder(ListItemImgBinding.inflate(layoutInflater, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(fileArray[position])
    }

    override fun getItemCount() = fileArray.size
}

关于如何使用viewPager和recyclerView的更详细的解释,请参考ViewPager2教程

最后,在GalleryActivity.kt ,做以下工作。

  • 向适配器提供文件列表。
  • 将适配器附加到viewPager上。
val directory = File(externalMediaDirs[0].absolutePath)
val files = directory.listFiles() as Array<File>

// array is reversed to ensure last taken photo appears first.
val adapter = GalleryAdapter(files.reversedArray())
binding.viewPager.adapter = adapter

现在你可以运行应用程序,拍摄照片,并查看它们。看吧

总结

在这篇文章中,我们已经学会了如何使用CameraX API来拍摄照片并在图库中查看它们。CameraX使得在应用程序中集成强大的相机功能变得容易。

你可以在Github资源库中找到源代码。

编码愉快!


同行评审贡献者:。Eric Gacoki

类似文章

[

How to Create a Reusable React Form component Hero Image

语言

如何创建一个可重复使用的React表单组件

阅读更多

](www.section.io/engineering…

Building a payroll system with next.js Hero Image

语言, Node.js

用Next.js构建一个薪资系统

阅读更多

](www.section.io/engineering…

Creating and Utilizing Decorators in Django example image

架构

在Django中创建和使用装饰器

阅读更多

](www.section.io/engineering…)