Camera API1 使用说明

Camera API2 使用说明

目录

一、开启相机

1.1创建项目

1.2注册权限

1.3配置相机特性要求

1.4 获取摄像头的个数

1.5 根据 ID 获取 CameraInfo

facing

1.6 开启相机

1.7 关闭相机

二、预览

2.1认识 Parameters

2.2 设置预览尺寸

2.3添加预览 Surface

2.4 开启和关闭预览

2.5 校正预览画面方向

自然方向

设备方向

局部坐标系

屏幕方向

摄像头传感器方向

画面方向校正

2.6 适配预览比例

2.7获取预览数据

2.8 切换前后置摄像头


一、开启相机

1.1创建项目

        开发一个具有完整相机功能的应用程序,所以第一步要做的就是创建一个相机项目,并且有一个 Activity 叫 MainActivity。为了降低源码的阅读难度,不打算引入任何的第三方库,不去关注性能问题,也不进行任何模式上的设计,大部分的代码我都会写在这个 MainActivity 里面,所有的功能的实现都尽可能简化。

1.2注册权限

       在使用相机 Camera API1 之前,必须在 AndroidManifest.xml 注册相机权限android.permission.CAMERA,声明我们开发的应用程序需要相机权限,另外如果你有保存照片的操作,那么读写 SD 卡的权限也是必须的:

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

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

</manifest>

         需要注意的是 6.0 以上的系统需要我们在程序运行的时候进行动态权限申请,所以我们需要在程序启动的时候去检查权限,有任何一个必要的权限被用户拒绝时,我们就弹窗提示用户程序因为权限被拒绝而无法正常工作:

public class MainActivity extends AppCompatActivity {

    private static final int REQUEST_PERMISSIONS_CODE = 1;
    private static final String[] REQUIRED_PERMISSIONS = {"android.permission.CAMERA","android.permission.WRITE_EXTERNAL_STORAGE"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onResume() {
        super.onResume();
        // 动态权限检查
        if (!isRequiredPermissionsGranted() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            requestPermissions(REQUIRED_PERMISSIONS, REQUEST_PERMISSIONS_CODE);
        }
    }

    /**
     * 判断我们需要的权限是否被授予,只要有一个没有授权,我们都会返回 false。
     *
     * @return true 权限都被授权
     */
    private boolean isRequiredPermissionsGranted() {
        for (String permission : REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_DENIED) {
                return false;
            }
        }
        return true;
    }
}

1.3配置相机特性要求

         所以接下来要做的就是在 AndroidManifest.xml 中配置一些程序运行时必要的相机特性,如果这些特性不支持,那么用户在安装 apk 的时候就会因为条件不符合而无法安装。

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

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <uses-feature
        android:name="android.hardware.camera"
        android:required="true" />

</manifest>

         我们通过 <uses-feature> 标签声明了我们的应用程序必须在具有摄像头的手机上才能运行。另外你还可以配置更多的特性要求,例如必须支持自动对焦的摄像头才能运行你的应用程序,更多的特性可以在 官方文档 上查询。

1.4 获取摄像头的个数

        并不是所有的手机都支持前后置摄像头,甚至有手机一个摄像头都没有,所以我们首先要确定当前的设备支持多少个摄像头。我们可以通过 Camera.getNumberOfCameras() 获取设备支持的摄像头个数,它是一个静态方法,返回一个 int 值代表摄像头个数。

1.5 根据 ID 获取 CameraInfo

  Camera.CameraInfo 正如其名,里面存储了几个相机相关的信息,信息并不多:

  • facing

      摄像头的方向,可选值有 Camera.CameraInfo.CAMERA_FACING_BACK 和Camera.CameraInfo.CAMERA_FACING_FRONT。

  • orientation

        摄像头的画面经过顺时针旋转多少度之后是正常画面,这个属性比较难以理解,我们在后面会专门解释。

  • canDisableShutterSound

        是否支持静音拍照,也就是说在用户拍照的时候是否会“咔嚓”一声,例如在日本由于隐私政策,所有手机都是不允许静音拍照的,那么该字段就会返回 false。该字段一般配合 Camera.enableShutterSound(boolean) 使用,当它返回 false 的时候,即使你调用 Camera.enableShutterSound(false),相机在拍照的时候也会发出声音。

