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 上,而不做任何适配,画面就会被拉伸。
正确的做法是:
- 从 Camera 支持的预览尺寸列表中,选择一个最合适的尺寸(通常根据目标宽高比选择)。
- 调整 SurfaceView 或 TextureView 的尺寸,使其宽高比与预览尺寸一致。
举个例子,假设你希望预览全屏显示,但 Camera 输出是 4:3,屏幕是 16:9。这时候有两种方案:
- 方案一:让 Surface 填满屏幕,但画面会被裁剪(部分内容显示不全)。
- 方案二:让 Surface 按比例缩放,上下或左右留黑边。
我个人更倾向于方案一,因为全屏体验更好。但要注意,裁剪会导致部分画面丢失,你需要根据实际场景权衡。
代码实现上,你可以在 surfaceChanged 或 onSurfaceTextureAvailable 里动态调整 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 度。
处理方式分两步:
- 设置 Camera 的显示方向:通过
Camera.setDisplayOrientation(int)或 Camera2 的CaptureRequest来旋转预览画面。 - 调整 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,你需要设置 CaptureRequest 的 JPEG_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 应用、旋转处理,每一步都环环相扣。
嗯,这张图基本把整个流程串起来了。你从选择 View 开始,然后管理 Surface 生命周期,接着匹配尺寸,再用 Transform 矩阵做精细控制,最后处理旋转。每一步都踩对,预览就不会出问题。
个人经验:我建议你在项目初期就把这些逻辑封装成一个工具类或基类,比如 CameraPreviewHelper。这样后续切换 Camera API 或调整 UI 时,只需要改少量代码。我曾经在一个项目里因为没封装,导致三个页面各写了一套预览逻辑,后来维护起来简直想哭。
好了,关于 Camera 与 SurfaceView/TextureView 的核心内容就这些。记住,Surface 生命周期是基础,尺寸匹配是保证不变形的关键,Transform 矩阵是灵活控制的利器,旋转处理是适配不同设备的必修课。把这四点吃透,Camera 预览这块你就稳了。