25、Camera 与 SurfaceView/TextureView:Surface生命周期管理、Surface尺寸匹配、Transform矩阵应用、预览旋转处理。

说实话,Camera 预览这块,很多开发者一开始都会栽跟头。我自己刚做相机开发那会儿,也遇到过预览画面拉伸、旋转方向不对、甚至黑屏闪退的问题。后来才明白,核心就在于你选的 View 组件——SurfaceView 还是 TextureView——以及你对 Surface 生命周期的掌控。

今天我们就来掰扯清楚这几个关键点:Surface 怎么生怎么死、尺寸怎么匹配、Transform 矩阵怎么用、预览旋转怎么处理。嗯,内容有点干,但都是实战经验。

SurfaceView vs TextureView:选哪个?

先说说这两个组件。SurfaceView 和 TextureView 都能用来显示 Camera 预览,但底层机制完全不同。

  • SurfaceView:它有一个独立的 Surface,在单独的窗口层上绘制。说白了,它不跟普通 View 共享同一个绘图表面。好处是性能好,坏处是动画、平移、缩放这些操作比较麻烦,因为它的窗口层是独立的。
  • TextureView:它把 Surface 内容作为纹理,在 View 层级中绘制。所以它可以像普通 View 一样做动画、变换。但代价是性能开销稍大,而且需要显式管理生命周期。

我个人习惯:如果只是全屏预览,用 SurfaceView 就够了。如果需要做缩放、旋转、滤镜等效果,我会选 TextureView。我在项目中遇到过用 SurfaceView 做圆形裁剪预览,结果折腾了半天窗口层级,最后还是换成了 TextureView 才搞定。

核心结论:SurfaceView 性能优先,TextureView 灵活性优先。没有绝对的好坏,看场景。

Surface 生命周期管理

不管是 SurfaceView 还是 TextureView,Camera 预览都需要一个 Surface 来输出数据。Surface 什么时候创建、什么时候销毁,直接决定了预览能不能正常启动和关闭。

对于 SurfaceView,你需要监听 SurfaceHolder.Callback

SurfaceView surfaceView = findViewById(R.id.surfaceView);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        // Surface 已创建,可以打开 Camera 并设置预览 Surface
        // 注意:此时还没有尺寸信息
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // Surface 尺寸发生变化,通常在这里重新配置 Camera
        // 比如设置预览尺寸、启动预览循环
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        // Surface 即将销毁,必须停止 Camera 预览并释放资源
        // 否则会报错或闪退
    }
});

对于 TextureView,你需要监听 TextureView.SurfaceTextureListener

TextureView textureView = findViewById(R.id.textureView);
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        // SurfaceTexture 可用,可以打开 Camera
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
        // 尺寸变化,重新配置 Camera
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        // SurfaceTexture 即将销毁,停止预览
        return true; // 返回 true 表示 SurfaceTexture 可以被回收
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        // 每帧更新时回调,一般不用处理
    }
});

注意:我曾经在 surfaceDestroyed 里忘记停止 Camera 预览,结果导致应用切到后台再回来时黑屏。后来养成了习惯:在 Surface 销毁回调里,一定要先 camera.stopPreview()camera.release()

Surface 尺寸匹配:别让预览变形

预览画面拉伸变形,是新手最容易犯的错。原因很简单:Camera 输出的预览尺寸和 Surface 的尺寸不一致。

Camera 预览尺寸通常有固定的宽高比,比如 4:3 或 16:9。而 SurfaceView 或 TextureView 的尺寸由布局决定。如果你直接把 Camera 预览设置到 Surface 上,而不做任何适配,画面就会被拉伸。

正确的做法是:

  1. 从 Camera 支持的预览尺寸列表中,选择一个最合适的尺寸(通常根据目标宽高比选择)。
  2. 调整 SurfaceView 或 TextureView 的尺寸,使其宽高比与预览尺寸一致。

举个例子,假设你希望预览全屏显示,但 Camera 输出是 4:3,屏幕是 16:9。这时候有两种方案:

  • 方案一:让 Surface 填满屏幕,但画面会被裁剪(部分内容显示不全)。
  • 方案二:让 Surface 按比例缩放,上下或左右留黑边。

我个人更倾向于方案一,因为全屏体验更好。但要注意,裁剪会导致部分画面丢失,你需要根据实际场景权衡。

代码实现上,你可以在 surfaceChangedonSurfaceTextureAvailable 里动态调整 View 的尺寸:

// 假设 cameraPreviewSize 是 Camera 支持的预览尺寸
int previewWidth = cameraPreviewSize.getWidth();
int previewHeight = cameraPreviewSize.getHeight();

// 计算目标宽高比
float aspectRatio = (float) previewWidth / previewHeight;

// 根据父容器尺寸调整 SurfaceView 的宽高
int parentWidth = parent.getWidth();
int parentHeight = parent.getHeight();
int newWidth, newHeight;

if (parentWidth / parentHeight > aspectRatio) {
    // 父容器更宽,按高度缩放
    newHeight = parentHeight;
    newWidth = (int) (parentHeight * aspectRatio);
} else {
    // 父容器更高,按宽度缩放
    newWidth = parentWidth;
    newHeight = (int) (parentWidth / aspectRatio);
}