Camera.CameraInfo 其实就这么几个字段,最重要的就是 facing 和 orientation 了,我们可以通过 facing 判断摄像头是前置还是后置,通过 orientation 值取校正摄像头的画面。

看下如何获取 Camera.CameraInfo 实例:

@Nullable
private Camera.CameraInfo mFrontCameraInfo = null;
private int mFrontCameraId = -1;

@Nullable
private Camera.CameraInfo mBackCameraInfo = null;
private int mBackCameraId = -1;

/**
 * 初始化摄像头信息。
 */
private void initCameraInfo() {
    int numberOfCameras = Camera.getNumberOfCameras();// 获取摄像头个数
    for (int cameraId = 0; cameraId < numberOfCameras; cameraId++) {
        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
        Camera.getCameraInfo(cameraId, cameraInfo);
        if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
            // 后置摄像头信息
            mBackCameraId = cameraId;
            mBackCameraInfo = cameraInfo;
        } else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT){
            // 前置摄像头信息
            mFrontCameraId = cameraId;
            mFrontCameraInfo = cameraInfo;
        }
    }
}

         从上面的代码可以看出 Camera.CameraInfo 的获取非常简单,只需要你创建一个 Camera.CameraInfo 实例,然后通过 Camera.getCameraInfo(int, Camera.CameraInfo) 方法就可以将相机信息填充到你创建的实例中。另外值得注意的一点是,相机的 ID 实际上就是从 0 到 numberOfCameras 递增,大部分手机的后置摄像头的 ID 是 0,前置摄像头的 ID 是 1,但是我们最好还是通过 facing 字段去判断比较靠谱。

Camera.getNumberOfCameras() 和 Camera.getCameraInfo(int, Camera.CameraInfo) 都不需要相机权限。

1.6 开启相机

        接下来我们要做的就是调用 Camera.open(int) 方法开启相机了,需要注意的是你必须确保在开启相机之前已经被授予了相机权限,否则会抛权限异常。一个比较稳妥的做法就是每次开启相机之前检查相机权限。下面是主要代码前段:

/**
 * 开启指定摄像头
 */
private void openCamera() {
    if (mCamera != null) {
        throw new RuntimeException("相机已经被开启,无法同时开启多个相机实例!");
    }

    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
        if (hasFrontCamera()) {
            // 优先开启前置摄像头
            mCamera = Camera.open(mFrontCameraId);
        } else if (hasBackCamera()) {
            // 没有前置,就尝试开启后置摄像头
            mCamera = Camera.open(mBackCameraId);
        } else {
            throw new RuntimeException("没有任何相机可以开启!");
        }
    }
}

1.7 关闭相机

       和其他硬件资源的使用一样,当我们不再需要使用相机时记得调用 Camera.release() 方法及时关闭相机回收资源。关闭相机的操作至关重要,因为如果你一直占用相机资源,其他基于相机开发的功能都会无法正常使用,严重情况下直接导致其他相机相关的 APP 无法正常使用。那么在什么时候关闭相机最合适呢?我个人的建议是在 onPause() 的时候就一定要关闭相机,因为在这个时候相机页面已经不是用户关注的焦点,大部分情况下已经可以关闭相机了

@Override
protected void onPause() {
    super.onPause();
    closeCamera();
}

/**
 * 关闭相机。
 */
private void closeCamera() {
    if (mCamera != null) {
        mCamera.release();
        mCamera = null;
    }
}

二、预览

2.1认识 Parameters

        相机功能的强大与否完全取决于各手机厂商的底层实现,在基于相机开发任何功能之前,你都需要通过某些手段判断当前设备相机的能力是否足以支撑你要开发的功能,而 Camera.Parameters 就是我们判断相机能力大小的手段,在 Camera.Parameters 里提供了大量形如 getSupportedXXX 的方法,通过这些方法你就可以判断相机某方面的功能是否达到你的要求,例如通过 getSupportedPreviewSizes() 可以获取相机支持的预览尺寸列表,进而从这个列表中查询是否有满足你需求的尺寸。

         除了通过 Camera.Parameters 判断相机功能的支持情况之外,我们还通过 Camera.Parameters 设置绝大部分相机参数,并且通过 Camera.setParameters() 方法将设置好的参数传给底层,让这些参数生效。

