☰
Android Camera2实战:从预览到手动曝光与帧数据采集
2026/10/15 15:29:12 网站建设 项目流程

简介:面向 Android 开发者深入理解 Camera2 API 的完整资料包,围绕相机架构、CaptureRequest 控制、对焦与自动曝光、图像回调、录像、性能优化及兼容性等主题展开,适合已有 Android 基础、希望实现专业拍照或视频功能的中高级开发者。压缩包为 zip 格式,共 1520 个文件,大小约 44.72MB,包含 649 个 xml 界面布局与资源、412 个 png 图标、128 个 json 配置、218 个 class 字节码,以及 aidl 接口、jar 依赖、dex 与 app-debug.apk 可直接安装的示例应用,整体为完整的 Android Studio 工程及构建产物。目前已有 1832 人学习下载。这份资料的价值在于将 Camera2 API 的抽象概念落到可运行代码中,既能查看关键接口与类实现,也能通过示例 APK 直接体验曝光、对焦、白平衡等参数调整效果;对照资源描述的十大知识模块逐项研读,可快速搭建自己的相机应用,并规避厂商兼容性与性能优化中的常见陷阱。

1. 为什么还在折腾Camera2:从灰度预览和手动曝光说起

做扫码、OCR、文档拍摄或者真正的手动相机类App时,你会发现绕不开Android Camera2 API。CameraX确实更省事,但它的封装把HAL级别的曝光补偿、对焦区域、RAW输出、逐帧回调都收进了黑匣子;Camera2正好反过来,它让开发者直接面对CaptureRequest、CaptureSession、ImageReader这套管线,把每一帧的流向和参数变化都摊在明面上。这篇实战笔记写给两类人:一类是新手,想搞清楚Camera2到底怎么组织预览和拍照;另一类是有Camera1或CameraX经验、被设备兼容性逼到需要自己控制底层的熟手。下面从核心概念开始,一路落到可复现的代码和踩坑记录。

2. Camera2核心概念:CaptureRequest如何一步步影响一帧画面

2.1 一张职责表理清相机管线里的角色

Camera2 API最大的入门门槛,是它把相机抽象成了多个对象配合工作的管线。先用一张表把角色和职责划清楚:

角色对应类职责常见误用
相机设备CameraDevice与单个物理/逻辑相机建立连接,负责打开和关闭当成一次性资源用完不close
会话CameraCaptureSession管理一组Surface的输出通道,是提交请求的入口以为切换Surface必须重建会话
请求CaptureRequest描述“接下来这一帧/一串帧怎么拍”的完整参数和输出目标修改后不重新提交,参数不生效
结果CaptureResult报告这一帧实际使用的参数和状态认为结果总是和请求一致
帧数据ImageReader把帧数据曝光到内存缓冲区,供应用直接读取忘记close导致缓冲区耗尽

从这张表能看出一个核心观点:Camera2不是“调个参数、拍一张照片”的接口,而是一个“不断提交请求、不断拿回结果”的循环。请求在某个时间点被消费,结果在稍后的时间点带着真实参数返回,两者之间的对应关系,是理解后面所有代码的关键。

2.2 请求、会话、结果的循环关系

Camera2默认是“重复请求”的工作模式。你可以把CaptureSession想象成一条流水线,CaptureRequest是送进流水线的工单,CameraDevice按工单要求曝光出帧,最后捕获回调把这份工单的执行结果送回应用侧。

我一般会这样组织循环:

// 1. 在会话配置成功后,提交一个持续预览请求 session.setRepeatingRequest(previewRequest, metadataCallback, backgroundHandler); // 2. 在metadataCallback里读取每一帧的真实参数 @Override public void onCaptureCompleted(CaptureSession session, CaptureRequest request, TotalCaptureResult result) { // 注意:这里拿到的曝光时间、ISO、对焦状态才是硬件真正使用的值 long exposureTime = result.get(CaptureResult.SENSOR_EXPOSURE_TIME); int iso = result.get(CaptureResult.SENSOR_SENSITIVITY); }

这段代码的逻辑是:setRepeatingRequest告诉相机“以后每一帧都按这个工单来”,CaptureCallback负责接收每帧执行完的结果。如果你要做一个停顿即拍摄的相机,就再提交一个一次性的TEMPLATE_STILL_CAPTURE请求,拍完再恢复预览请求。

