Camera1 API 核心方法详解:从打开到拍照的完整流程

各位同学,今天我们来啃 Camera1 API 这块硬骨头。说实话,现在新项目基本都用 CameraX 了,但你要是去维护老项目、做 ROM 开发、或者搞一些定制化需求——Camera1 你绕不开。我自己刚入行那会儿,被 Camera1 的 callback 机制坑过好几次,今天就把这些经验全抖出来。

1. Camera.open() —— 一切从这里开始

打开摄像头,就这么简单?嗯,表面上是这样。但背后有不少门道。

Camera camera = Camera.open(); // 打开后置摄像头
Camera camera = Camera.open(0); // 显式指定摄像头 ID

我个人习惯用带参数的版本。为什么?因为有些设备上,Camera.open() 默认打开的是前置摄像头——你想想看,这要是做扫码功能,画面一片漆黑,用户不骂娘才怪。

注意:Camera.open() 是耗时操作,千万别在主线程调用。我曾经在一个项目里直接在 UI 线程调了 open(),结果 ANR 弹窗直接教做人。

另外,记得检查摄像头是否存在:

int cameraCount = Camera.getNumberOfCameras();
if (cameraCount == 0) {
    // 没有摄像头,别硬来
    return;
}

2. setParameters() —— 调参的艺术

打开摄像头之后,第一件事就是配参数。说白了,就是告诉摄像头:「我要多大分辨率、要不要闪光灯、对焦模式怎么搞」。

Camera.Parameters params = camera.getParameters();
params.setPictureSize(1920, 1080);
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
camera.setParameters(params);

这里有个坑——不是所有参数都支持。你设了一个不支持的参数,setParameters() 直接抛异常。我早期写代码时就被这个坑过,后来养成了习惯:

List<Size> supportedSizes = params.getSupportedPictureSizes();
// 遍历找到最合适的尺寸,别硬编码
小技巧:setParameters() 最好在 startPreview() 之前调用。虽然之后也能调,但有些设备会闪一下黑屏,用户体验很差。

3. setPreviewDisplay() —— 把画面「投」到屏幕上

摄像头采集到的数据,总得有个地方显示吧?setPreviewDisplay() 就是干这个的。它需要一个 SurfaceHolder,通常来自 SurfaceView

SurfaceView surfaceView = findViewById(R.id.surface_view);
SurfaceHolder holder = surfaceView.getHolder();
holder.addCallback(new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // Surface 准备好了,可以设置预览
        try {
            camera.setPreviewDisplay(holder);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // ... 其他回调
});

注意顺序:先等 Surface 创建完成,再 setPreviewDisplay。你要是反过来,摄像头数据来了但没地方画,画面就是黑的。

4. startPreview() —— 让画面动起来

参数配好了,显示目标也设了,该让摄像头干活了。

camera.startPreview();

就一行?对,就一行。但这一行背后,摄像头开始疯狂采集数据、送数据到 Surface、刷新画面。你想想看,每秒 30 帧的画面,背后是硬件和软件的高强度配合。

核心要点:startPreview() 之后,你才能调用 takePicture()。没开预览就拍照?有些设备会直接闪退,有些会拍出黑图。别问我怎么知道的。

5. takePicture() 的回调机制 —— 最绕的部分来了

拍照不是「咔嚓」一下就完事的。它有三个回调,按顺序触发:

回调 触发时机 数据内容
ShutterCallback 按下快门瞬间 无数据,只做通知(比如播放快门声)
PictureCallback (raw) 原始数据可用时 未压缩的 RAW 数据(可能为 null)
PictureCallback (jpeg) JPEG 编码完成 最终的 JPEG 字节数组
camera.takePicture(shutterCallback, rawCallback, jpegCallback);

// 实际项目中,我通常只关心 JPEG 回调
Camera.PictureCallback jpegCallback = new Camera.PictureCallback() {
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
        // data 就是 JPEG 图片的字节流
        // 可以保存到文件、上传服务器、或者显示在 ImageView 上
        saveImageToFile(data);
        camera.startPreview(); // 别忘了重新开始预览!
    }
};

这里有个关键点:拍照后预览会自动停止。你要是不重新调 startPreview(),画面就卡住了。我见过不少新手在这栽跟头——拍了一张照片后,预览画面不动了,以为手机死机了。

避坑指南:我曾经在 takePicture() 的回调里做耗时操作(比如写文件),结果 ANR 了。正确的做法是:回调里只做轻量操作,写文件扔到子线程去。

知识体系总览

下面这张图,把 Camera1 的核心流程串起来了。你照着这个顺序写代码,基本不会出大问题。

Camera1 API 核心调用流程 Camera.open() 打开摄像头硬件 setParameters() 配置分辨率/对焦/闪光 setPreviewDisplay() 绑定 Surface 显示 预览 takePicture() 触发拍照 ShutterCallback 快门声/动画 PictureCallback (raw) 原始数据(可能为null) PictureCallback (jpeg) 最终 JPEG 数据 重新 startPreview()

总结一下

Camera1 的这五个方法,说白了就是一条流水线:开设备 → 调参数 → 绑屏幕 → 开预览 → 拍照收数据。每一步都有坑,但只要你按顺序来,注意回调里的线程问题,基本稳了。

我个人建议,初学者先把 takePicture() 的 JPEG 回调玩熟。别一上来就搞 raw 数据——那玩意儿处理起来麻烦,而且很多设备根本不支持。先把照片拍出来、存下来,成就感有了,后面再慢慢深入。

最后提醒一句:用完摄像头记得释放!camera.release() 一定要在 finally 块里调,不然下次打开摄像头会报错。我早期写代码时漏过几次,结果测试同事天天找我:「摄像头打不开了!」——尴尬。

公众号:蓝海资料掘金营,微信deep3321