所以相机参数的配置流程基本就是以下三个步骤:

  • 通过 Camera.getParameters() 获取 Camera.Parameters 实例。

  • 通过 Camera.Parameters.getSupportedXXX 获取某个参数的支持情况。

  • 通过 Camera.Parameters.set() 方法设置参数。

  • 通过 Camera.setParameters() 方法将参数应用到底层。

注意:

      Camera.getParameters() 是一个比较耗时的操作,实测 20ms 到 100ms不等,所以尽可能地一次性设置所有必要的参数,然后通过 Camera.setParameters() 一次性应用到底层。

2.2 设置预览尺寸

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

Camera.Parameters parameters = mCamera.getParameters();
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();

         如果我们把所有的预览尺寸都打印出来看时,会发现一个比较特别的情况就是预览尺寸的宽是长边,高是短边,例如 1920x1080,而不是 1080x1920,这一点大家需要特别注意

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

/**
 * 根据指定的尺寸要求设置预览尺寸,我们会同时考虑指定尺寸的比例和大小。
 *
 * @param shortSide 短边长度
 * @param longSide  长边长度
 */
@WorkerThread
private void setPreviewSize(int shortSide, int longSide) {
    if (mCamera != null && shortSide != 0 && longSide != 0) {
        float aspectRatio = (float) longSide / shortSide;
        Camera.Parameters parameters = mCamera.getParameters();
        List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
        for (Camera.Size previewSize : supportedPreviewSizes) {
            if ((float) previewSize.width / previewSize.height == aspectRatio && previewSize.height <= shortSide && previewSize.width <= longSide) {
                parameters.setPreviewSize(previewSize.width, previewSize.height);
                mCamera.setParameters(parameters);
                break;
            }
        }
    }
}

2.3添加预览 Surface

        相机输出的预览画面最终都是绘制到指定的 Surface 上,这个 Surface 可以来自 SurfaceHolder 或者 SurfaceTexture,至于什么是 Surface 这里就不过多解释,大家可以自行了解。所以在开启预览之前,我们还要告诉相机把画面输出到哪个 Surface 上,Camera 支持两种方式设置预览的 Surface: 

  • 通过 Camera.setPreviewDisplay() 方法设置 SurfaceHolder 给相机,通常是在你使用 SurfaceView 作为预览控件时会使用该方法。

  • 通过 Camera.setPreviewTexture() 方法设置 SurfaceTexture 给相机,通常是在你使用 SurfaceTexture 作为预览控件或者自己创建 SurfaceTexture 时使用该方法。

在我们的 Demo 里,使用的 SurfaceView,所以会通过 Camera.setPreviewDisplay() 方法设置预览的 Surface,代码片段如下所示:

/**
 * 设置预览 Surface。
 */