参数说明上要注意两个容易忽略的点:第一,CaptureRequest里设置的曝光、ISO是对HAL的“请求”,HAL有权就近修正;第二个,真正写入JPEG头里的曝光时间,要读CaptureResult而不是读你设置的请求值。调试时我经常遇到“我设置了ISO 800,照片属性里却是640”的情况,原因就是AE还没有完全收敛,HAL做了自动微调。

2.3 模板选型:TEMPLATE_PREVIEW还是TEMPLATE_STILL_CAPTURE

CameraDevice提供了多套预设模板,直接决定了请求的基础参数策略。最常用的是这两个:

// 预览模板:帧率优先,AE/AF持续运转 CaptureRequest.Builder previewBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); // 拍照模板:画质优先,允许更长的曝光时间 CaptureRequest.Builder stillBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);

两套模板的区别不是“预览请求不能拍照”,而是HAL在自动曝光、自动对焦、降噪策略上的偏好完全不同。预览模板会把目标帧率放在前面,曝光时间会被压缩;拍照模板则允许曝光时间变长、噪声优化更积极,代价是帧率低。所以一个常见的错误做法是把预览用的Builder直接拿走去拍照,结果就是照片偏暗或噪声偏多。

选择建议:如果只是做扫一扫、灰度预览、人脸跟踪,用TEMPLATE_PREVIEW就够了;要做文档拍摄或手动摄影,拍照帧必须单独建TEMPLATE_STILL_CAPTURE请求,并且不直接使用预览Surface,而是接到ImageReader的Surface上。TEMPLATE_RECORD适合视频录制、TEMPLATE_ZERO_SHUTTER_LAG适合高速连拍,这两类场景后面再展开。

3. 把预览跑起来:最小可用Camera2项目的完整落地

3.1 权限声明与相机ID选择

Camera2对权限的检查和Camera1不同,它要求同时声明相机权限和可见性声明,而且从Android 9开始,运行时权限申请失败会导致相机打开后立即出现CAMERA_DISCONNECTED。我的做法是打开Activity就先申请Manifest.permission.CAMERA,拒绝就直接禁用入口,不做那种“首次能聊天、二次才能进”的模糊处理。

<uses-feature android:name="android.hardware.camera.any" android:required="true" /> <uses-permission android:name="android.permission.CAMERA" />

相机ID选取上,别写死"0"。多摄机型的ID规则并不统一,有的是0/1代表主摄和广角,有的0就是前置,1才是主摄。我一般这样探测:

CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); for (String cameraId : manager.getCameraIdList()) { CameraCharacteristics chars = manager.getCameraCharacteristics(cameraId); Integer facing = chars.get(CameraCharacteristics.LENS_FACING); // 优先选后置:LENS_FACING_BACK;找不到再回退到任一可用ID if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) { selectedId = cameraId; break; } }

这段逻辑里值得说明的是index和ID并不等价。getCameraIdList()返回的可能是"0"、"1"、"2",但也可能是非连续的数字串。在多摄场景下,厂商对ID顺序并没有统一规范,通过LENS_FACING选择朝向是更稳的方案。如果设备是折叠屏或外接摄像头,ID列表可能更多,但按朝向过滤总是成立的。

3.2 打开相机、创建会话、发起持续预览

最小可用流程分三步:openCamera→createCaptureSession→setRepeatingRequest。下面这段代码把三者串起来,注意回调的时序:onOpened里能创建会话,onConfigured里才允许提交请求。

// 第一步:打开相机,状态回调落在后台线程 manager.openCamera(selectedId, new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice camera) { device = camera; try { // 第二步:把预览Surface装进会话的输出去向 device.createCaptureSession( Arrays.asList(previewSurface, imageReader.getSurface()), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { sessionRef.set(session); buildAndStartPreview(); } }, backgroundHandler ); } catch (CameraAccessException e) { // 处理CAMERA_IN_USE或MAX_CAMERAS_IN_USE } } // onDisconnected / onError 都要回调 }, backgroundHandler); // 第三步:发起重复预览请求 private void buildAndStartPreview() { CaptureRequest.Builder builder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); builder.addTarget(previewSurface); CaptureRequest request = builder.build(); sessionRef.get().setRepeatingRequest(request, callback, backgroundHandler); }

逻辑说明:createCaptureSession的List<Surface>参数决定“这一组会话接受哪些输出目标”,之后再提交的请求必须包含至少其中一个Surface,否则会抛IllegalArgumentException。Camera2不允许给请求添加不在会话列表里的Surface,这一点和直觉相反——很多人以为请求里可以随便加Surface,实际必须先建会话、再建请求。

参数说明有两个点:第一,onConfigured回调可能不在主线程触发,所有UI相关操作记得再post回主线程;第二,setRepeatingRequest的请求与capture的请求差异在于前者会持续接管预览,后者只作用一帧。如果你在拍照后希望回到预览,必须再调用一次setRepeatingRequest。

3.3 方向与旋转:黑屏问题的高发源头

预览黑屏最隐蔽的两个原因,一个是Surface尺寸不匹配,一个是旋转方向错了。先说旋转,前置摄像头输出到TextureView时,如果Activity是竖屏,就需要把预览帧旋转90度或270度展示,否则画面是横的,严重时会直接黑屏。

// 从传感器朝向和显示器旋转计算最终旋转角 int sensorOrientation = chars.get(CameraCharacteristics.SENSOR_ORIENTATION); int displayRotation = getWindowManager().getDefaultDisplay().getRotation(); // 后置:旋转角 = (sensorOrientation - deviceRotation * 因子 + 360) % 360 // 其中deviceRotation把Surface.ROTATION_0/90/180/270换算成度数

常见做法是:后置相机旋转角等于(sensorOrientation - deviceRotationDegrees + 360) % 360,前置要加镜像逻辑,公式变成(sensorOrientation + deviceRotationDegrees + 360) % 360。这里不用硬记,HAL给的sensorOrientation经常是90或270,你只需要保证TextureView.getTransform()里的旋转角和你测出来的最终值一致。

黑屏的第二类原因是Surface配置在会话创建前没准备好。SurfaceTexture需要先设好默认缓冲区尺寸,再创建Surface传进会话。相比SurfaceView,TextureView会在主线程回调缺帧时表现得更加隐晦,黑屏概率也更高。我的习惯是开启setOpaque(false)并监听第一帧的回调做一个“预览已启动”的UI提示,超过两秒没回调就可以断言尺寸或会话配置有问题。

4. 手动控制实战:曝光、对焦与白平衡的参数级拆解

4.1 曝光补偿与ISO:AE模式下先把画面暗下来

手动相机的核心不是“关掉自动”,而是“保留自动引擎、调整它的目标”。大部分场景不需要把AE完全关掉,只调节曝光补偿就够了。Camera2里对应三个参数:CONTROL_AE_EXPOSURE_COMPENSATION、SENSOR_EXPOSURE_TIME、SENSOR_SENSITIVITY。

// 在TEMPLATE_PREVIEW的Builder上叠加手动曝光配置 builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); builder.set(CaptureRequest.CONTROL_AE_LAOCK, false); // 注意拼写,LOCK不是LAOCK // 曝光补偿范围先查后设 Range<Integer> evRange = chars.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); builder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, clamp(evValue, evRange.getLower(), evRange.getUpper()));

逻辑说明:CONTROL_AE_EXPOSURE_COMPENSATION的单位是“步进”,每步一般是1/3 EV,具体步长要读CONTROL_AE_COMPENSATION_STEP。设计上,AE补偿不需要关掉自动曝光,它只改变HAL内部的亮度目标。如果你把曝光补偿从0调到-6,画面看起来会明显变暗,但高光溢出反而减轻,这就是文档拍摄压高光的常见思路。

真正需要关掉AE的场景是夜景长曝光和固定曝光时间连拍。这种时候要把CONTROL_AE_MODE设为CONTROL_AE_MODE_OFF,再手动设置SENSOR_EXPOSURE_TIME和SENSOR_SENSITIVITY,两个值必须同时设置,只设曝光时间不设ISO会得到一张全黑的帧:

builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF); builder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, 33_000_000L); // 33ms builder.set(CaptureRequest.SENSOR_SENSITIVITY, 400); // ISO 400

参数说明里有个容易踩的坑:曝光时间的单位是纳秒,33毫秒要写成33_000_000L,写33f就变成33纳秒,图片几乎全黑。而ISO的取值范围需要读sensorInfo.sensitivityRange,不同设备上限差别很大,从800到102400都有。设置非法值时,请求会在HAL层被拒绝或悄悄修改,调试时很难定位。

4.2 点按对焦:AF_REGIONS的坐标换算与触发时序

