Android Camera SurfaceView 获取预览数据

本文详细介绍如何在Android中利用Camera、SurfaceView和SurfaceHolder获取并处理相机预览图像,适用于视频直播、美颜相机等功能的开发。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

        Android 调用相机拍照,相信大家已经很熟悉了,通过Intent指定Action和Category,再通过startActivityForResult()获取到拍照的回调,这样获取的是系统相机,我们只能得到它拍摄的图片,再进行后续处理。有些功能,比如视频直播,美颜相机,需要对相机预览的图像进行处理,这时候我们就需要通过Android的相机Camera来进行操作。文章主要偏使用,不会过多的将原理,想要了解原理的,后续我会写一篇关于Android Usb相机的实现,当然这是后话了。

        获取相机预览的视频主要需要三个东西

          1.Camera  不说了,既然相机当然需要他

          2.Surfaceview 说的浅显一点,就是一个View,用来显示预览视频的控件,具体可以看一下官方文档,当然也可以用TextureView,后续会进行比较。

          3.SurfaceHolder 我的理解,Holder在其中扮演了纽带的作用,控制着Camera和Surface之间的联系。

实现过程分三步:

1.创建SurfaceView

          首先,我们需要准备好布局对吧,很简单,就一个SurfaceView,这儿我就截图了,文章后面会放上Demo链接

            

同时获取Holder,并添加监听addCallBack() 用于监听SurfaceView的生命周期,三个阶段create,change,destory。

            

2.初始化相机

          布局创建好了,就该相机出场了,一贯的习惯,开相机,设置一堆参数,最后再开始预览,6.0之后需要申请权限,同时需要检查相机固件是否存在,这儿就不赘述了,Demo里面有。具体操作方式,我会一步步解析。

           看上面的截图,可以看到camera.open(cameraID),简单说一下,其实在Linux中,一切都是文件。相机也不例外,而open其实最终打开的是/dev/video这个文件。感兴趣的朋友可以了解一下。

打开成功之后,就可以获取到Camera的实例了,这时候我们就需要去设置一些列的参数,包括分辨率,帧率,是否对焦,旋转等参数,具体看下面截图:

注意 parameters.setPreviewFormat(ImageFormat.NV21); 表示相机的预览数据格式,NV21,感兴趣的可以了解一下。

3.开启预览,获取回调

          

这儿有几个点,

1.首先尽量用setPreviewCallbackWithBuffer()这个回调,因为他在回调前先做了addCallbackBuffer()的操作,该操作的目的是分配一块内存空间,用于临时存放回调的buffer,当每次camera.addCallbackBuffer(datas)的时候会复用之前分配的内存地址,避免了频繁的创建buffer造成不必要的GC。

2.不知道有没有留意到,new byte[]的时候做了计算,其实我这儿是有点错的,现在才发现。byte[]的大小是装一帧NV21的大小,当然可以适当的大一点点,但是不能小。NV21是YUV的一种,大小计算方式为width*height*3/2 这样才是正确的大小。所以这儿可以修改一下。byte[]过大,可能会导致CPU的占用率升高,所以最好是一样大小。

最后拿到了回调数据,就可以自由发挥了,可以编码之后推流出去,也就是直播使用,也可以加点滤镜,每个图再重新渲染,总之就是为所欲为。当然这些操作,等你的就是一堆格式转换,编码解码的问题,后续我也会一一介绍。好了,附上demo链接

1.优快云 : https://download.youkuaiyun.com/download/renlei0012/10983330

2.GitHub: https://github.com/renlei521/CameraDemo.git