// 更新 SurfaceView 的布局参数
surfaceView.getLayoutParams().width = newWidth;
surfaceView.getLayoutParams().height = newHeight;
surfaceView.requestLayout();

小技巧:如果你用 TextureView,还可以通过 setTransform() 来缩放和裁剪,而不必改变 View 的尺寸。后面会讲到 Transform 矩阵。

Transform 矩阵应用:灵活控制预览画面

Transform 矩阵是 TextureView 的杀手锏。你可以通过 setTransform(Matrix) 来对预览画面进行平移、缩放、旋转等操作,而不影响 View 本身的位置。

比如,你想实现一个「镜像」效果(前置摄像头自拍时常用):

Matrix matrix = new Matrix();
// 水平翻转:x 轴缩放 -1,然后平移回来
matrix.preScale(-1f, 1f);
matrix.postTranslate(textureView.getWidth(), 0);
textureView.setTransform(matrix);

再比如,你想让预览画面居中裁剪显示:

// 假设预览尺寸是 1920x1080,TextureView 尺寸是 1080x1920(竖屏)
// 需要将预览画面缩放并裁剪到 TextureView 的尺寸
float scaleX = (float) textureView.getWidth() / previewWidth;
float scaleY = (float) textureView.getHeight() / previewHeight;
float scale = Math.max(scaleX, scaleY); // 取较大值,保证填满

Matrix matrix = new Matrix();
matrix.setScale(scale, scale);
// 平移使画面居中
float dx = (textureView.getWidth() - previewWidth * scale) / 2f;
float dy = (textureView.getHeight() - previewHeight * scale) / 2f;
matrix.postTranslate(dx, dy);
textureView.setTransform(matrix);

我在项目中遇到过一个问题:用 TextureView 做预览,然后叠加一个自定义的绘制层。如果直接修改 TextureView 的 Transform,绘制层的位置也会跟着变。后来我改用 SurfaceView + 单独的绘制层,才避免了相互干扰。所以,Transform 虽好,但要注意它会影响整个 TextureView 的内容。

预览旋转处理:方向不对,一切白费

Camera 预览的方向问题,可以说是最坑的。因为 Camera 传感器默认是横屏的,而手机屏幕通常是竖屏的。如果不做处理,预览画面会旋转 90 度或 270 度。

处理方式分两步:

  1. 设置 Camera 的显示方向:通过 Camera.setDisplayOrientation(int) 或 Camera2 的 CaptureRequest 来旋转预览画面。
  2. 调整 Surface 的尺寸匹配:旋转后,预览的宽高会互换,需要重新计算尺寸。

对于 Camera1 API,设置方向很简单:

// 根据当前屏幕旋转角度计算
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;
}

// 对于后置摄像头,旋转角度为 degrees
// 对于前置摄像头,需要镜像处理
int result = (cameraInfo.orientation - degrees + 360) % 360;
camera.setDisplayOrientation(result);

对于 Camera2 API,你需要设置 CaptureRequestJPEG_ORIENTATION 和预览的 Surface 方向。但更常见的是,你直接旋转 TextureView 的 Transform 矩阵来匹配。

举个例子,如果 Camera 输出是横屏(1920x1080),而屏幕是竖屏(1080x1920),你可以通过 Transform 矩阵旋转 90 度:

Matrix matrix = new Matrix();
matrix.postRotate(90f, textureView.getWidth() / 2f, textureView.getHeight() / 2f);
// 旋转后需要平移,使画面居中
// 具体平移量取决于旋转方向和尺寸
textureView.setTransform(matrix);

避坑指南:我曾经在旋转后忘记调整预览尺寸的宽高比,结果画面被拉伸。后来我总结了一个原则:先旋转,再匹配尺寸。也就是说,先确定 Camera 输出的方向,然后根据这个方向去选择预览尺寸,最后再调整 Surface 的尺寸。

知识体系总览

下面这张图梳理了本章的核心逻辑,从 View 选择到生命周期、尺寸匹配、Transform 应用、旋转处理,每一步都环环相扣。

Camera 预览核心流程 选择 View 组件 SurfaceView TextureView Surface 生命周期管理 创建 尺寸变化 销毁 尺寸匹配 选择预览尺寸 调整 View 尺寸 Transform 矩阵应用 缩放 平移 旋转 预览旋转处理 设置显示方向 调整尺寸匹配 正确显示 Camera 预览

嗯,这张图基本把整个流程串起来了。你从选择 View 开始,然后管理 Surface 生命周期,接着匹配尺寸,再用 Transform 矩阵做精细控制,最后处理旋转。每一步都踩对,预览就不会出问题。

个人经验:我建议你在项目初期就把这些逻辑封装成一个工具类或基类,比如 CameraPreviewHelper。这样后续切换 Camera API 或调整 UI 时,只需要改少量代码。我曾经在一个项目里因为没封装,导致三个页面各写了一套预览逻辑,后来维护起来简直想哭。

好了,关于 Camera 与 SurfaceView/TextureView 的核心内容就这些。记住,Surface 生命周期是基础,尺寸匹配是保证不变形的关键,Transform 矩阵是灵活控制的利器,旋转处理是适配不同设备的必修课。把这四点吃透,Camera 预览这块你就稳了。

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