@WorkerThread
private void setPreviewSurface(SurfaceHolder previewSurface) {
    if (mCamera != null && previewSurface != null) {
        try {
            mCamera.setPreviewDisplay(previewSurface);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.4 开启和关闭预览

 接下来,我们就要正式开启相机预览了,相关的方法就下面两个:

  • Camera.startPreview():开启预览

  • Camera.stopPreview():关闭预览

在 Demo 的代码中,我们做了一些逻辑处理,代码如下:

/**
 * 开始预览。
 */
@WorkerThread
private void startPreview() {
    if (mCamera != null) {
        mCamera.startPreview();
        Log.d(TAG, "startPreview() called");
    }
}

/**
 * 停止预览。
 */
@WorkerThread
private void stopPreview() {
    if (mCamera != null) {
        mCamera.stopPreview();
        Log.d(TAG, "stopPreview() called");
    }
}

2.5 校正预览画面方向

       如果没有做任何画面方向的校正,我们看到的画面很可能是横向的,这是因为手机上的摄像头传感器方向不一定是垂直的。在做预览画面方向的校正之前我们先来了解五个概念,分别是自然方向、设备方向、局部坐标系、屏幕方向和摄像头传感器方向。

自然方向

       当我们谈论方向的时候,实际上都是相对于某一个 0° 方向的角度,这个 0° 方向被称作自然方向,例如人站立的时候就是自然方向。

设备方向

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

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

  • 当我们把手机垂直放置且屏幕朝向我们的时候,设备方向为 0°,即设备自然方向

  • 当我们把手机向右横放且屏幕朝向我们的时候,设备方向为 90°

  • 当我们把手机倒着放置且屏幕朝向我们的时候,设备方向为 180°

  • 当我们把手机向左横放且屏幕朝向我们的时候,设备方向为 270°

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

局部坐标系

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

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

  • y 轴是当手机处于自然方向时,和手机屏幕平行且指向上方的坐标轴。

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

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

屏幕方向

      屏幕方向指的是屏幕上显示画面与局部坐标系 y 轴的顺时针夹角,注意这里实际上指的是显示的画面,而不是物理硬件上的屏幕,只是我们习惯上称作屏幕方向而已。

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

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

总结: 设备方向和屏幕方向之间没有任何关系,设备方向是相对于其现实空间中自然方向的角度,而屏幕方向是相对局部坐标系的角度。

摄像头传感器方向

摄像头传感器方向指的是传感器采集到的画面方向经过顺时针旋转多少度之后才能和局部坐标系的 y 轴正方向一致也就是我们提到的 Camera.CameraInfo.orientation 属性。

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

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

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

画面方向校正

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

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

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

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

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

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

        如果你有自己看过 Camera 的官方 API 文档,你会发现官方已经给我们写好了一个同时考虑屏幕方向和摄像头传感器方向的方法:

private int getCameraDisplayOrientation(Camera.CameraInfo cameraInfo) {
    int rotation = getWindowManager().getDefaultDisplay().getRotation();
    int degrees = 0;
    switch (rotation) {
        case Surface.ROTATION_0:
            degrees = 0;
            break;
        case Surface.ROTATION_90:
            degrees = 90;
            break;
        case Surface.ROTATION_180:
            degrees = 180;
            break;
        case Surface.ROTATION_270:
            degrees = 270;
            break;
    }
    int result;
    if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        result = (cameraInfo.orientation + degrees) % 360;
        result = (360 - result) % 360;  // compensate the mirror
    } else {  // back-facing
        result = (cameraInfo.orientation - degrees + 360) % 360;
    }
    return result;
}

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

        计算出要矫正的角度之后,我们要通过 Camera.setDisplayOrientation() 方法设置画面的矫正方向,下面是 Demo 中开启相机之后,马上配置画面矫正方向的代码:

private void openCamera(int cameraId) {
    if (mCamera != null) {
        throw new RuntimeException("You must close previous camera before open a new one.");
    }
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
        mCamera = Camera.open(cameraId);
        mCameraId = cameraId;
        mCameraInfo = cameraId == mFrontCameraId ? mFrontCameraInfo : mBackCameraInfo;
        Log.d(TAG, "Camera[" + cameraId + "] has been opened.");
        assert mCamera != null;
        mCamera.setDisplayOrientation(getCameraDisplayOrientation(mCameraInfo));
    }
}

2.6 适配预览比例

        前面介绍矫正预览画面方向的时候,我们看到了画面变形的情况,这因为展示预览画面的 Surface 和预览尺寸的比例不一致导致的,所以接下来我们要学习的是如何适配不同的预览比例。实际上预览比例的适配有两种方式:

  • 根据预览比例修改 Surface 的比例,这个是我们实际业务中经常用的方式,比如用户选择了 4:3 的预览比例,这个时候我们会选取 4:3 的预览尺寸并且把 Surface 修改成 4:3 的比例,从而让画面不会变形。

  • 根据 Surface 的比例修改预览比例,这种情况适用于 Surface 的比例是固定的,然后根据 Surface 的比例去选取适合的预览尺寸。

        在 Demo 中,出于简化的目的,我们选择了第二种方式适配比例,因为这种方式实现起来比较简单,所以我们会写一个自定义的 SurfaceView,让它的比例固定是 4:3,它的宽度固定填满父布局,高度根据比例动态计算:

public class SurfaceView43 extends SurfaceView {

    public SurfaceView43(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = width / 3 * 4;
        setMeasuredDimension(width, height);
    }

}

        上面的 SurfaceView43 使我们自定义的 SurfaceView,它的比例固定为 4:3,所以在它的 surfaceChanged() 回调中拿到的宽高的比例固定是 4:3,我们根据这个宽高比去调用前面定义好的设置预览尺寸方法就可以设置正确比例的预览尺寸:


@WorkerThread
private void setPreviewSize(int shortSide, int longSide) {
    if (mCamera != null && shortSide != 0 && longSide != 0) {
        float aspectRatio = (float) longSide / shortSide;
        Camera.Parameters parameters = mCamera.getParameters();
        List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
        for (Camera.Size previewSize : supportedPreviewSizes) {
            if ((float) previewSize.width / previewSize.height == aspectRatio && previewSize.height <= shortSide && previewSize.width <= longSide) {
                parameters.setPreviewSize(previewSize.width, previewSize.height);
                mCamera.setParameters(parameters);
                Log.d(TAG, "setPreviewSize() called with: width = " + previewSize.width + "; height = " + previewSize.height);
            }
        }
    }
}

        经过上面的比例适配之后,相机的预览画面就应该固定是 4:3 的比例并且不会变形了。

2.7获取预览数据

        开启相机预览的时候我们可以通过回调方法获取相机的预览数据,并且可以配置预览数据的数据格式,拿到预览数据之后进而做一些算法处理什么的。首先我们要通过 Parameters.getSupportedPreviewFormats() 方法获取相机支持哪些预览数据格式,所以我们定义了下面的方法:

/**
 * 判断指定的预览格式是否支持。
 */
private boolean isPreviewFormatSupported(Camera.Parameters parameters, int format) {
    List<Integer> supportedPreviewFormats = parameters.getSupportedPreviewFormats();
    return supportedPreviewFormats != null && supportedPreviewFormats.contains(format);
}

        确定了你要的数据格式是支持的之后,就可以通过 Parameters.setPreviewFormat() 放配置预览数据的格式了,代码片段如下所示:

private static final int PREVIEW_FORMAT = ImageFormat.NV21;

if (isPreviewFormatSupported(parameters, PREVIEW_FORMAT)) {
    parameters.setPreviewFormat(PREVIEW_FORMAT);
}

        上面说到我们是通过回调的方式获取相机预览数据的,所以相机为我们提供了一个回调接口叫 Camera.PreviewCallback,我们只需实现该接口并且注册给相机就可以在预览的时候接收到数据了,注册回调接口的方式有两种:

  • setPreviewCallback():注册预览回调

  • setPreviewCallbackWithBuffer():注册预览回调,并且使用已经配置好的缓冲池

        使用 setPreviewCallback() 注册预览回调获取预览数据是最简单的,因为你不需要其他配置流程,直接注册即可,但是出于性能考虑,官方推荐我们使用 setPreviewCallbackWithBuffer(),因为它会使用我们配置好的缓冲对象回调预览数据,避免重复创建内存占用很大的对象。所以接下来我们重点介绍如何根据预览尺寸配置对象池并注册回调,整个步骤如下:

  • 根据需求确定预览尺寸

  • 根据需求确定预览数据格式

  • 根据预览尺寸和数据格式计算出每一帧画面要占用的内存大小

  • 通过 addCallbackBuffer() 方法提前添加若干个创建好的 byte 数组对象作为缓冲对象供回调预览数据使用

  • 通过 setPreviewCallbackWithBuffer() 注册预览回调

  • 使用完缓冲对象之后,通过 addCallbackBuffer() 方法回收缓冲对象

        根据上述步骤,我们修改原来设置预览尺寸的方法,在配置预览尺寸的同时根据预览尺寸和数据格式配置缓冲对象,代码如下:

@WorkerThread
private void setPreviewSize(int shortSide, int longSide) {
    Camera camera = mCamera;
    if (camera != null && shortSide != 0 && longSide != 0) {
        float aspectRatio = (float) longSide / shortSide;
        Camera.Parameters parameters = camera.getParameters();
        List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
        for (Camera.Size previewSize : supportedPreviewSizes) {
            if ((float) previewSize.width / previewSize.height == aspectRatio && previewSize.height <= shortSide && previewSize.width <= longSide) {
                parameters.setPreviewSize(previewSize.width, previewSize.height);
                Log.d(TAG, "setPreviewSize() called with: width = " + previewSize.width + "; height = " + previewSize.height);

                if (isPreviewFormatSupported(parameters, PREVIEW_FORMAT)) {
                    parameters.setPreviewFormat(PREVIEW_FORMAT);
                    int frameWidth = previewSize.width;
                    int frameHeight = previewSize.height;
                    int previewFormat = parameters.getPreviewFormat();
                    PixelFormat pixelFormat = new PixelFormat();
                    PixelFormat.getPixelFormatInfo(previewFormat, pixelFormat);
                    int bufferSize = (frameWidth * frameHeight * pixelFormat.bitsPerPixel) / 8;
                    camera.addCallbackBuffer(new byte[bufferSize]);
                    camera.addCallbackBuffer(new byte[bufferSize]);
                    camera.addCallbackBuffer(new byte[bufferSize]);
                    Log.d(TAG, "Add three callback buffers with size: " + bufferSize);
                }

                camera.setParameters(parameters);
                break;
            }
        }
    }
}

        上面代码中,我们使用 PixelFormat 工具类根据当前的预览尺寸和格式计算出每一个像素占用多少 Bit,进而算出一帧画面需要占用的内存大小,最后创建三个 Buffer 通过 addCallbackBuffer() 添加给相机供相机循环使用。

        当面我们开启预览的时候,相机就会通过 Camera.PreviewCallback 将每一帧画面的数据填充到 Buffer 里传递给我们,我们在使用完 Buffer 之后,必须通过 addCallbackBuffer() 将用完的 Buffer 重新设置回去一遍相机继续重复利用该缓冲,代码如下:

private class PreviewCallback implements Camera.PreviewCallback {
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        // 在使用完 Buffer 之后记得回收复用。
        camera.addCallbackBuffer(data);
    }
}

