6. 预览Surface配置:使用TextureView作为预览载体、配置SurfaceTexture、设置预览尺寸
好,我们接着往下走。上一节我们把Camera打开,拿到了数据流。但数据流往哪送?你得有个“画布”吧。Android里做相机预览,最常见的两个选择是SurfaceView和TextureView。我个人更偏爱TextureView,原因很简单——它本身就是一个View,可以随便加动画、做变换,甚至能直接当普通控件用。SurfaceView虽然性能好一点,但它在WMS里是独立窗口,很多UI操作受限。
今天我们就来聊聊,怎么用TextureView搭起预览的“舞台”。
为什么选TextureView?
说白了,TextureView就是一个带了SurfaceTexture的View。它不像SurfaceView那样需要独立的窗口,而是直接参与View树的绘制流程。这意味着你可以对它做平移、缩放、旋转,甚至用setAlpha做透明效果。
我在项目中遇到过这样一个场景:需要把相机预览画面缩小到屏幕右下角,同时左上角显示一个AR叠加层。用SurfaceView的话,我得处理窗口层级和Z-order,麻烦得很。换成TextureView,直接一个setScaleX和setTranslationX就搞定了。
核心结论:如果你需要灵活的UI交互,选TextureView。如果追求极致性能和低延迟,选SurfaceView。
TextureView的初始化
在布局里加一个TextureView很简单,但有几个关键点要注意。
<TextureView
android:id="@+id/preview_texture"
android:layout_width="match_parent"
android:layout_height="match_parent" />
嗯,这里要注意:宽高不要写死。因为相机输出的尺寸和屏幕尺寸往往不一样,我们需要在代码里动态调整。
接下来,在Activity或Fragment里拿到这个View,并设置监听器:
TextureView textureView = findViewById(R.id.preview_texture);
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
// SurfaceTexture准备好了,可以打开相机了
openCamera(surface, width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
// 尺寸变了,可能需要重新配置
configureTransform(width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
// 销毁了,记得释放相机
releaseCamera();
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
// 每帧更新时回调,一般不用管
}
});
你想想看,这个回调的顺序很重要。onSurfaceTextureAvailable只触发一次,之后如果尺寸变化,走的是onSurfaceTextureSizeChanged。我曾经踩过一个坑:在onSurfaceTextureAvailable里直接开了相机,但当时TextureView的尺寸还没最终确定,导致预览画面被拉伸。后来我改成在onSurfaceTextureSizeChanged里再做一次尺寸校正,问题就解决了。
配置SurfaceTexture
SurfaceTexture是TextureView背后的“幕后英雄”。它接收Camera输出的图像数据,然后交给TextureView渲染。我们需要做的是:把SurfaceTexture传给Camera,告诉它“往这里画”。
private void openCamera(SurfaceTexture surfaceTexture, int width, int height) {
// 设置默认缓冲区大小,避免内存浪费
surfaceTexture.setDefaultBufferSize(previewWidth, previewHeight);
// 创建Surface对象,Camera需要这个
Surface surface = new Surface(surfaceTexture);
try {
camera = Camera.open();
camera.setPreviewDisplay(surface);
camera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
这里有个细节:setDefaultBufferSize一定要调用。如果不设,SurfaceTexture会使用TextureView的尺寸作为缓冲区大小。但TextureView的尺寸是屏幕像素,而相机输出的尺寸是传感器像素,两者往往不匹配。不设的话,轻则浪费内存,重则画面变形。
小技巧:setDefaultBufferSize的宽高,应该和你最终期望的预览尺寸一致,而不是TextureView的显示尺寸。比如你希望预览是1280x720,那就设成1280x720。
设置预览尺寸
预览尺寸的选择,直接决定了画面清晰度和性能。Camera API里有一个getParameters()方法,可以拿到所有支持的预览尺寸。我们需要从中选一个最合适的。
private Size chooseOptimalSize(List<Camera.Size> supportedSizes, int textureViewWidth, int textureViewHeight) {
// 先找和TextureView宽高比一致的尺寸
float targetRatio = (float) textureViewWidth / textureViewHeight;
Size optimalSize = null;
float minDiff = Float.MAX_VALUE;
for (Camera.Size size : supportedSizes) {
float ratio = (float) size.width / size.height;
if (Math.abs(ratio - targetRatio) < 0.01f) {
// 宽高比匹配,再选分辨率最高的
if (optimalSize == null || size.width > optimalSize.width) {
optimalSize = size;
}
}
}
// 如果没有完全匹配的,就选最接近的
if (optimalSize == null) {
for (Camera.Size size : supportedSizes) {
float ratio = (float) size.width / size.height;
float diff = Math.abs(ratio - targetRatio);
if (diff < minDiff) {
minDiff = diff;
optimalSize = size;
}
}
}
return optimalSize;
}
这段代码的逻辑是:先找宽高比完全匹配的,找不到就找最接近的。为什么优先匹配宽高比?因为如果宽高比不一致,画面会被拉伸,用户体验很差。
我在项目中遇到过一个问题:某些低端手机支持的预览尺寸列表特别长,里面有很多奇怪的尺寸,比如176x144。如果不加筛选,算法可能会选到这种低分辨率尺寸,画面糊得一塌糊涂。后来我加了一个最小分辨率阈值,低于640x480的直接跳过,效果好了很多。
处理画面方向
嗯,这里还有一个容易忽略的点:方向。手机摄像头传感器默认是横屏的,但我们的App可能是竖屏的。如果不做处理,预览画面会旋转90度。
private void configureTransform(int viewWidth, int viewHeight) {
if (textureView == null || previewSize == null) {
return;
}
Matrix matrix = new Matrix();
float scaleX = (float) viewWidth / previewSize.getWidth();
float scaleY = (float) viewHeight / previewSize.getHeight();
// 以中心点为基准进行缩放
float centerX = viewWidth / 2f;
float centerY = viewHeight / 2f;
matrix.postScale(scaleX, scaleY, centerX, centerY);
// 旋转90度(竖屏场景)
matrix.postRotate(90, centerX, centerY);
textureView.setTransform(matrix);
}
这段代码做了两件事:缩放和旋转。缩放是为了让预览画面填满TextureView,旋转是为了纠正方向。注意旋转的中心点要设在TextureView的中心,否则画面会偏移。
警告:不要直接在Camera的setDisplayOrientation里设置90度就完事了。那个方法只影响预览数据的输出方向,但TextureView本身的坐标系没变。如果你不配合setTransform做矩阵变换,画面虽然正了,但触摸坐标会错位。比如你点左上角,实际响应的是右上角。
整体流程梳理
说了这么多,我们来画个图,把整个流程串起来。
从图上可以看得很清楚:整个流程是串行的。TextureView准备好之后,我们选尺寸、配Surface、开相机、调方向,一步都不能少。少了哪一步,预览要么出不来,要么画面有问题。
常见问题与避坑
- 预览画面黑屏:检查SurfaceTexture是否已经可用。onSurfaceTextureAvailable没回调之前,不要尝试打开相机。
- 画面拉伸变形:预览尺寸和TextureView的宽高比不一致。用我上面写的chooseOptimalSize方法,优先匹配宽高比。
- 画面旋转不对:Camera的setDisplayOrientation和TextureView的setTransform要配合使用。只设一个,另一个不设,就会出问题。
- 内存泄漏:在onSurfaceTextureDestroyed里一定要释放Camera。我曾经因为忘记释放,导致App切到后台再切回来,Camera打开失败。
我的经验:如果你发现预览画面有闪烁或者撕裂感,试试在setDefaultBufferSize时把尺寸设小一点。比如从1920x1080降到1280x720,很多时候肉眼根本看不出区别,但性能提升很明显。
好了,TextureView的配置就聊到这里。下一节我们会深入Camera2 API,看看新的架构下怎么玩转预览。不过在那之前,建议你把今天的内容动手敲一遍。光看代码是记不住的,亲手调过尺寸、转过方向,你才能真正理解这些配置的意义。
公众号:蓝海资料掘金营,微信deep3321