相机自动对焦默认是连续对焦(CONTINUOUS_PICTURE),但点按对焦需要你把屏幕坐标换算成sensor坐标,并设置到CONTROL_AF_REGIONS里。Audio作为坐标换算最容易出错的环节,因为Camera2的坐标体系和屏幕方向并不一致。

// 把点击的屏幕坐标,换算成sensor坐标并构造对焦区域 Rect sensorRect = chars.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); float x = (event.getX() / viewWidth) * sensorRect.width() + sensorRect.left; float y = (event.getY() / viewHeight) * sensorRect.height() + sensorRect.top; // MeteringRectangle权重最小为1,设为0会导致区域被忽略 MeteringRectangle focusArea = new MeteringRectangle( (int) (x - 100), (int) (y - 100), 200, 200, 1000); builder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{focusArea}); builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START);

逻辑说明:CONTROL_AF_TRIGGER_START是“触发并锁定”的意思,相机完成对焦后需要再提交一个CONTROL_AF_TRIGGER_CANCEL的请求来解锁,否则之后的连续对焦不会恢复。更精细的做法是监听CaptureResult.CONTROL_AF_STATE,从FOCUSED_LOCKED变成ACTIVE_SCAN代表开始对焦,回到PASSIVE_SCAN后重新设置AF_TRIGGER_IDLE恢复默认。

参数说明:屏幕坐标系和sensor坐标系的比例不是直接映射。如果预览画面经过了旋转,你必须先做坐标更正。检查镜头镜像状态LENS_RADIAL_DISTORTION虽然不能直接坐标换算,但能帮你判断这个机型是否在自拍时做了镜面输出。真正常用的做法是:预览View的宽高比和sensor的宽高比不一致时,按scaleType.CENTER_CROP先裁剪,再手算偏移。

4.3 白平衡与色温:AWB_OFF后的参数边界

白平衡在Camera2里用CONTROL_AWB_MODE控制,手动色温则设置COLOR_CORRECTION_MODE和COLOR_CORRECTION_GAINS。很多资料会把“手动白平衡”错误地等同于“改色温值”,实际Camera2没有直接暴露色温K,而是通过三通道增益控制。

builder.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_OFF); builder.set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_MODE_TRANSFORM_MATRIX); // 三通道增益,典型日光平衡:R=1.8, G=1.0, B=1.4(具体数值要结合设备) RggbChannelVector gains = new RggbChannelVector(1.8f, 1.0f, 1.0f, 1.4f); builder.set(CaptureRequest.COLOR_CORRECTION_GAINS, gains);

逻辑说明:RggbChannelVector四通道分别对应RGGB排列的四个子像素,而不是三个通道。想偏暖就把R通道增益调高、B通道增益调低;想偏冷就反过来。该功能只在AWB关闭时生效,如果你只设了COLOR_CORRECTION_GAINS没关AWB,大多数设备会忽略设定值。

这里的关系是:CONTROL_AWB_MODE_AUTO每帧都会覆盖你设置的增益,所以手动白平衡必须先OFF,再设置色彩校正参数。部分机型还提供CONTROL_AWB_LOCK,它在AWB模式下锁定当前色温,用途是拍摄连续画面时防止白平衡跳跃;如果设备不支持手动增益,锁定AWB是退而求其次的方案,效果稳定但不区分色温方向。

5. 拍照与帧数据采集:ImageReader、YUV转JPEG与RAW输出

5.1 ImageReader的配置与Image关闭时机

ImageReader是应用侧读取帧数据的窗口。预览Surface直连显示器,ImageReader则连内存缓冲区,适合做拍照、分析、算法前处理。它的最关键参数是:图像格式、最大图像数量、宽高。

