Android Camera2 教程 · 第三章 · 预览

Android Camera2 教程 · 第三章 · 预览

DarylGo关注

Android Camera

上一章《Camera2 开启相机》我们学习了如何开启和关闭相机,接下来我们来学习如何开启预览。

阅读完本章,你将会学到以下几个知识点:

  1. 如何配置预览尺寸
  2. 如何创建 CameraCaptureSession
  3. 如何创建 CaptureRequest
  4. 如何开启和关闭预览
  5. 如何适配预览画面的比例
  6. 如何使用 ImageReader 获取预览数据
  7. 设备方向的概念
  8. 局部坐标系的概念
  9. 显示方向的概念
  10. 摄像头传感器方向的概念
  11. 如何矫正图像数据的方向

你可以在 https://github.com/darylgo/Camera2Sample 下载相关的源码,并且切换到 Tutorial3 标签下。

1 获取预览尺寸

在第一章《Camera2 概览》我们提到了 CameraCharacteristics 是一个只读的相机信息提供者,其内部携带大量的相机信息,包括代表相机朝向的 LENS_FACING;判断闪光灯是否可用的 FLASH_INFO_AVAILABLE;获取所有可用 AE 模式的 CONTROL_AE_AVAILABLE_MODES 等等。如果你对 Camera1 比较熟悉,那么 CameraCharacteristics 有点像 Camera1 的 Camera.CameraInfo 或者 Camera.Parameters。CameraCharacteristics 以键值对的方式提供相机信息,你可以通过 CameraCharacteristics.get() 方法获取相机信息,该方法要求你传递一个 Key 以确定你要获取哪方面的相机信息,例如下面的代码展示了如何获取摄像头方向信息:

val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
val lensFacing = cameraCharacteristics[CameraCharacteristics.LENS_FACING]
when(lensFacing) {
    CameraCharacteristics.LENS_FACING_FRONT -> { // 前置摄像头 }
    CameraCharacteristics.LENS_FACING_BACK -> { // 后置摄像头 }
    CameraCharacteristics.LENS_FACING_EXTERNAL -> { // 外置摄像头 }
}

CameraCharacteristics 有大量的 Key 定义,这里就不一一阐述,当你在开发过程中需要获取某些相机信息的时候再去查阅 API文档即可。

由于不同厂商对相机的实现都会有差异,所以很多参数在不同的手机上支持的情况也不一样,相机的预览尺寸也是,所以接下来我们就要通过 CameraCharacteristics 获取相机支持的预览尺寸列表。所谓的预览尺寸,指的就是相机把画面输出到手机屏幕上供用户预览的尺寸,通常来说我们希望预览尺寸在不超过手机屏幕分辨率的情况下,越大越好。另外,出于业务需求,我们的相机可能需要支持多种不同的预览比例供用户选择,例如 4:3 和 16:9 的比例。由于不同厂商对相机的实现都会有差异,所以很多参数在不同的手机上支持的情况也不一样,相机的预览尺寸也是。所以在设置相机预览尺寸之前,我们先通过 CameraCharacteristics 获取该设备支持的所有预览尺寸:

val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedSizes = streamConfigurationMap?.getOutputSizes(SurfaceTexture::class.java)

从上面的代码可以看出预览尺寸列表并不是直接从 CameraCharacteristics 获取的,而是先通过 SCALER_STREAM_CONFIGURATION_MAP 获取 StreamConfigurationMap 对象,然后通过 StreamConfigurationMap.getOutputSizes() 方法获取尺寸列表,该方法会要求你传递一个 Class 类型,然后根据这个类型返回对应的尺寸列表,如果给定的类型不支持,则返回 null,你可以通过 StreamConfigurationMap.isOutputSupportedFor() 方法判断某一个类型是否被支持,常见的类型有:

  • ImageReader:常用来拍照或接收 YUV 数据。
  • MediaRecorder:常用来录制视频。
  • MediaCodec:常用来录制视频。
  • SurfaceHolder:常用来显示预览画面。
  • SurfaceTexture:常用来显示预览画面。

由于我们使用的是 SurfaceTexture,所以显然这里我们就要传递 SurfaceTexture.class 获取支持的尺寸列表。如果我们把所有的预览尺寸都打印出来看时,会发现一个比较特别的情况,就是预览尺寸的宽是长边,高是短边,例如 1920x1080,而不是 1080x1920,这是因为相机 Sensor 的宽是长边,而高是短边。

在获取到预览尺寸列表之后,我们要根据自己的实际需求过滤出其中一个最符合要求的尺寸,并且把它设置给相机,在我们的 Demo 里,只有当预览尺寸的比例和大小都满足要求时才能被设置给相机,如下所示:

@WorkerThread
private fun getOptimalSize(cameraCharacteristics: CameraCharacteristics, clazz: Class<*>, maxWidth: Int, maxHeight: Int): Size? {
    val aspectRatio = maxWidth.toFloat() / maxHeight
    val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    val supportedSizes = streamConfigurationMap?.getOutputSizes(clazz)
    if (supportedSizes != null) {
        for (size in supportedSizes) {
            if (size.width.toFloat() / size.height == aspectRatio && size.height <= maxHeight && size.width <= maxWidth) {
                return size
            }
        }
    }
    return null
}

2 配置预览尺寸

在获取适合的预览尺寸之后,接下来就是配置预览尺寸使其生效了。在配置尺寸方面,Camera2 和 Camera1 有着很大的不同,Camera1 是将所有的尺寸信息都设置给相机,而 Camera2 则是把尺寸信息设置给 Surface,例如接收预览画面的 SurfaceTexture,或者是接收拍照图片的 ImageReader,相机在输出图像数据的时候会根据 Surface 配置的 Buffer 大小输出对应尺寸的画面。

获取 Surface 的方式有很多种,可以通过 TextureView、SurfaceView、ImageReader 甚至是通过 OpenGL 创建,这里我们要将预览画面显示在屏幕上,所以我们选择了 TextureView,并且通过 TextureView.SurfaceTextureListener 回调接口监听 SurfaceTexture 的状态,在获取可用的 SurfaceTexture 对象之后通过 SurfaceTexture.setDefaultBufferSize() 设置预览画面的尺寸,最后使用 Surface(SurfaceTexture) 构造方法创建出预览的 Surface 对象:

首先,我们在布局文件中添加一个 TextureView,并给它取个 ID 叫 camera_preview:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextureView
        android:id="@+id/camera_preview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

然后我们在 Activity 里获取 TextureView 对象,并且注册一个 TextureView.SurfaceTextureListener 用于监听 SurfaceTexture 的状态:

private inner class PreviewSurfaceTextureListener : TextureView.SurfaceTextureListener {
    @MainThread
    override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) = Unit

    @MainThread
    override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) = Unit

    @MainThread
    override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean = false

    @MainThread
    override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
        previewSurfaceTexture = surfaceTexture
    }
}
cameraPreview = findViewById<CameraPreview>(R.id.camera_preview)
cameraPreview.surfaceTextureListener = PreviewSurfaceTextureListener()

当 SurfaceTexture 可用的时候会回调 onSurfaceTextureAvailable() 方法并且把 SurfaceTexture 对象和尺寸传递给我们,此时我们要做的就是通过 SurfaceTexture.setDefaultBufferSize() 设置预览画面的尺寸并且创建 Surface 对象:

val previewSize = getOptimalSize(cameraCharacteristics, SurfaceTexture::class.java, width, height)!!
previewSurfaceTexture.setDefaultBufferSize(previewSize.width, previewSize.height)
previewSurface = Surface(previewSurfaceTexture)

到这里,用于预览的 Surface 就准备好了,接下来我们来看下如何创建 CameraCaptureSession。

3 创建 CameraCaptureSession

用于接收预览画面的 Surface 准备就绪了,接了下来我们要使用这个 Surface 创建一个 CameraCaptureSession 实例,涉及的方法是 CameraDevice.createCaptureSession(),该方法要求你传递以下三个参数:

  • outputs:所有用于接收图像数据的 Surface,例如本章用于接收预览画面的 Surface,后续还会有用于拍照的 Surface,这些 Surface 必须在创建 Session 之前就准备好,并且在创建 Session 的时候传递给底层用于配置 Pipeline。
  • callback:用于监听 Session 状态的 CameraCaptureSession.StateCallback 对象,就如同开关相机一样,创建和销毁 Session 也需要我们注册一个状态监听器。
  • handler:用于执行 CameraCaptureSession.StateCallback 的 Handler 对象,可以是异步线程的 Handler,也可以是主线程的 Handler,在我们的 Demo 里使用的是主线程 Handler。
private inner class SessionStateCallback : CameraCaptureSession.StateCallback() {
    @MainThread
    override fun onConfigureFailed(session: CameraCaptureSession) {

    }

    @MainThread
    override fun onConfigured(session: CameraCaptureSession) {
       
    }

    @MainThread
    override fun onClosed(session: CameraCaptureSession) {
        
    }
}
val sessionStateCallback = SessionStateCallback()
val outputs = listOf(previewSurface)
cameraDevice.createCaptureSession(outputs, sessionStateCallback, mainHandler)

4 创建 CaptureRequest

在介绍如何开启和关闭预览之前,我们有必要先介绍下 CaptureRequest,因为它是我们执行任何相机操作都绕不开的核心类,因为 CaptureRequest 是向 CameraCaptureSession 提交 Capture 请求时的信息载体,其内部包括了本次 Capture 的参数配置和接收图像数据的 Surface。CaptureRequest 可以配置的信息非常多,包括图像格式、图像分辨率、传感器控制、闪光灯控制、3A 控制等等,可以说绝大部分的相机参数都是通过 CaptureRequest 配置的。我们可以通过 CameraDevice.createCaptureRequest() 方法创建一个 CaptureRequest.Builder 对象,该方法只有一个参数 templateType 用于指定使用何种模板创建 CaptureRequest.Builder 对象。因为 CaptureRequest 可以配置的参数实在是太多了,如果每一个参数都要我们手动去配置,那真的是既复杂又费时,所以 Camera2 根据使用场景的不同,为我们事先配置好了一些常用的参数模板:

  • TEMPLATE_PREVIEW:适用于配置预览的模板。
  • TEMPLATE_RECORD:适用于视频录制的模板。
  • TEMPLATE_STILL_CAPTURE:适用于拍照的模板。
  • TEMPLATE_VIDEO_SNAPSHOT:适用于在录制视频过程中支持拍照的模板。
  • TEMPLATE_MANUAL:适用于希望自己手动配置大部分参数的模板。

这里我们要创建一个用于预览的 CaptureRequest,所以传递了 TEMPLATE_PREVIEW 作为参数:

val requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)

一个 CaptureRequest 除了需要配置很多参数之外,还要求至少配置一个 Surface(任何相机操作的本质都是为了捕获图像),并且配置的 Surface 必须属于创建 Session 时添加的那些 Surface,涉及的方法是 CaptureRequest.Builder.addTarget(),你可以多次调用该方法添加多个 Surface。

requestBuilder.addTarget(previewSurface)

最后,我们通过 CaptureRequest.Builder.build() 方法创建出一个只读的 CaptureRequest 实例:

val request = requestBuilder.build()

5 开启和停止预览

在 Camera2 里,预览本质上是不断重复执行的 Capture 操作,每一次 Capture 都会把预览画面输出到对应的 Surface 上,涉及的方法是 CameraCaptureSession.setRepeatingRequest(),该方法有三个参数:

  • request:在不断重复执行 Capture 时使用的 CaptureRequest 对象。
  • callback:监听每一次 Capture 状态的 CameraCaptureSession.CaptureCallback 对象,例如 onCaptureStarted() 意味着一次 Capture 的开始,而 onCaptureCompleted() 意味着一次 Capture 的结束。
  • hander:用于执行 CameraCaptureSession.CaptureCallback 的 Handler 对象,可以是异步线程的 Handler,也可以是主线程的 Handler,在我们的 Demo 里使用的是主线程 Handler。

了解了核心方法之后,开启预览的操作就很显而易见了:

val requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
requestBuilder.addTarget(previewSurface)
val request = requestBuilder.build()
captureSession.setRepeatingRequest(request, RepeatingCaptureStateCallback(), mainHandler)

如果要关闭预览的话,可以通过 CameraCaptureSession.stopRepeating() 停止不断重复执行的 Capture 操作:

captureSession.stopRepeating()

到目前为止,如果一切正常的话,预览画面应该就已经显示出来了。

6 适配预览比例

前面我们使用了一个占满屏幕的 TextureView 来显示预览画面,并且预览尺寸我们选择了 4:3 的比例,你很可能会看到预览画面变形的情况,这因为 Surface 的比例和 TextureView 的比例不一致导致的,你可以想象 Surface 就是一张图片,TextureView 就是 ImageView,将 4:3 的图片显示在 16:9 的 ImageView 上必然会出现画面拉伸变形的情况:

预览画面变形

所以接下来我们要学习的是如何适配不同的预览比例。预览比例的适配有多种方式:

  1. 根据预览比例修改 TextureView 的宽高,比如用户选择了 4:3 的预览比例,这个时候我们会选取 4:3 的预览尺寸并且把 TextureView 修改成 4:3 的比例,从而让画面不会变形。
  2. 使用固定的预览比例,然后根据比例去选取适合的预览尺寸,例如固定 4:3 的比例,选择 1440x1080 的尺寸,并且把 TextureView 的宽高也设置成 4:3。
  3. 固定 TextureView 的宽高,然后根据预览比例使用 TextureView.setTransform() 方法修改预览画面绘制在 TextureView 上的方式,从而让预览画面不变形,这跟 ImageView.setImageMatrix() 如出一辙。

简单来说,解决预览画面变形的问题,本质上就是解决画面和画布比例不一致的问题。在我们的 Demo 中,出于简化的目的,我们选择了第二种方式适配比例,因为这种方式实现起来比较简单,所以我们会写一个自定义的 TextureView,让它的比例固定是 4:3,它的宽度固定填满父布局,高度根据比例动态计算:

class CameraPreview @JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : TextureView(context, attrs, defStyleAttr) {
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val width = MeasureSpec.getSize(widthMeasureSpec)
        setMeasuredDimension(width, width / 3 * 4)
    }
}

7 认识 ImageReader

在 Camera2 里,ImageReader 是获取图像数据的一个重要途径,我们可以通过它获取各种各样格式的图像数据,例如 JPEG、YUV 和 RAW 等等。我们可以通过 ImageReader.newInstance() 方法创建一个 ImageReader 对象,该方法要求我们传递以下四个参数:

  • width:图像数据的宽度。
  • height:图像数据的高度。
  • format:图像数据的格式,定义在 ImageFormat 里,例如 ImageFormat.YUV_420_888
  • maxImages:最大 Image 个数,可以理解成 Image 对象池的大小。