<think>我们正在解决Android中使用SurfaceView全屏预览Camera时出现的画面拉伸问题。 根据引用内容,我们知道问题根源在于SurfaceView的宽高比与Camera设置的预览尺寸宽高比不一致。 解决方案的核心是调整Camera预览尺寸,使其与SurfaceView的宽高比匹配(注意:不是设置屏幕物理宽高,而是设置相机支持的预览分辨率)。 步骤: 1. 获取相机支持的预览尺寸列表。 2. 计算SurfaceView的宽高比(注意:SurfaceView的尺寸可能是全屏,但全屏的宽高比可能与相机支持的预览尺寸宽高比不同)。 3. 从相机支持的预览尺寸中选择一个与SurfaceView宽高比最接近的尺寸。 4. 设置相机使用该预览尺寸。 另外,引用[2]中提到,也可以考虑对TextureView使用setTransform(Matrix)进行缩放,但这可能导致预览拉伸,因此他们采用了折中方案:根据相机所能提供的分辨率和屏幕分辨率之间做“适配”,在做到预览不拉伸的前提下尽可能让预览尺寸接近全屏。 因此,我们有两种思路: 思路一:调整预览尺寸(适用于SurfaceView和TextureView) 思路二:使用TextureView并通过矩阵变换来适配(仅适用于TextureView,因为SurfaceView不支持变换) 由于问题中明确使用SurfaceView,因此我们采用思路一。 具体实现: 步骤1:获取相机支持的预览尺寸 在Camera1 API中,我们可以通过Camera.Parameters.getSupportedPreviewSizes()获取支持的预览尺寸列表。 步骤2:计算目标宽高比 目标宽高比 = SurfaceView的宽度 / SurfaceView的高度(注意:这里需要考虑屏幕方向,如果是竖屏,通常宽高比是9:16,横屏是16:9) 步骤3:选择最佳预览尺寸 我们需要从支持的预览尺寸中选择一个宽高比最接近目标宽高比的尺寸。同时,还要考虑预览尺寸不能超过SurfaceView的尺寸(或者选择尽可能大的预览尺寸,但保证宽高比匹配)。 步骤4:设置相机参数 将选择的预览尺寸设置给Camera,然后重新设置参数。 但是,在SurfaceView的尺寸确定之前,我们可能无法获取到它的实际尺寸。因此,我们可以在SurfaceView的surfaceChanged回调中执行上述步骤。 参考引用[3]中提到,在SurfaceHolder.Callback的surfaceChanged方法中重新创建或打开CaptureSession(针对Camera2 API)。对于Camera1 API,我们可以在surfaceChanged中设置相机参数。 代码示例(Camera1 API): 注意:以下代码仅为示例,实际中需要处理异常和不同设备兼容性问题。 ```java public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private Camera mCamera; private SurfaceHolder mHolder; public CameraPreview(Context context, Camera camera) { super(context); mCamera = camera; mHolder = getHolder(); mHolder.addCallback(this); // 设置Surface类型(已过时,但为了兼容) mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } @Override public void surfaceCreated(SurfaceHolder holder) { // 当Surface被创建时,开始预览 try { mCamera.setPreviewDisplay(holder); mCamera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // 当Surface尺寸改变时,调整预览 // 注意:在surfaceCreated之后,surfaceChanged至少会被调用一次 if (mHolder.getSurface() == null) { return; } try { mCamera.stopPreview(); } catch (Exception e) { // 忽略停止预览失败 } // 获取相机参数 Camera.Parameters parameters = mCamera.getParameters(); // 获取支持的预览尺寸 List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes(); // 计算目标宽高比(注意:这里width和height是SurfaceView的宽高,即全屏尺寸) float targetRatio = (float) width / height; // 选择最佳预览尺寸 Camera.Size bestSize = null; float minRatioDiff = Float.MAX_VALUE; for (Camera.Size size : supportedPreviewSizes) { float ratio = (float) size.width / size.height; float ratioDiff = Math.abs(ratio - targetRatio); if (ratioDiff < minRatioDiff) { minRatioDiff = ratioDiff; bestSize = size; } } if (bestSize != null) { // 设置预览尺寸 parameters.setPreviewSize(bestSize.width, bestSize.height); mCamera.setParameters(parameters); } try { mCamera.setPreviewDisplay(mHolder); mCamera.startPreview(); } catch (Exception e) { e.printStackTrace(); } } @Override public void surfaceDestroyed(SurfaceHolder holder) { // 释放相机等操作 } } ``` 注意:上述代码在Camera1 API中有效,但Camera1 API已经过时,建议使用Camera2 API。 对于Camera2 API,处理逻辑类似,但API调用方式不同。在Camera2中,我们需要在创建CaptureSession时指定一个Surface,而这个Surface来自于SurfaceView。在设置预览尺寸时,我们需要从StreamConfigurationMap中获取可用的输出尺寸,然后选择与目标宽高比最匹配的尺寸。最后,在创建CaptureSession时使用该尺寸。 由于Camera2 API较为复杂,这里不展开详细代码,但思路一致。 另外,引用[2]中提到,他们采用折中方案:在预览不拉伸的前提下尽可能让预览尺寸接近全屏。这意味着我们可能无法完全填满屏幕,但可以保证不变形。通常的做法是让预览按比例缩放,并在屏幕中居中显示,多余的部分留黑边。 但是,如果使用SurfaceView,我们无法直接通过设置缩放来避免拉伸(因为SurfaceView不支持缩放变换),所以只能通过调整预览尺寸来匹配SurfaceView的宽高比。如果无法精确匹配,则会出现黑边(即预览不会拉伸,但可能无法填满整个SurfaceView)。要解决黑边问题,可以调整SurfaceView的布局,使其与预览尺寸的宽高比一致,但这样可能无法全屏。 因此,全屏预览且不变形的完美方案是:选择与屏幕宽高比一致的预览尺寸。如果相机不支持该宽高比,则选择最接近的,然后通过裁剪或留黑边来处理。但裁剪可能会丢失部分画面,留黑边则不是真正的全屏。 引用[3]中建议在surfaceChanged中重新创建CaptureSession,这适用于Camera2 API。在Camera2中,当Surface尺寸变化时,需要重新创建会话。 总结: 1. 使用Camera1 API时,在surfaceChanged回调中重新设置预览尺寸(选择与SurfaceView宽高比最接近的预览尺寸)。 2. 使用Camera2 API时,在onSurfaceChanged中重新打开相机并创建CaptureSession,同时选择匹配的预览尺寸。 注意:由于不同设备支持的预览尺寸不同,我们需要动态选择。 另外,如果使用TextureView(它支持变换),则可以通过矩阵变换来适配,这样即使预览尺寸与视图宽高比不一致,也可以通过变换来适配。但是,问题要求使用SurfaceView,所以我们不展开TextureView的方案。 最后,关于全屏预览,我们可能无法在所有设备上实现完美的全屏且不变形,因为相机硬件可能不支持屏幕的宽高比。因此,折中方案是:保证预览不变形,并尽可能接近全屏,允许出现黑边。 如果必须全屏且不变形,可能需要裁剪预览画面(即选择比屏幕宽高比更大的预览尺寸,然后裁剪掉多余部分),但这需要更复杂的处理,例如自定义SurfaceView或使用TextureView。 根据引用内容,我们主要采用调整预览尺寸的方法来解决拉伸问题。 相关问题: 1. 如何获取相机支持的预览尺寸列表? 2. 在Camera2 API中如何实现预览尺寸的适配? 3. 除了调整预览尺寸,还有哪些方法可以解决画面拉伸问题? 4. 如何避免全屏预览时出现黑边?</think>### Android SurfaceView 全屏预览 Camera 画面拉伸解决方案 画面拉伸问题的核心在于 **相机预览尺寸与 SurfaceView 宽高比不匹配**。以下是完整解决方案: --- #### 一、核心解决步骤(Camera1 API) 1. **获取屏幕真实分辨率**(非物理尺寸) 在 `surfaceChanged()` 中获取 SurfaceView 的实际宽高: ```java @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // width/height 是 SurfaceView 的实际像素尺寸 adjustPreviewSize(width, height); } ``` 2. **计算目标宽高比** ```java float targetRatio = (float) width / height; // 屏幕宽高比 ``` 3. **选择匹配的预览尺寸** ```java Camera.Parameters params = camera.getParameters(); List<Camera.Size> previewSizes = params.getSupportedPreviewSizes(); Camera.Size bestSize = null; float minRatioDiff = Float.MAX_VALUE; for (Camera.Size size : previewSizes) { float ratio = (float) size.width / size.height; float ratioDiff = Math.abs(ratio - targetRatio); // 选择宽高比最接近的尺寸 if (ratioDiff < minRatioDiff) { minRatioDiff = ratioDiff; bestSize = size; } } ``` 4. **设置预览尺寸** ```java if (bestSize != null) { params.setPreviewSize(bestSize.width, bestSize.height); camera.setParameters(params); } ``` --- #### 二、Camera2 API 额外步骤 1. **在 `onSurfaceChanged()` 中重建会话**: ```java @Override public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) { closeCamera(); openCamera(width, height); // 重新打开相机 } ``` 2. **创建 CaptureSession 时使用新尺寸**: ```java private void createCameraPreviewSession(int width, int height) { StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); Size[] previewSizes = map.getOutputSizes(SurfaceHolder.class); // 选择匹配尺寸的逻辑同上... } ``` --- #### 三、关键注意事项 1. **尺寸选择优先级**: - 第一优先级:宽高比匹配(差值 < 0.01) - 第二优先级:分辨率接近屏幕尺寸 $$ \text{分辨率差异} = |\text{size.width} - \text{width}| + |\text{size.height} - \text{height}| $$ 2. **方向处理**: ```java // 竖屏时固定为 90° 旋转 camera.setDisplayOrientation(90); ``` 3. **黑边处理**(无法避免时): ```xml <!-- 在 SurfaceView 外层包裹 FrameLayout --> <FrameLayout android:background="@color/black"> <SurfaceView android:layout_gravity="center"/> </FrameLayout> ``` --- #### 四、替代方案:改用 TextureView 若 SurfaceView 限制无法解决,可切换为 TextureView 并添加矩阵变换: ```java textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { Matrix matrix = new Matrix(); // 计算缩放比例 (保持比例不变形) float scaleX = (float) previewHeight / width; float scaleY = (float) previewWidth / height; matrix.setScale(scaleX, scaleY, width/2f, height/2f); textureView.setTransform(matrix); } }); ``` 此方案通过视觉缩放避免变形,但实际预览数据仍需按比例匹配[^2]。 --- ### 总结流程图 ```mermaid graph TD A[SurfaceView 创建] --> B{获取屏幕分辨率} B --> C[计算目标宽高比] C --> D[遍历相机支持的预览尺寸] D --> E{是否匹配宽高比?} E -->|是| F[设置该预览尺寸] E -->|否| G[选择最接近的尺寸] G --> H[设置预览尺寸 + 黑边处理] ``` > **关键提示**:不同设备支持的预览尺寸差异较大,建议在 `onCreate()` 中打印 `getSupportedPreviewSizes()` 列表调试[^1]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值