注意:在预览回调方法里使用完 Buffer 之后,记得一定要调用 addCallbackBuffer() 将 Buffer 重新添加到缓冲池里供相机使用。

2.8 切换前后置摄像头

        实际需求经常要求 APP 能够支持前后置摄像头的切换,所以这里我们也介绍下如何实现前后置摄像头的切换。大部分情况下我们在切换前后置摄像头的时候,都会直接复用同一个 Surface,所以我们会在 surfaceChanged() 的时候把 Surface 保存下来,如下所示:

private class PreviewSurfaceCallback implements SurfaceHolder.Callback {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        mPreviewSurface = holder;
        mPreviewSurfaceWidth = width;
        mPreviewSurfaceHeight = height;
        setupPreview(holder, width, height);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        mPreviewSurface = null;
        mPreviewSurfaceWidth = 0;
        mPreviewSurfaceHeight = 0;
    }
}

        然后就是添加一个切换前后置的按钮,当点击按钮的时候回去获取和当前摄像头 ID 相反方向的 ID,所以我们定义了一个 switchCameraId() 方法,如下所示:

/**
 * 切换前后置时切换ID
 */
private int switchCameraId() {
    if (mCameraId == mFrontCameraId && hasBackCamera()) {
        return mBackCameraId;
    } else if (mCameraId == mBackCameraId && hasFrontCamera()) {
        return mFrontCameraId;
    } else {
        throw new RuntimeException("No available camera id to switch.");
    }
}

        最后就是走一个标准的切换前后置摄像头流程了:

  1. 停止预览

  2. 关闭当前摄像头

  3. 开启新的摄像头

  4. 配置预览尺寸

  5. 配置预览 Surface

  6. 开启预览

        因为我们的 Demo 中使用 HandlerThread 控制了相机的操作流程,所以你可以看到如下代码,具体的实现请看 Demo:

private class OnSwitchCameraButtonClickListener implements View.OnClickListener {
    @Override
    public void onClick(View v) {
        Handler cameraHandler = mCameraHandler;
        SurfaceHolder previewSurface = mPreviewSurface;
        int previewSurfaceWidth = mPreviewSurfaceWidth;
        int previewSurfaceHeight = mPreviewSurfaceHeight;
        if (cameraHandler != null && previewSurface != null) {
            int cameraId = switchCameraId();// 切换摄像头 ID
            cameraHandler.sendEmptyMessage(MSG_STOP_PREVIEW);// 停止预览
            cameraHandler.sendEmptyMessage(MSG_CLOSE_CAMERA);// 关闭当前的摄像头
            cameraHandler.obtainMessage(MSG_OPEN_CAMERA, cameraId, 0).sendToTarget();// 开启新的摄像头
            cameraHandler.obtainMessage(MSG_SET_PREVIEW_SIZE, previewSurfaceWidth, previewSurfaceHeight).sendToTarget();// 配置预览尺寸
            cameraHandler.obtainMessage(MSG_SET_PREVIEW_SURFACE, previewSurface).sendToTarget();// 配置预览 Surface
            cameraHandler.sendEmptyMessage(MSG_START_PREVIEW);// 开启预览
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/35431.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

指针的进阶(1)

指针的回顾 在C语言中&#xff0c;指针是一个变量&#xff0c;与其他数据不同的是&#xff0c;它的作用是用来存储其它变量的内存地址。指针可以指向不同类型的数据&#xff0c;包括整数、浮点数 、字符、数组等。通过使用指针&#xff0c;我们可以直接访问和修改存储在内存中…

Zabbix 的使用

Zabbix 的使用 一、添加 zabbix 客户端主机1.1 环境准备1.2 服务端和客户端都配置时间同步1.3 服务端和客户端都设置 hosts 解析1.4 设置 zabbix 的下载源&#xff0c;安装 zabbix-agent21.5 修改 agent2 配置文件1.6 启动 zabbix-agent21.7 在服务端验证 zabbix-agent2 的连通…

Android Studio实现内容丰富的安卓校园新闻浏览平台

如需源码可以添加q-------3290510686&#xff0c;也有演示视频演示具体功能&#xff0c;源码不免费&#xff0c;尊重创作&#xff0c;尊重劳动。 项目编号070 1.开发环境 android stuido jdk1.8 eclipse mysql tomcat 2.功能介绍 安卓端&#xff1a; 1.注册登录 2.查看新闻列表…

【爬虫】百度FengXiangBiao(完全爬虫卡住了,是爬虫+文本提取方式)

学习使用。爬虫有风险。使用需谨慎。切记切记。 参考链接&#xff1a;学习python爬虫—爬虫实践&#xff1a;爬取B站排行榜 都是排行榜反正 网页细节 按F12&#xff0c;打开控制台。前端就是这点好&#xff0c;非常直观。 找到排行的具体位置&#xff0c;如下图&#xff0c;这…

【如何成功加载 HuggingFace 数据集】不使用Colab,以ChnSentiCorp数据集为例

【如何成功加载 HuggingFace 数据集】不使用Colab&#xff0c;以ChnSentiCorp数据集为例 前置加载数据集尝试一&#xff1a;标准加载数据库代码尝试二&#xff1a;科学上网尝试三&#xff1a;把 Huggingface 的数据库下载到本地尝试3.5 创建 state.json彩蛋 前置 Huggingface …

找不到 配置管理器。sql server 2008 r2 在win10下

win10 打开sqlserver2008r2的SQL Server 配置管理器 &#xff0c;通过开始程序中找不到“SQL Server 配置管理器”。去自己电脑上此目录找&#xff0c;“C:\Windows\SysWOW64\SQLServerManager10.msc”&#xff0c;直接运行此文件就可以调出“SQL Server 配置管理器”。 在win1…

c语言内存

程序是保存在硬盘中的&#xff0c;要载入内存才能运行&#xff0c;CPU也被设计为只能从内存中读取数据和指令。 对于CPU来说&#xff0c;内存仅仅是一个存放指令和数据的地方&#xff0c;并不能在内存中完成计算功能&#xff0c; 如&#xff1a;计算abc,必须将a,b,c都读取到CPU…

CodeMirror 对 XML 文档熟悉及元素控制自定义

CodeMirror 是一个网络代码编辑器组件。它可以在网站中用于实现支持多种编辑功能的文本输入字段&#xff0c;并具有丰富的编程接口以允许进一步扩展。 本文为 xml 格式的代码提示约束格式规范的自定义示例内容。 先看效果&#xff0c;如下&#xff1a; 官方 Demo 的完整代码如…

十八、Jenkins(centos7)执行接口自动化测试脚本,飞书推送测试结果消息

十八、Jenkins&#xff08;centos7&#xff09;执行接口自动化测试脚本&#xff0c;飞书推送测试结果消息 1.创建 Freestyle project 项目 2. 输入git仓库地址 https://gitee.com/HP_mojin/pytest_allure_request_20220811 3. 增加构建步骤-Execute shell&#xff08;Jenkins…

怎样做好客户自助服务?

在当前高速发展的信息化时代&#xff0c;人们已经习惯了即时满足的方式。对于品牌来说&#xff0c;当客户遇到问题时&#xff0c;他们希望能够获得即时细致的解答。如果客户需要等待很长时间才能获取答案&#xff0c;他们的满意度就会降低。因此&#xff0c;企业是否提供客户自…

Java - 异常处理

异常介绍 对异常进行捕获&#xff0c;保证程序可以继续运行&#xff0c;提升程序的健壮性。 执行过程中所发生的异常时间可分为两大类&#xff1a; Error&#xff1a; Java虚拟机无法解决的严重问题。如&#xff1a;JVM系统内部错误&#xff0c;资源耗尽等严重情况。比如&…

mysql的一些练习题

1. 第1题 mysql> create database Market charset utf8; Query OK, 1 row affected (0.01 sec)第二题 mysql> use Market Database changed mysql> mysql> create table customers(-> c_num int(11) primary key auto_increment,-> c_name varchar(50),-&…

mac怎么把m4a转换成mp3?

mac怎么把m4a转换成mp3&#xff1f;大家都知道m4a是苹果公司专属的音频文件格式&#xff0c;因此它是可以直接在mac电脑上打开播放的&#xff0c;但这并不代表m4a音频文件可以在其他播放器或者播放设备上直接打开和使用&#xff0c;相信这个问题大家都遇到过&#xff0c;造成这…

黑马头条-day02

文章目录 前言一、文章列表加载1.1 需求分析1.2 表结构分析1.3 导入文章数据库1.4 实现思路1.5 接口定义1.6 功能实现 二、freemarker2.1 freemarker简介2.2 环境搭建&&快速入门2.2.1 创建测试工程 2.3 freemarker基础2.3.1 基础语法种类2.3.2 集合指令2.3.3 if指令2.3…

使用LiteSpeed缓存插件将WordPress优化到100%的得分

页面速度优化应该是每个网站所有者的首要任务&#xff0c;因为它直接影响WordPress SEO。此外&#xff0c;网站加载的时间越长&#xff0c;其跳出率就越高。这可能会阻止您产生转化并为您的网站带来流量。 使用正确的工具和配置&#xff0c;缓存您的网站可以显着提高其性能。因…

【Spark_BigData】期末复习考试——

复习题目 yarn框架中不包含的进程为 Yarn包括两个主要进程:资源管理器Resource-Manager,节点管理器Node-Manager。 Scheduler zookeeper spark SQL 前身 Shark 在Spark中,DataFrame是一种以RDD为基础的分布式数据集,类似于传统数据库中的二维表格。 HiveContext继承自SQLCon…

Jvisualvm内存模型剖析-JVM(五)

上篇文章代码讲解了tomcat加载以及gc回收流程。 Jvm内存模型剖析优化-JVM&#xff08;四&#xff09; Jvisualvm 我们可以编写如上代码&#xff0c;之后打开jvm自带的工具jvisualvm。 如果我们看visual不会明显变化&#xff0c;则可以修改睡眠参数&#xff0c;时间改小。 当…

Vue中的el-date-picker时间选择器的使用

1、value-format属性设置需要什么格式的时间 2、type类型选择datetime、date 年月日时分秒 <el-date-pickervalue-format"yyyy-MM-dd HH:mm:ss"v-model"excelRuleForm.startTime"type"datetime":placeholder"选择开始时间"> &…

【尚医通】vue3+ts前端项目开发笔记——项目分析

尚医通开发笔记 一、项目分析 项目在线地址&#xff1a;http://syt.atguigu.cn测试帐号&#xff1a;17720125002 首页 home header 全局组件布局 左&#xff1a;logo 、title中&#xff1a;初始隐藏 搜索框 公共组件显示条件&#xff1a;在页面滚动到页面内搜索框的位置显示…

爬虫入门04——requests库中的User-Agent请求头

import requests#定义请求的url url https://www.baidu.com/ #https://site.ip138.com/www.xicidaili.com/#发起get请求 res requests.get(url url)#获取响应结果#响应对象 print(res)#获取响应状态码 print(res.status_code)#获取响应数据 print(res.text) #返回的是字符…
最新文章