当有图像数据生成的时候,ImageReader 会通过通过 ImageReader.OnImageAvailableListener.onImageAvailable() 方法通知我们,然后我们可以调用 ImageReader.acquireNextImage() 方法获取存有最新数据的 Image 对象,而在 Image 对象里图像数据又根据不同格式被划分多个部分分别存储在单独的 Plane 对象里,我们可以通过调用 Image.getPlanes() 方法获取所有的 Plane 对象的数组,最后通过 Plane.getBuffer() 获取每一个 Plane 里存储的图像数据。以 YUV 数据为例,当有 YUV 数据生成的时候,数据会被分成 Y、U、V 三部分分别存储到 Plane 里,如下图所示:

override fun onImageAvailable(imageReader: ImageReader) {
    val image = imageReader.acquireNextImage()
    if (image != null) {
        val planes = image.planes
        val yPlane = planes[0]
        val uPlane = planes[1]
        val vPlane = planes[2]
        val yBuffer = yPlane.buffer // Data from Y channel
        val uBuffer = uPlane.buffer // Data from U channel
        val vBuffer = vPlane.buffer // Data from V channel
    }
    image?.close()
}

上面的代码是获取 YUV 数据的流程,特别要注意的是最后一步调用 Image.close() 方法十分重要,当我们不再需要使用某一个 Image 对象的时候记得通过该方法释放资源,因为 Image 对象实际上来自于一个创建 ImageReader 时就确定大小的对象池,如果我们不释放它的话就会导致对象池很快就被耗光,并且抛出一个异常。类似的的当我们不再需要使用 某一个 ImageReader 对象的时候,也要记得调用 ImageReader.close() 方法释放资源。

8 获取预览数据

介绍完 ImageReader 之后,接下来我们就来创建一个接收每一帧预览数据的 ImageReader,并且数据格式为 YUV_420_888。首先,我们要先判断 YUV_420_888 数据格式是否支持,所以会有如下的代码:

val imageFormat = ImageFormat.YUV_420_888
val streamConfigurationMap = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
if (streamConfigurationMap?.isOutputSupportedFor(imageFormat) == true) {
    // YUV_420_888 is supported
}

接着,我们使用前面已经确定好的预览尺寸创建一个 ImageReader,并且注册一个 ImageReader.OnImageAvailableListener 用于监听数据的更新,最后通过 ImageReader.getSurface() 方法获取接收预览数据的 Surface:

val imageFormat = ImageFormat.YUV_420_888
val streamConfigurationMap = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
if (streamConfigurationMap?.isOutputSupportedFor(imageFormat) == true) {
    previewDataImageReader = ImageReader.newInstance(previewSize.width, previewSize.height, imageFormat, 3)
    previewDataImageReader?.setOnImageAvailableListener(OnPreviewDataAvailableListener(), cameraHandler)
    previewDataSurface = previewDataImageReader?.surface
}

创建完 ImageReader,并且获取它的 Surface 之后,我们就可以在创建 Session 的时候添加这个 Surface 告诉 Pipeline 我们有一个专门接收 YUV_420_888 的 Surface:

val sessionStateCallback = SessionStateCallback()
val outputs = mutableListOf<Surface>()
val previewSurface = previewSurface
val previewDataSurface = previewDataSurface
outputs.add(previewSurface!!)
if (previewDataSurface != null) {
    outputs.add(previewDataSurface)
}
cameraDevice.createCaptureSession(outputs, sessionStateCallback, mainHandler)

获取预览数据和显示预览画面一样都是不断重复执行的 Capture 操作,所以我们只需要在开始预览的时候通过 CaptureRequest.Builder.addTarget() 方法添加接收预览数据的 Surface 即可,所以一个 CaptureRequest
会有两个 Surface,一个现实预览画面的 Surface,一个接收预览数据的 Surface:

val requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
val previewSurface = previewSurface
val previewDataSurface = previewDataSurface
requestBuilder.addTarget(previewSurface!!)
if (previewDataSurface != null) {
    requestBuilder.addTarget(previewDataSurface)
}
val request = requestBuilder.build()
captureSession.setRepeatingRequest(request, RepeatingCaptureStateCallback(), mainHandler)

在开始预览之后,每一次刷新预览画面的时候,都会通过 ImageReader.OnImageAvailableListener.onImageAvailable() 方法通知我们:

private inner class OnPreviewDataAvailableListener : ImageReader.OnImageAvailableListener {

    /**
     * Called every time the preview frame data is available.
     */
    override fun onImageAvailable(imageReader: ImageReader) {
        val image = imageReader.acquireNextImage()
        if (image != null) {
            val planes = image.planes
            val yPlane = planes[0]
            val uPlane = planes[1]
            val vPlane = planes[2]
            val yBuffer = yPlane.buffer // Data from Y channel
            val uBuffer = uPlane.buffer // Data from U channel
            val vBuffer = vPlane.buffer // Data from V channel
        }
        image?.close()
    }
}

9 如何矫正图像数据的方向

如果你熟悉 Camera1 的话,也许已经发现了一个问题,就是 Camera2 不需要经过任何预览画面方向的矫正,就可以正确现实画面,而 Camera1 则需要根据摄像头传感器的方向进行预览画面的方向矫正。其实,Camera2 也需要进行预览画面的矫正,只不过系统帮我们做了而已,当我们使用 TextureView 或者 SurfaceView 进行画面预览的时候,系统会根据【设备自然方向】、【摄像传感器方向】和【显示方向】自动矫正预览画面的方向,并且该矫正规则只适用于显示方向和和设备自然方向一致的情况下,举个例子,当我们把手机横放并且允许自动旋转屏幕的时候,看到的预览画面的方向就是错误的。此外,当我们使用一个 GLSurfaceView 显示预览画面或者使用 ImageReader 接收图像数据的时候,系统都不会进行画面的自动矫正,因为它不知道我们要如何显示预览画面,所以我们还是有必要学习下如何矫正图像数据的方向,在介绍如何矫正图像数据方向之前,我们需要先了解几个概念,它们分别是【设备自然方向】、【局部坐标系】、【显示方向】和【摄像头传感器方向】。

9.1 设备方向

当我们谈论方向的时候,实际上都是相对于某一个 0° 方向的角度,这个 0° 方向被称作自然方向,例如人站立的时候就是自然方向,你总不会认为一个人要倒立的时候才是自然方向吧,而接下来我们要谈论的设备方向就有的自然方向的定义。

设备方向指的是硬件设备在空间中的方向与其自然方向的顺时针夹角。这里提到的自然方向指的就是我们手持一个设备的时候最习惯的方向,比如手机我们习惯竖着拿,而平板我们则习惯横着拿,所以通常情况下手机的自然方向就是竖着的时候,平板的自然方向就是横着的时候。

以手机为例,我们可以有以下四个比较常见的设备方向:

  • 当我们把手机垂直放置且屏幕朝向我们的时候,设备方向为 0°,即设备自然方向
  • 当我们把手机向右横放且屏幕朝向我们的时候,设备方向为 90°
  • 当我们把手机倒着放置且屏幕朝向我们的时候,设备方向为 180°
  • 当我们把手机向左横放且屏幕朝向我们的时候,设备方向为 270°