ImageReader reader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 3); reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireLatestImage(); if (image != null) { processImage(image); // 这里只做数据处理,不做耗时IO image.close(); // 必须立刻关闭,否则缓冲区耗尽后不再回调 } } }, backgroundHandler);

逻辑说明:acquireLatestImage()会丢掉已存在的旧帧、只返回最新一帧,适合实时处理场景;acquireNextImage()按队列顺序返回,适合连拍。无论用哪种,Image.close()必须在处理完数据后同步调用,否则缓冲池被占满,之后的帧回调会完全停止,表现就是预览黑屏、拍照卡死。

参数说明上,maxImages设成3已经能应对大部分场景。设1虽然省内存,但处理速度稍有波动就会丢帧;设5以上在旧设备上容易造成内存压力。需要特别注意的是格式匹配:ImageReader的format必须和CaptureRequest里addTarget的Surface格式一致,常见坑是在TEMPLATE_STILL_CAPTURE里用了JPEG格式Surface,却新建了一个YUV_420_888的ImageReader,系统会直接抛异常。

5.2 YUV_420_888转JPEG:旋转、布局与压缩细节

从ImageReader拿到的YUV_420_888数据,是三个独立的ByteBuffer,分别存储Y、U、V平面。注意它和JPEG编码器的输入格式不完全一致,大多数编码器需要连续的NV12或NV21数据,所以要自己做一次packing。最稳妥的做法是先用矩阵把Y平面保持不变、UV交错,再送给编码器。

// 把YUV_420_888的Y平面和UV平面合并成NV21字节数组 private byte[] yuv420ToNv21(Image image, int width, int height) { Image.Plane[] planes = image.getPlanes(); byte[] nv21 = new byte[width * height * 3 / 2]; // Y平面通常有pixelStride=1,按行拷贝 ByteBuffer yBuffer = planes[0].getBuffer(); int rowStride = planes[0].getRowStride(); // U/V平面的pixelStride常见为2,需要解包 ByteBuffer uBuffer = planes[1].getBuffer(); ByteBuffer vBuffer = planes[2].getBuffer(); // ... 省略逐行解包循环 return nv21; }

逻辑说明:Plane.getRowStride()返回的是行字节数,它可能大于宽度,因为硬件会做对齐。直接按width逐行拷贝会得到错位的画面。U/V平面的pixelStride指相邻同色像素的间隔,通常为2,因此不能把整个uBuffer当成一个紧凑平面。这两个布局参数是YUV转码里最隐蔽的坑,我见过很多开发者用固定分配方式导致颜色边缘发绿。

旋转的处理方式更省事:不用在ByteBuffer层面做旋转,直接等编码完再对JPEG做Matrix旋转。如果是带EXIF方向的Camera2原生JPEG,可以在CaptureResult.JPEG_ORIENTATION里取到方向,把旋转动作交给照片库处理;如果是自己拼的NV21,就绕不开编码前或编码后的旋转。两者的取舍是:前置镜头的镜像必须在编码前处理,否则拍出来的字方向相反。

5.3 RAW与DNG:超出JPEG的后期空间

手动相机App在硬件支持的前提下,输出RAW比直出JPEG更符合“手动”的定位。Camera2里RAW对应ImageFormat.RAW_SENSOR,格式是14位或16位 Bayer数据,不能直接显示,必须先封装成DNG再交给后期软件。

// 创建与RAW_SENSOR匹配的ImageReader ImageReader rawReader = ImageReader.newInstance(w, h, ImageFormat.RAW_SENSOR, 3); // 在STILL_CAPTURE请求里把rawReader.getSurface()加入目标 builder.addTarget(rawReader.getSurface()); // 收到RAW帧后,用DngCreator封装为DNG文件 DngCreator creator = new DngCreator(chars, metadataCaptureResult); FileOutputStream fout = new FileOutputStream(dngFile); creator.writeByteBuffer(fout, image.getPlanes()[0].getBuffer()); creator.close(); image.close();

逻辑说明:DngCreator是系统提供的DNG编码器,把RAW数据结合相机特性参数转换成标准DNG格式,前提是传入的CaptureResult是这份RAW帧对应的结果,不能拿几秒前的预览结果来代替。写入时要传入单个Plane的Buffer,而不是三平面。如果你发现DNG在PC端打开是全黑,往往是ISO或曝光时间设置在RAW请求里没有生效,也可能你传入了错误的TotalCaptureResult。

这里有一个特征值需要核对:LENS_INFO_AVAILABLE_FOCAL_LENGTHS和SENSOR_INFO_COLOR_FILTER_ARRANGEMENT在DNG写入时由chars自动带入,但如果设备本身把RAW定义为YCbCr + Bayer混合格式,就要看REQUEST_AVAILABLE_CAPABILITIES里是否有RAW能力。没有这个capability的旧设备不会响应RAW_SENSOR格式,创建ImageReader时直接抛IllegalArgumentException。

6. 兼容性避坑指南:从黑屏到崩溃的排查实战

6.1 打开相机失败:CAMERA_IN_USE 与 MAX_CAMERAS_IN_USE 的差异

现象:调用openCamera后立刻回调onError,错误码为ERROR_CAMERA_IN_USE或ERROR_MAX_CAMERAS_IN_USE,有时还有ERROR_CAMERA_DISCONNECTED。

原因:CAMERA_IN_USE表示这台相机被别的进程占用;MAX_CAMERAS_IN_USE表示当前进程请求的相机ID总数超过设备上限,比如部分设备允许同时打开3路摄像头,但你的App已经打开了前置又去开主摄,就可能触发后者。CAMERA_DISCONNECTED则通常是系统相机进程异常退出或设备被拔出。

解决:先检查Camera2内部没有重复打开,也就是CameraDevice.StateCallback的onOpened与onDisconnected成对出现。收到CAMERA_IN_USE时,用CameraManager.AvailabilityCallback监听onCameraAvailable,等资源释放后再重试;收到MAX_CAMERAS_IN_USE时,不要立即重试,先确认是否当前进程占用了其他相机。如果是USB摄像头热插拔导致DISCONNECTED,只能提示用户重新插拔,代码层面无法恢复。

6.2 会话创建失败与尺寸适配

现象:createCaptureSession回调onConfigureFailed,logcat里能看到Stream configuration failed或类似的硬件层报错;另一种隐蔽现象是会话创建成功,但预览画面只有上半部分或整屏绿条。

原因:最常见的是把预览尺寸设成了sensor原始尺寸,比如选了一个StreamConfigurationMap里不存在的组合;或者同一个Surface同时被预览和ImageReader以两种尺寸使用。Camera2的HAL会对输出组合做合法性限制,部分设备要求预览输出的宽高比和JPEG保持一致,否则会话配置失败。

解决:不要自己枚举尺寸,直接从StreamConfigurationMap.getOutputSizes(SurfaceTexture.class)里取可用列表,先按比例匹配预览View的宽高,再从中选最大的一组。选尺寸的顺序应该是——先过滤掉宽高小于480P的,再按16:9或4:3匹配,最后选最大;记住getOutputSizes()返回的顺序不保证分辨率从大到小,必须自己排序。

6.3 LEGACY级别的特性边界与硬件能力检测

现象:同一份代码在A机支持手动ISO、RAW、电子防抖,在B机上调用对应参数直接无效果,CaptureResult里的值没有变化。

原因:INFO_SUPPORTED_HARDWARE_LEVEL定义了LIMITED和FULL的区别,LIMITED设备只支持部分手动控制,LEGACY级别更少。很多“祖传”中低端设备标称支持Camera2,实际只有LEGACY级别,能开CameraSession,但不能提供RAW、传感器手动曝光等能力。

解决:所有进阶功能都要先读能力集再暴露。启动时做一次能力检测:

int level = chars.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); if (level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL) { // 可以展示手动曝光、RAW等高级功能 } else if (level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED) { // 只展示基础控制项,蒙版手动曝光滑块 }

同级设备之间还存在“名义支持但实际无效”的差异。一个可靠的做法是每次打开相机后提交一个测试请求,等收到CaptureResult后判断SENSOR_EXPOSURE_TIME是否确实改变了;没改变就在UI层隐藏对应控件,而不是报错闪退。这个探测开销很低,整个过程在后台Handler上执行,用户基本无感。

6.4 每次接入新机型时强制走一遍的三个动作

兼容性问题的本质是没有在真机上验证。我自己带过太多项目在模拟器上跑得好好的,换到实体设备就崩。接入新机型,我强制走这三步:

第一步,先打印INFO_SUPPORTED_HARDWARE_LEVEL和所有CONTROL_AE_COMPENSATION_RANGE到日志,确认设备声称的能力边界。第二步,用后台Handler实现一个“打开相机→建会话→取一帧→关闭”的自动测试,任何异常都要留字段做记录,不弹窗。第三步,验证旋转和镜像:拍一张有字的白纸,检查预览方向和拍照方向是否一致。这三步之外,还有一个实用技巧:利用系统自带的在线相机能力查看器,这类工具直接读取CameraCharacteristics全部参数,能迅速定位是代码问题还是HAL问题。

从那以后,我每次支持新设备都不再问“为什么模拟器正常”,而是默认“真机上一定有某个尺寸或参数是特殊的”,把上面三步走完再合代码。多花十分钟,能省掉用户反馈里大量的“打开闪退”“画面倒着”这类问题。希望这篇笔记能帮你绕过我已经踩过的坑,也欢迎在实际调整参数时对照自己的设备日志观察变化规律。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询