了解了设备方向的概念之后,我们可以通过 OrientationEventListener 监听设备的方向,进而判断设备当前是否处于自然方向,当设备的方向发生变化的时候会回调 OrientationEventListener.onOrientationChanged(int) 方法,传给我们一个 0° 到 359° 的方向值,其中 0° 就代表设备处于自然方向。

9.2 局部坐标系

所谓的局部坐标系指的是当设备处于自然方向时,相对于设备屏幕的坐标系,该坐标系是固定不变的,不会因为设备方向的变化而改变,下图是基于手机的局部坐标系示意图:

局部坐标系

  • x 轴是当手机处于自然方向时,和手机屏幕平行且指向右边的坐标轴。
  • y 轴是当手机处于自然方向时,和手机屏幕平行且指向上方的坐标轴。
  • z 轴是当手机处于自然方向时,和手机屏幕垂直且指向屏幕外面的坐标轴。

为了进一步解释【坐标系是固定不变的,不会因为设备方向的变化而改变】的概念,这里举个例子,当我们把手机向右横放且屏幕朝向我们的时候,此时设备方向为 90°,局部坐标系相对于手机屏幕是保持不变的,所以 y 轴正方向指向右边,x 轴正方向指向下方,z 轴正方向还是指向屏幕外面,如下图所示:

设备方向 90°

9.3 显示方向

显示方向指的是屏幕上显示画面与局部坐标系 y 轴的顺时针夹角。

为了更清楚的说明这个概念,我们举一个例子,假设我们将手机向右横放看电影,此时画面是朝上的,如下图所示:

屏幕方向

从上图来看,手机向右横放会导致设备方向变成了 90°,但是显示方向却是 270°,因为它是相对局部坐标系 y 轴的顺时针夹角,所以跟设备方向没有任何关系。如果把图中的设备换成是平板,结果就不一样了,因为平板横放的时候就是它的设备自然方向,y 轴朝上,屏幕画面显示的方向和 y 轴的夹角是 0°,设备方向也是 0°。

总结一下,设备方向是相对于其现实空间中自然方向的角度,而显示方向是相对局部坐标系的角度。

9.4 摄像头传感器方向

摄像头传感器方向指的是传感器采集到的画面方向经过顺时针旋转多少度之后才能和局部坐标系的 y 轴正方向一致,也就是通过 CameraCharacteristics.SENSOR_ORIENTATION 获取到的值。

例如 orientation 为 90° 时,意味我们将摄像头采集到的画面顺时针旋转 90° 之后,画面的方向就和局部坐标系的 y 轴正方向一致,换个说法就是原始画面的方向和 y 轴的夹角是逆时针 90°。

最后我们要考虑一个特殊情况,就是前置摄像头的画面是做了镜像处理的,也就是所谓的前置镜像操作,这个情况下, orientation 的值并不是实际我们要旋转的角度,我们需要取它的镜像值才是我们真正要旋转的角度,例如 orientation 为 270°,实际我们要旋转的角度是 90°。

注意:摄像头传感器方向在不同的手机上可能不一样,大部分手机都是 90°,也有小部分是 0° 的,所以我们要通过 CameraCharacteristics.SENSOR_ORIENTATION 去判断方向,而不是假设所有设备的摄像头传感器方向都是 90°。

9.5 矫正图像数据的方向

介绍完几个方向的概念之后,我们就来说下如何校正相机的预览画面。我们会举几个例子,由简到繁逐步说明预览画面校正过程中要注意的事项。

首先我们要知道的是摄像头传感器方向只有 0°、90°、180°、270° 四个可选值,并且这些值是相对于局部坐标系 的 y 轴定义出来的,现在假设一个相机 APP 的画面在手机上是竖屏显示,也就是显示方向是 0° ,并且假设摄像头传感器的方向是 90°,如果我们没有校正画面的话,则显示的画面如下图所示(忽略画面变形):

很明显,上面显示的画面内容方向是错误的,里面的人物应该是垂直向上显示才对,所以我们应该吧摄像头采集到的画面顺时针旋转 90°,才能得到正确的显示结果,如下图所示:

上面的例子是建立在我们的显示方向是 0° 的时候,如果我们要求显示方向是 90°,也就是手机向左横放的时候画面才是正的,并且假设摄像头传感器的方向还是 90°,如果我们没有校正画面的话,则显示的画面如下图所示(忽略画面变形):

此时,我们知道传感器的方向是 90°,如果我们将传感器采集到的画面顺时针旋转 90° 显然是无法得到正确的画面,因为它是相对于局部坐标系 y 轴的角度,而不是实际显示方向,所以在做画面校正的时候我们还要把实际显示方向也考虑进去,这里实际显示方向是 90°,所以我们应该把传感器采集到的画面顺时针旋转 180°(摄像头传感器方向 + 实际显示方向) 才能得到正确的画面,显示的画面如下图所示(忽略画面变形):

总结一下,在校正画面方向的时候要同时考虑两个因素,即摄像头传感器方向和显示方向。接下来我们要回到我们的相机应用里,看看通过代码是如何实现预览画面方向校正的。

如果你有自己看过 Camera 的官方 API 文档,你会发现官方已经给我们写好了一个同时考虑显示方向和摄像头传感器方向的方法,我把它翻译成 Kotlin 语法:

private fun getDisplayRotation(cameraCharacteristics: CameraCharacteristics): Int {
    val rotation = windowManager.defaultDisplay.rotation
    val degrees = when (rotation) {
        Surface.ROTATION_0 -> 0
        Surface.ROTATION_90 -> 90
        Surface.ROTATION_180 -> 180
        Surface.ROTATION_270 -> 270
        else -> 0
    }
    val sensorOrientation = cameraCharacteristics[CameraCharacteristics.SENSOR_ORIENTATION]!!
    return if (cameraCharacteristics[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_FRONT) {
        (360 - (sensorOrientation + degrees) % 360) % 360
    } else {
        (sensorOrientation - degrees + 360) % 360
    }
}

如果你已经完全理解前面介绍的那些角度的概念,那你应该很容易就能理解上面这段代码,实际上就是通过 WindowManager 获取当前的显示方向,然后再参照摄像头传感器方向以及是否是前后置,最后计算出我们实际要旋转的角度。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Google Android SDK开发范例大全(完整版)共4个分卷 目录 第1章 了解.深入.动手做. 1.1 红透半边天的Android 1.2 本书目的及涵盖范例范围 1.3 如何阅读本书 1.4 使用本书范例 1.5 参考网站 第2章 Android初体验 2.1 安装AndroidSDK与ADTplug-in 2.2 建立第一个Android项目(HelloAndroid!) 2.3 Android应用程序架构——从此开始 2.4 可视化的界面开发工具 2.5 部署应用程序到Android手机 第3章 用户人机界面 3.1 更改与显示文字标签——TextView标签的使用 3.2 更改手机窗口画面底色——drawable定义颜色常数的方法 3.3 更改TextView文字颜色——引用Drawable颜色常数及背景色 3.4 置换TextView文字——CharSequence数据类型与ResourceID应用 3.5 取得手机屏幕大小——DisplayMetrics取得画面宽高的方法 3.6 样式化的定型对象——Style样式的定义 3.7 简易的按钮事件——Button事件处理 3.8 手机页面的转换——setContentView的应用 3.9 调用另一个Activity——Intent对象的使用 3.10 不同Activity之间的数据传递——Bundle对象的实现 3.11 返回数据到前一个Activity——startActivityForResult方法 3.12 具有交互功能的对话框——AlertDialog窗口 3.13 置换文字颜色的机关——Button与TextView的交互 3.14 控制不同的文字字体——Typeface对象使用 3.15 如iPhone拖动相片特效——Gallery画廊 3.16 自制计算器——多按钮的整合应用 3.17 关于(About)程序信息——Menu功能菜单程序设计 3.18 程序加载中,请稍后——ProgressDialog与线程整合应用 3.19 全屏幕以按钮覆盖——动态产生按钮并最大化 3.20 今晚到哪儿打牙祭?——具选择功能的对话框 3.21 Android变脸——主题(Theme)实现 第4章 史上超豪华的手机控件 4.1 EditText与TextView共舞——setOnKeyListener事件 4.2 设计具有背景图的按钮——ImageButton的焦点及事件处理 4.3 给耶诞老人的信息——Toast对象的使用 4.4 我同意条款——CheckBox的isChecked属性 4.5 消费券采购列表——多选项CheckBox的应用 4.6 向左或向右——RadioGroup组与onCheckedChanged事件 4.7 专业相框设计——ImageView的堆栈应用 4.8 自定义下拉菜单模式——Spinner与setDropDownViewResource 4.9 动态添加/删除的Spinner菜单——ArrayList与Widget的依赖性 4.10 心爱小宝贝相片集——Gallery与衍生BaseAdapter容器 4.11 快速的搜索手机文件引擎——JavaI/O的应用 4.12 按钮也能随点击变换——ImageButton选择特效 4.13 具自动提示功能的菜单——AutoCompleteTextView与数组 4.14 数字及模拟小时钟设计——AnalogClock与DigitalClock的原理 4.15 动态输入日期与时间——DatePicker与TimePicker应用 4.16 猜猜红心A在那儿——ImageView点击事件与透明度处理 4.17 后台程序运行进度提示——ProgressBar与Handler的整合应用 4.18 动态文字排版——GridView与ArrayAdapter设计 4.19 在Activity里显示列表列表——ListView的布局 4.20 以动态列表配置选项——ListActivity与Menu整合技巧 4.21 查找程序根目录下所有文件——JavaI/O与ListActivity的结合.. 4.22 加载手机磁盘里的图文件——使用decodeFile方法 4.23 动态放大缩小ImageView里的图片——运用Matrix对象来缩放图文件 4.24 动态旋转图片——Bitmap与Matrix旋转ImageView 4.25 猜猜我在想什么——RadioButtonID 4.26 离开与关闭程序的弹出窗口——对话窗口上的ICON图标 第5章 交互式通信服务与手机控制 5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent
Google Android SDK开发范例大全(完整版)共4个分卷 目录 第1章 了解.深入.动手做. 1.1 红透半边天的Android 1.2 本书目的及涵盖范例范围 1.3 如何阅读本书 1.4 使用本书范例 1.5 参考网站 第2章 Android初体验 2.1 安装AndroidSDK与ADTplug-in 2.2 建立第一个Android项目(HelloAndroid!) 2.3 Android应用程序架构——从此开始 2.4 可视化的界面开发工具 2.5 部署应用程序到Android手机 第3章 用户人机界面 3.1 更改与显示文字标签——TextView标签的使用 3.2 更改手机窗口画面底色——drawable定义颜色常数的方法 3.3 更改TextView文字颜色——引用Drawable颜色常数及背景色 3.4 置换TextView文字——CharSequence数据类型与ResourceID应用 3.5 取得手机屏幕大小——DisplayMetrics取得画面宽高的方法 3.6 样式化的定型对象——Style样式的定义 3.7 简易的按钮事件——Button事件处理 3.8 手机页面的转换——setContentView的应用 3.9 调用另一个Activity——Intent对象的使用 3.10 不同Activity之间的数据传递——Bundle对象的实现 3.11 返回数据到前一个Activity——startActivityForResult方法 3.12 具有交互功能的对话框——AlertDialog窗口 3.13 置换文字颜色的机关——Button与TextView的交互 3.14 控制不同的文字字体——Typeface对象使用 3.15 如iPhone拖动相片特效——Gallery画廊 3.16 自制计算器——多按钮的整合应用 3.17 关于(About)程序信息——Menu功能菜单程序设计 3.18 程序加载中,请稍后——ProgressDialog与线程整合应用 3.19 全屏幕以按钮覆盖——动态产生按钮并最大化 3.20 今晚到哪儿打牙祭?——具选择功能的对话框 3.21 Android变脸——主题(Theme)实现 第4章 史上超豪华的手机控件 4.1 EditText与TextView共舞——setOnKeyListener事件 4.2 设计具有背景图的按钮——ImageButton的焦点及事件处理 4.3 给耶诞老人的信息——Toast对象的使用 4.4 我同意条款——CheckBox的isChecked属性 4.5 消费券采购列表——多选项CheckBox的应用 4.6 向左或向右——RadioGroup组与onCheckedChanged事件 4.7 专业相框设计——ImageView的堆栈应用 4.8 自定义下拉菜单模式——Spinner与setDropDownViewResource 4.9 动态添加/删除的Spinner菜单——ArrayList与Widget的依赖性 4.10 心爱小宝贝相片集——Gallery与衍生BaseAdapter容器 4.11 快速的搜索手机文件引擎——JavaI/O的应用 4.12 按钮也能随点击变换——ImageButton选择特效 4.13 具自动提示功能的菜单——AutoCompleteTextView与数组 4.14 数字及模拟小时钟设计——AnalogClock与DigitalClock的原理 4.15 动态输入日期与时间——DatePicker与TimePicker应用 4.16 猜猜红心A在那儿——ImageView点击事件与透明度处理 4.17 后台程序运行进度提示——ProgressBar与Handler的整合应用 4.18 动态文字排版——GridView与ArrayAdapter设计 4.19 在Activity里显示列表列表——ListView的布局 4.20 以动态列表配置选项——ListActivity与Menu整合技巧 4.21 查找程序根目录下所有文件——JavaI/O与ListActivity的结合.. 4.22 加载手机磁盘里的图文件——使用decodeFile方法 4.23 动态放大缩小ImageView里的图片——运用Matrix对象来缩放图文件 4.24 动态旋转图片——Bitmap与Matrix旋转ImageView 4.25 猜猜我在想什么——RadioButtonID 4.26 离开与关闭程序的弹出窗口——对话窗口上的ICON图标 第5章 交互式通信服务与手机控制 5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 5.4 自制发送Email程序——Intent在Email上的使用 5.5 自制日历手机数据库——实现SQLiteOpenHelper 5.6 手机震动的节奏——Vibrator对象及周期运用 5.7 图文可视化提醒——Toast与LinearLayoutView 5.8 状态栏的图标与文字提醒——NotificationManager与Notification对象的应用 5.9 搜索手机通讯录自动完成——使用ContentResolver 5.10 取得联系人资料——Provider.Contact的使用 5.11 制作有图标的文件资源管理器——自定义Adapter对象 5.12 还原手机默认桌面——重写clearWallpaper方法 5.13 置换手机背景图——Gallery与setWallpaper整合实现 5.14 撷取手机现存桌面——getWallpaper与setImageDrawable 5.15 文件资源管理器再进化——JavaI/O修改文件名及删除 5.16 取得目前File与Cache的路径——getCacheDir与getFilesDir 5.17 打开/关闭WiFi服务——WifiManager状态判断 5.18 取得SIM卡内的信息——TelephonyManager的应用 5.19 调用拨号按钮——打电话CALL_BUTTON 5.20 DPAD按键处理——onKeyDown事件与Layout坐标交互 5.21 任务管理器正在运行的程序——RunningTaskInfo 5.22 动态更改屏幕方向——LANDSCAPE与PORTRAIT 5.23 系统设置更改事件——onConfigurationChanged信息处理 5.24 取得电信网络与手机相关信息——TelephonyManager与android.provider.Settings.System的应用 第6章 手机自动服务纪实 6.1 您有一条短信popup提醒——常驻BroadcastReceiver的应用 6.2 手机电池计量还剩多少——使用BroadcastReceiver捕捉Intent.ACTION_BATTERY_CHANGED 6.3 群发拜年短信给联系人——ACTION_PICK与Uri对象 6.4 开始与停止系统服务——Service与Runnable整合并用 6.5 通过短信发送email通知——BroadcastReceiver与Intent整合 6.6 手机拨接状态——PhoneStateListener之onCallStateChanged 6.7 有来电,发送邮件通知——PhoneStateListener与ACTION_SEND 6.8 存储卡剩余多少容量——Environment加StatFs 6.9 访问本机内存与存储卡——File的创建与删除 6.10 实现可定时响起的闹钟——PendingIntent与AlarmManager的运用 6.11 黑名单来电自动静音——PhoneStateListener与AudioManager 6.12 手机翻背面即静音震动——SensorListener及AudioManager整合应用 6.13 指定时间置换桌面背景——多AlarmManager事件处理 6.14 判断发送短信后的状态——BroadcastReceiver聆听PendingIntent 6.15 后台服务送出广播信息——sendBroadcast与BroadcastReceiver 6.16 开机程序设计——receiver与intent-filter协同作业 6.17 双向短信常驻服务——Service与receiver实例 第7章 娱乐多媒体 7.1 访问Drawable资源的宽高——ContextMenu与Bitmap的应用 7.2 绘制几何图形——使用android.graphics类 7.3 手机屏幕保护程序——FadeIn/FadeOut特效与运行线程 7.4 用手指移动画面里的照片——onTouchEvent事件判断 7.5 加载存储卡的Gallery相簿——FileArrayList 7.6 取得手机内置媒体里的图文件——ACTION_GET_CONTENT取回InputStream 7.7 相片导航向导与设置背景桌面——ImageSwitcher与Gallery 7.8 调整音量大小声——AudioManager控制音量 7.9 播放mp3资源文件——raw文件夹与MediaPlayer的使用 7.10 播放存储卡里的mp3音乐——MediaPlayer.setDataSource 7.11 自制录音/播放录音程序——MediaRecorder与AudioEncoder 7.12 通过收到短信开始秘密录音——MediaRecorder与BroadcastReceiver实例 7.13 内置影片播放器载入3gp电影——VideoViewWidget 7.14 自制3gp影片播放器——MediaPlayer与实现SurfaceView 7.15 相机预览及拍照临时文件——Camera及PictureCallback事件 第8章 当Android与Internet接轨 8.1 HTTPGET/POST传递参数——HTTP连接示范 8.2 在程序里浏览网页——WebView.loadUrl 8.3 嵌入HTML标记的程序——WebView.loadData 8.4 设计前往打开网页功能——Intent与Uri.parse 8.5 将网络图像网址放入Gallery中显示——URL.URLConnection.BaseAdapter 8.6 即时访问网络图文件展示——HttpURLConnection 8.7 手机气象局,实时卫星云图——HttpURLConnection与URLConnection和运行线程 8.8 通过网络播放MP3——Runnable存储FileOutputStream技巧 8.9 设置远程下载音乐为手机铃声——RingtoneManager与铃声存放路径 8.10 远程下载桌面背景图案——URLConnection与setWallpaper()搭配 8.11 将手机文件上传至网站服务器——模拟HTTPFORM的POSTACTION 8.12 移动博客发布器——以XML-RPC达成远程过程调用 8.13 移动RSS阅读器——利用SAXParser解析XML 8.14 远程下载安装Android程序——APKInstaller的应用 8.15 手机下载看3gp影片——Runnable混搭SurfaceView 8.16 访问网站LoginAPI——远程服务器验证程序运行权限 8.17 地震速报!——HttpURLConnection与Service侦测服务 第9章 Google服务与Android混搭 9.1 Google帐号验证Token——AuthSub 9.2 Google搜索——AutoCompleteTextView与GoogleSearchAPI 9.3 前端产生QRCode二维条形码——GoogleChartAPI 9.4 以经纬度查找目的地位置——GeoPoint与MapView的搭配运用 9.5 GPSGoogle地图——LocationListener与MapView实时更新 9.6 移动版GoogleMap——Geocoder反查Address对象 9.7 规划导航路径——DirectionsRoute 9.8 移动设备上的Picasa相册——GooglePicasaAPI 9.9 随身翻译机——GoogleTranslateAPI 第10章 创意Android程序设计 10.1 手机手电筒——PowerManager控制WakeLock并改变手机亮度 10.2 GPS轨迹记录器——利用LocationListener在地图上画图并换算距离 10.3 女性贴身看护——AlarmManager.DatePicker.TimePicker 10.4 手机QRCode二维条形码生成器——Canvas与SurfaceHolder绘图 10.5 AndroidQRCode二维条形码扫描仪——BitmapFactory.decodeByteArray 10.6 上班族今天中午要吃什么——热量骰子地图 10.7 掷杯筊——把手机放在空中甩事件处理...
Google Android SDK开发范例大全(完整版)共4个分卷 目录 第1章 了解.深入.动手做. 1.1 红透半边天的Android 1.2 本书目的及涵盖范例范围 1.3 如何阅读本书 1.4 使用本书范例 1.5 参考网站 第2章 Android初体验 2.1 安装AndroidSDK与ADTplug-in 2.2 建立第一个Android项目(HelloAndroid!) 2.3 Android应用程序架构——从此开始 2.4 可视化的界面开发工具 2.5 部署应用程序到Android手机 第3章 用户人机界面 3.1 更改与显示文字标签——TextView标签的使用 3.2 更改手机窗口画面底色——drawable定义颜色常数的方法 3.3 更改TextView文字颜色——引用Drawable颜色常数及背景色 3.4 置换TextView文字——CharSequence数据类型与ResourceID应用 3.5 取得手机屏幕大小——DisplayMetrics取得画面宽高的方法 3.6 样式化的定型对象——Style样式的定义 3.7 简易的按钮事件——Button事件处理 3.8 手机页面的转换——setContentView的应用 3.9 调用另一个Activity——Intent对象的使用 3.10 不同Activity之间的数据传递——Bundle对象的实现 3.11 返回数据到前一个Activity——startActivityForResult方法 3.12 具有交互功能的对话框——AlertDialog窗口 3.13 置换文字颜色的机关——Button与TextView的交互 3.14 控制不同的文字字体——Typeface对象使用 3.15 如iPhone拖动相片特效——Gallery画廊 3.16 自制计算器——多按钮的整合应用 3.17 关于(About)程序信息——Menu功能菜单程序设计 3.18 程序加载中,请稍后——ProgressDialog与线程整合应用 3.19 全屏幕以按钮覆盖——动态产生按钮并最大化 3.20 今晚到哪儿打牙祭?——具选择功能的对话框 3.21 Android变脸——主题(Theme)实现 第4章 史上超豪华的手机控件 4.1 EditText与TextView共舞——setOnKeyListener事件 4.2 设计具有背景图的按钮——ImageButton的焦点及事件处理 4.3 给耶诞老人的信息——Toast对象的使用 4.4 我同意条款——CheckBox的isChecked属性 4.5 消费券采购列表——多选项CheckBox的应用 4.6 向左或向右——RadioGroup组与onCheckedChanged事件 4.7 专业相框设计——ImageView的堆栈应用 4.8 自定义下拉菜单模式——Spinner与setDropDownViewResource 4.9 动态添加/删除的Spinner菜单——ArrayList与Widget的依赖性 4.10 心爱小宝贝相片集——Gallery与衍生BaseAdapter容器 4.11 快速的搜索手机文件引擎——JavaI/O的应用 4.12 按钮也能随点击变换——ImageButton选择特效 4.13 具自动提示功能的菜单——AutoCompleteTextView与数组 4.14 数字及模拟小时钟设计——AnalogClock与DigitalClock的原理 4.15 动态输入日期与时间——DatePicker与TimePicker应用 4.16 猜猜红心A在那儿——ImageView点击事件与透明度处理 4.17 后台程序运行进度提示——ProgressBar与Handler的整合应用 4.18 动态文字排版——GridView与ArrayAdapter设计 4.19 在Activity里显示列表列表——ListView的布局 4.20 以动态列表配置选项——ListActivity与Menu整合技巧 4.21 查找程序根目录下所有文件——JavaI/O与ListActivity的结合.. 4.22 加载手机磁盘里的图文件——使用decodeFile方法 4.23 动态放大缩小ImageView里的图片——运用Matrix对象来缩放图文件 4.24 动态旋转图片——Bitmap与Matrix旋转ImageView 4.25 猜猜我在想什么——RadioButtonID 4.26 离开与关闭程序的弹出窗口——对话窗口上的ICON图标 第5章 交互式通信服务与手机控制 5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 5.4 自制发送Email程序——Intent在Email上的使用 5.5 自制日历手机数据库——实现SQLiteOpenHelper 5.6 手机震动的节奏——Vibrator对象及周期运用 5.7 图文可视化提醒——Toast与LinearLayoutView 5.8 状态栏的图标与文字提醒——NotificationManager与Notification对象的应用 5.9 搜索手机通讯录自动完成——使用ContentResolver 5.10 取得联系人资料——Provider.Contact的使用 5.11 制作有图标的文件资源管理器——自定义Adapter对象 5.12 还原手机默认桌面——重写clearWallpaper方法 5.13 置换手机背景图——Gallery与setWallpaper整合实现 5.14 撷取手机现存桌面——getWallpaper与setImageDrawable 5.15 文件资源管理器再进化——JavaI/O修改文件名及删除 5.16 取得目前File与Cache的路径——getCacheDir与getFilesDir 5.17 打开/关闭WiFi服务——WifiManager状态判断 5.18 取得SIM卡内的信息——TelephonyManager的应用 5.19 调用拨号按钮——打电话CALL_BUTTON 5.20 DPAD按键处理——onKeyDown事件与Layout坐标交互 5.21 任务管理器正在运行的程序——RunningTaskInfo 5.22 动态更改屏幕方向——LANDSCAPE与PORTRAIT 5.23 系统设置更改事件——onConfigurationChanged信息处理 5.24 取得电信网络与手机相关信息——TelephonyManager与android.provider.Settings.System的应用 第6章 手机自动服务纪实 6.1 您有一条短信popup提醒——常驻BroadcastReceiver的应用 6.2 手机电池计量还剩多少——使用BroadcastReceiver捕捉Intent.ACTION_BATTERY_CHANGED 6.3 群发拜年短信给联系人——ACTION_PICK与Uri对象 6.4 开始与停止系统服务——Service与Runnable整合并用 6.5 通过短信发送email通知——BroadcastReceiver与Intent整合 6.6 手机拨接状态——PhoneStateListener之onCallStateChanged 6.7 有来电,发送邮件通知——PhoneStateListener与ACTION_SEND 6.8 存储卡剩余多少容量——Environment加StatFs 6.9 访问本机内存与存储卡——File的创建与删除 6.10 实现可定时响起的闹钟——PendingIntent与AlarmManager的运用 6.11 黑名单来电自动静音——PhoneStateListener与AudioManager 6.12 手机翻背面即静音震动——SensorListener及AudioManager整合应用 6.13 指定时间置换桌面背景——多AlarmManager事件处理 6.14 判断发送短信后的状态——BroadcastReceiver聆听PendingIntent 6.15 后台服务送出广播信息——sendBroadcast与BroadcastReceiver 6.16 开机程序设计——receiver与intent-filter协同作业 6.17 双向短信常驻服务——Service与receiver实例 第7章 娱乐多媒体 7.1 访问Drawable资源的宽高——ContextMenu与Bitmap的应用 7.2 绘制几何图形——使用android.graphics类 7.3 手机屏幕保护程序——FadeIn/FadeOut特效与运行线程 7.4 用手指移动画面里的照片——onTouchEvent事件判断 7.5 加载存储卡的Gallery相簿——FileArrayList 7.6 取得手机内置媒体里的图文件——ACTION_GET_CONTENT取回InputStream 7.7 相片导航向导与设置背景桌面——ImageSwitcher与Gallery 7.8 调整音量大小声——AudioManager控制音量 7.9 播放mp3资源文件——raw文件夹与MediaPlayer的使用 7.10 播放存储卡里的mp3音乐——MediaPlayer.setDataSource 7.11 自制录音/播放录音程序——MediaRecorder与AudioEncoder 7.12 通过收到短信开始秘密录音——MediaRecorder与BroadcastReceiver实例 7.13 内置影片播放器载入3gp电影——VideoViewWidget 7.14 自制3gp影片播放器——MediaPlayer与实现SurfaceView 7.15 相机预览及拍照临时文件——Camera及PictureCallback事件 第8章 当Android与Internet接轨 8.1 HTTPGET/POST传递参数——HTTP连接示范 8.2 在程序里浏览网页——WebView.loadUrl 8.3 嵌入HTML标记的程序——WebView.loadData 8.4 设计前往打开网页功能——Intent与Uri.parse 8.5 将网络图像网址放入Gallery中显示——URL.URLConnection.BaseAdapter 8.6 即时访问网络图文件展示——HttpURLConnection 8.7 手机气象局,实时卫星云图——HttpURLConnection与URLConnection和运行线程 8.8 通过网络播放MP3——Runnable存储FileOutputStream技巧 8.9 设置远程下载音乐为手机铃声——RingtoneManager与铃声存放路径 8.10 远程下载桌面背景图案——URLConnection与setWallpaper()搭配 8.11 将手机文件上传至网站服务器——模拟HTTPFORM的POSTACTION 8.12 移动博客发布器——以XML-RPC达成远程过程调用 8.13 移动RSS阅读器——利用SAXParser解析XML 8.14 远程下载安装Android程序——APKInstaller的应用 8.15 手机下载看3gp影片——Runnable混搭SurfaceView 8.16 访问网站LoginAPI——远程服务器验证程序运行权限 8.17 地震速报!——HttpURLConnection与Service侦测服务 第9章 Google服务与Android混搭 9.1 Google帐号验证Token——AuthSub 9.2 Google搜索——AutoCompleteTextView与GoogleSearchAPI 9.3 前端产生QRCode二维条形码——GoogleChartAPI 9.4 以经纬度查找目的地位置——GeoPoint与MapView的搭配运用 9.5 GPSGoogle地图——LocationListener与MapView实时更新 9.6 移动版GoogleMap——Geocoder反查Address对象 9.7 规划导航路径——DirectionsRoute 9.8 移动设备上的Picasa相册——GooglePicasaAPI 9.9 随身翻译机——GoogleTranslateAPI 第10章 创意Android程序设计 10.1 手机手电筒——PowerManager控制WakeLock并改变手机亮度 10.2 GPS轨迹记录器——利用LocationListener在地图上画图并换算距离 10.3 女性贴身看护——AlarmManager.DatePicker.TimePicker 10.4 手机QRCode二维条形码生成器——Canvas与SurfaceHolder绘图 10.5 AndroidQRCode二维条形码扫描仪——BitmapFactory.decodeByteArray 10.6 上班族今天中午要吃什么——热量骰子地图 10.7 掷杯筊——把手机放在空中甩事件处理...
Android Camera开发入门:目录 第一篇: 前景  一、Android Camera开发前景;      1)camera相关应用的领域      2)相关岗位介绍;      3)市场招聘介绍;      4)发展前景介绍;  二、学习这门课的重要性;      1)适合的人群;      2)熟悉和了解Android Camera 应用开发流程的重要性 第二篇: 开发环境安装  一、jdk、sdk的配置;  二、android studio的安装介绍;  三、adb命令的使用; 第三篇: Camera 常用api和最新框架介绍  一、android camera api介绍      1)camera1、camera2 区别;      2)camera 1、camera2 常用api介绍;      3)android camerax;  二、android camera最新框架介绍 第四篇:Camera api1实现预览、拍照、录像功能  一、预览  二、拍照  三、录像  四、获取实时预览流 第五篇: Camera2相机 打开功能实现第六篇: Camera2相机 预览功能实现  1)surfaceview、textureview 第七篇: Camera2相机 拍照功能实现 1)单拍; 第八篇:Camera2相机 录像功能实现1)正常录像 第九篇:Camera2预览方向、拍照方向设置     1) 预览变形问题如何处理? 第十篇:YUV流处理  1)如何获取实时预览流?  2)  思考:双码流方案如何实现?一边本地录像,一边后台推流 第十一篇:dumpsys media.camera 第十二篇:Camera2 Zoom变焦第十三篇:人脸识别(android 原生 & 三方人脸识别算法)第十四篇:Uvc UsbCamera第十五篇:Android Camera2拍摄RAW图第十六篇: Android Camera2同时打开前后摄 并 录像第十七篇: Android Camera2 视频慢动作  附:1)提供android开发相关资源      软件工具、Android相关学习书籍、学习相关网站博客等链接2)提供课程讲解中设计到的App 源码    * Camera API1使用源码    * Camera API2使用源码    * 调用三方算法人脸识别源码    *  录像慢动作源码    * Uvc UsbCamera相关源码3)课件
Android Camera2 相机的手动对焦(Manual Focus),需要进行以下步骤: 1. 获取 CameraDevice 对象,并打开相机。 2. 创建 CameraCaptureSession 对象,并设置预览 Surface。 3. 创建 CaptureRequest.Builder 对象,并设置相机参数(如曝光、ISO等)。 4. 设置手动对焦模式: - 获取相机支持的手动对焦模式列表。 - 选择最合适的手动对焦模式。 - 设置手动对焦模式到 CaptureRequest.Builder 对象中。 5. 监听相机状态变化,并在相机状态变为 CameraDevice.STATE_ACTIVE 时进行手动对焦操作。 6. 创建 CaptureRequest 对象,并将其提交给 CameraCaptureSession 进行拍照或预览。 以下是示例代码: ```java private void setupCamera() { CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); String cameraId = null; try { // 获取相机列表中的第一个相机 cameraId = cameraManager.getCameraIdList()[0]; } catch (CameraAccessException e) { e.printStackTrace(); } try { // 打开相机 cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice camera) { mCameraDevice = camera; createCaptureSession(); } @Override public void onDisconnected(@NonNull CameraDevice camera) { camera.close(); mCameraDevice = null; } @Override public void onError(@NonNull CameraDevice camera, int error) { camera.close(); mCameraDevice = null; } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } } private void createCaptureSession() { try { // 创建预览 Surface SurfaceTexture texture = mTextureView.getSurfaceTexture(); texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); Surface surface = new Surface(texture); // 创建 CameraCaptureSession mCameraDevice.createCaptureSession(Collections.singletonList(surface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { mCaptureSession = session; try { // 创建 CaptureRequest.Builder mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewRequestBuilder.addTarget(surface); // 设置手动对焦模式 int[] afModes = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); if (afModes != null && afModes.length > 0) { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); if (Arrays.asList(afModes).contains(CaptureRequest.CONTROL_AF_MODE_MANUAL)) { mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_MANUAL); } } // 监听相机状态变化 mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { mCaptureSession = session; updatePreview(); } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { Toast.makeText(MainActivity.this, "配置相机失败", Toast.LENGTH_SHORT).show(); } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { Toast.makeText(MainActivity.this, "配置相机失败", Toast.LENGTH_SHORT).show(); } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } } private void updatePreview() { try { // 创建 CaptureRequest 对象 CaptureRequest previewRequest = mPreviewRequestBuilder.build(); mCaptureSession.setRepeatingRequest(previewRequest, null, null); } catch (CameraAccessException e) { e.printStackTrace(); } } // 手动对焦 private void manualFocus(float x, float y) { try { // 计算对焦区域 Rect rect = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); int areaSize = 200; int right = rect.right; int bottom = rect.bottom; int centerX = (int) (x / (float) mTextureView.getWidth() * right); int centerY = (int) (y / (float) mTextureView.getHeight() * bottom); int left = centerX - areaSize; int top = centerY - areaSize; right = centerX + areaSize; bottom = centerY + areaSize; left = Math.max(left, rect.left); top = Math.max(top, rect.top); right = Math.min(right, rect.right); bottom = Math.min(bottom, rect.bottom); Rect touchArea = new Rect(left, top, right, bottom); // 计算对焦区域的权重 MeteringRectangle[] areas = new MeteringRectangle[1]; areas[0] = new MeteringRectangle(touchArea, 1000); // 设置手动对焦区域和权重 mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, areas); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_REGIONS, areas); mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); // 更新 CaptureRequest mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null); } catch (CameraAccessException e) { e.printStackTrace(); } } ``` 在 Activity 中实现 onTouchEvent 方法,实现手动对焦: ```java @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { manualFocus(event.getX(), event.getY()); return true; } return super.onTouchEvent(event); } ``` 以上是一个简单的手动对焦实现,需要注意的是不同手机的相机支持的手动对焦模式可能不同,需要根据具体情况进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值