ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android摄像头开发实战:CameraX/Camera2选型与踩坑指南

Android摄像头开发实战:CameraX/Camera2选型与踩坑指南

简介:面向Android开发者的摄像头功能实现资料包,围绕Camera1与Camera2两代API,深入覆盖设备打开与参数配置、SurfaceView与TextureView预览显示、拍照与视频录制、运行时权限申请、画面旋转校正、多摄像头切换、闪光灯控制、面部检测等核心场景,既适合刚接触相机开发的初学者按示例搭建基础功能,也适合中高级开发者研究Camera2高级控制与YUV原始数据处理的实现思路。全套源码含470个文件,以Java源码、XML资源配置、PNG界面图片及编译后的class文件为主,另附Gradle工程配置和Git版本管理文件,压缩包仅4.69MB,整体结构清晰,便于按模块检索与复用。已有6093人学习,读者可重点参考其中权限请求与兼容处理、Camera2多路流并行、闪光灯与面部检测的触发逻辑,直接移植到实际项目中作为自定义相机或图像采集模块的改造起点。

1. 安卓摄像头开发到底在做什么:先分清「调通」和「做全」

很多人拿到一个「Android摄像头软件所有功能」这样的标题,第一反应是打开 Android 官方文档,把 Camera2 的十几个回调全抄一遍,然后发现预览是黑的、拍照是绿的、录像没声音、插上耳机后对焦声和快门声全没了。这个标题表面上是「硬件能力的调用清单」,实际上是一整套软件工程:把光路变成数据,把数据变成帧,把帧变成文件,再把文件变成用户能看懂的预览和相册内容。你能在这个标题下找到自己的位置,通常意味着你正在做一个相机类 App、巡检记录工具、扫码或 AR 场景应用,甚至是远程视频采集的前端。

我先把话说透:Android 摄像头软件功能不是一个 API 能覆盖完的。它至少横跨 CameraX 或 Camera2 的选型、预览与输出通道分离、对焦曝光策略、图像方向与 EXIF 修正、前后摄切换的兼容策略、录像与拍照的状态机切换、以及一颗亮灯(闪光灯)的控制时机。这篇文章不打算把每个功能都贴一遍官方示例,而是按一条可落地的路径推进:先讲清楚选型和工程骨架,再逐个拆解成像控制、对焦曝光、方向修正、多摄策略这些「软件功能」的真实触点,最后单独用一章说踩坑和排查。你不需要先读完全部文档再动手,跟着每一章的代码和参数说明走,就能在真实设备上逐步验证。

2. CameraX 还是 Camera2:先选对 API 层,再谈功能全集

2.1 两者的能力边界:官方支持力度与你设备的真实上限

Android 摄像头软件功能这件事,水面上是你要做什么,水面下是 API 层决定了你能控制到什么粒度。Camera2 API 是 Camera HAL3 的直接映射,意味着你能拿到每一帧的曝光时间、ISO、光圈、传感器时间戳、是否处于 AE/AF 收敛状态,甚至能通过 CaptureSession 设置自定义的 3A 策略。CameraX 则是在 Camera2 之上做了一层 use case 抽象,把ImageCapture、Preview、VideoCapture封装成自带生命周期的组件。选择的第一原则不是「哪个新用哪个」,而是「你需要控制到什么深度」。

对比维度CameraXCamera2
预览与拍照集成开箱即用,绑定生命周期需要手动管理 Session 和 Surface
对焦/曝光回调有分析器,但粒度中等每个请求都能带 AE/AF 触发器和状态回调
多摄与逻辑摄像头支持后置/前置切换,逻辑摄像头跟随系统策略可以指定物理 ID,但发现物理摄像头依赖 CameraManager
录像与拍照并发VideoCapture + ImageCapture 可同时绑定需要自行设计 ReprocessSession 或两个 Session 切换
帧级定制受限,适合常规场景可做手动曝光、RAW、HDR 多帧合成
维护成本低,系统 API 兼容性好高,国产机型碎片化明显

我一般会这样建议:如果你做的是扫码工具、标准拍照 App、巡检拍照记录,CameraX 是效率最高的;如果你是做直播推流、老式设备兼容、需要手动感光度或帧级降噪的,Camera2 才是正路。注意一个细节:CameraX 底层仍然是 Camera2,所以你通过Camera2CameraControl把 CameraX 的CameraControl强转成 Camera2 的 interop 接口,在某些机型上可以拿到更细的控制能力。这是一个合规的逃生通道。

2.2 CameraX 初始化工程骨架:一个能跑通预览拍照的最小代码

下面这段代码是一个通用的 CameraX 绑定骨架,我把关键步骤都注释出来了。它解决的是「摄像头软件所有功能」中最底层的问题:生命周期、预览、拍照三件事的同步。注意:CameraX 的绑定是基于ProcessCameraProvider的单例,每个生命周期只能绑定一次,重复绑定会抛IllegalArgumentException。

// 建议在 Activity 中实现 LifecycleOwner,或使用 AppCompatActivity public class CameraXSetup { private Preview mPreview; private ImageCapture mImageCapture; private VideoCapture mVideoCapture; // 需要额外依赖 camera-video 扩展库 // 初始化:获取 CameraProvider 是异步操作,回调中再做绑定 public void startCamera(@NonNull LifecycleOwner owner, @NonNull ListenableFuture<ProcessCameraProvider> cameraProviderFuture, @NonNull PreviewView previewView) { cameraProviderFuture.addListener(() -> { try { ProcessCameraProvider provider = cameraProviderFuture.get(); // 构建 Preview use case:目标分辨率优先选 720p,降低配准与内存压力 Preview.Builder previewBuilder = new Preview.Builder() .setTargetResolution(new Size(1280, 720)); // 注意不是屏幕分辨率 mPreview = previewBuilder.build(); mPreview.setSurfaceProvider(previewView.getSurfaceProvider()); // 构建 ImageCapture use case:用 JPEG 格式,无需额外编解码 ImageCapture.Builder captureBuilder = new ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // 追求快门响应速度 .setFlashMode(ImageCapture.FLASH_MODE_AUTO) // 自动闪光,后文讲手动控制 .setJpegQuality(90); mImageCapture = captureBuilder.build(); // 选择后置摄像头;如果要兼容逻辑摄像头,默认选默认后置即可 CameraSelector selector = CameraSelector.DEFAULT_BACK_CAMERA; // 解绑旧 use case,避免页面二次进来时冲突 provider.unbindAll(); provider.bindToLifecycle(owner, selector, mPreview, mImageCapture); } catch (Exception e) { // 常见异常:摄像头被占用(其他 App 常驻)、设备不支持 CameraX // CameraX 在部分低端机上 bind 失败是常态,需做降级处理 Log.e("CameraX", "bind failed: " + e.getMessage()); } }, ContextCompat.getMainExecutor(context)); } // 拍照:takePicture 的回调需要立刻处理或转码,不要持有 Bitmap public void takePhoto(@NonNull File outputFile) { ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(outputFile).build(); mImageCapture.takePicture(options, ContextCompat.getMainExecutor(context), new ImageCapture.OnImageSavedCallback() { @Override public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) { // 这里只是「文件已落盘」,方向修正和 EXIF 在后面章节处理 } @Override public void onError(@NonNull ImageCaptureException exception) { // 常见错误码:ERROR_CAPTURE_FAILED / ERROR_CAMERA_CLOSED / ERROR_INVALID_CAMERA } }); } }

代码里的逻辑说明:setTargetResolution不是最终输出分辨率,而是让 CameraX 去选择「最接近这个目标」的可用流配置。你如果传Size(1920, 1080),在一些只有 4:3 或 16:9 非标准的摄像头上,实际预览可能是 1600×720 或其他值。CAPTURE_MODE_MINIMIZE_LATENCY优先快门响应,利于抓拍,但在暗光下会造成噪点更明显;需要画质优先就换成CAPTURE_MODE_MAXIMIZE_QUALITY。provider.unbindAll()是防止页面重建后重复绑定黑屏的关键,很多初级项目翻车就在没做这一步。

2.3 Camera2 手动链路:从 CameraManager 到 CaptureSession

如果你选择了 Camera2,最小链路是四步:打开摄像头、建立CaptureSession、创建CaptureRequest、循环取帧。我不建议从零手写CameraDevice.StateCallback和CameraCaptureSession.StateCallback,因为人很容易在回调嵌套里搞混线程。更稳的做法是写一个基于HandlerThread的状态机,把打开、配置、Session 建立三个状态显式地放在一个队列里。

# 注意:Android 摄像头逻辑都是 Java/Kotlin,这里用一个「键值表」说明 Camera2 链路中必须设置的元数据字段 # 实际代码请参考 CameraManager.openCamera + CameraDevice.createCaptureSession { "camera_id": "0,即后置主摄(实际是 CameraManager.getCameraIdList()[0] 返回的字符串)", "surface_list": "[预览 SurfaceTexture, ImageReader.Surface],两个 Surface 必须同属一个 session", "capture_request_template": "TEMPLATE_PREVIEW 用于预览/manual 用于手动控制/STILL_CAPTURE 用于拍照", "ae_mode": "CONTROL_AE_MODE_ON_AUTO_FLASH 或 CONTROL_AE_MODE_OFF 手动曝光", "af_mode": "CONTROL_AF_MODE_CONTINUOUS_PICTURE 用于拍照,VIDEO 用于录像", "jpeg_orientation": "依赖传感器方向值,不能直接用屏幕方向" }

参数说明:TEMPLATE_PREVIEW在大多数设备上默认开启 3A,也就是自动对焦、自动曝光、自动白平衡;你只需要在预览请求上设置CONTROL_AF_MODE_CONTINUOUS_PICTURE,就能得到一个连续对焦的预览画面。拍照时则需要重新提交一个TEMPLATE_STILL_CAPTURE请求,并且等待CONTROL_AE_STATE_CONVERGED再触发捕获,否则拍出来的照片曝光不稳定。这个「连续对焦预览 + 收敛后捕获」的组合是 Camera2 软件功能中最核心的交互设计,新手容易一上来就想手动调 AF,反而把自动策略弄坏了。

3. 分辨率、方向与 EXIF:三个最容易让「所有功能」翻车的硬参数

3.1 预览变形不是摄像头的问题,是分辨率配错

很多人在「Android摄像头软件所有功能」这个需求里反复改预览布局,发现图像被拉伸或裁剪,然后怀疑摄像头坏了。其实绝大多数情况是PreviewView的布局尺寸和预览流尺寸比例不一致。CameraX 的PreviewView提供setScaleType来控制填充方式,但真正优雅的做法是让画面充满且不裁切,你需要知道系统选出来的预览流实际尺寸。用一个Preview.SurfaceProvider回调去拿分辨率:

// 通过 surface provider 拿到实际预览尺寸,再反过来调整 UI previewView.surfaceProvider = object : SurfaceProvider { override fun onSurfaceProvided(completionStage: SurfaceRequest, surface: Surface) { // surfaceRequest 内部持有分辨率信息;此时拿到的是 CameraX 调整后的真实尺寸 surface.request.updateTransform() // 处理旋转 } }

我通常的做法是在应用启动时用CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP列出所有输出尺寸,再选择长宽比最接近 4:3 或 16:9 且面积最大的一组。注意:若是用 Camera2 手动链路,ImageReader的尺寸必须包含在 stream configuration map 里,否则createCaptureSession直接抛IllegalArgumentException。另外还有一个低概率坑:某些设备的摄像头传感器是横置的,也就是传感器的行方向与屏幕自然方向相差 90 度,此时SENSOR_ORIENTATION的值是 90 或 270,需要按后置和前置分别做旋转补偿。

3.2 EXIF 方向修正:拍照文件旋转错误的真凶

你以为你把JPEG_ORIENTATION设置为屏幕旋转角度就对了,但你会发现前置摄像头拍出来的照片是镜像翻过来再转了 90 度的。原因是前置传感器方向 + 镜像标志需要合成。修正 EXIF 方向有三个要素:传感器方向、摄像头前后、屏幕当前旋转。执行方式很简单,但在代码里必须严格做一次合成计算:

// 计算 JPEG 方向:input 为传感器朝向,isFront 为前置标志,displayRotation 为 UI 旋转角度 public static int computeJpegRotation(int sensorOrientation, boolean isFront, int displayRotation) { int result; if (isFront) { // 前置摄像头需要镜像处理,所以旋转公式是 (sensorOrientation + displayRotation) % 360 result = (sensorOrientation + displayRotation) % 360; } else { // 后置简单做差: (sensorOrientation - displayRotation + 360) % 360 result = (sensorOrientation - displayRotation + 360) % 360; } return result; // 这个值直接填入 CaptureRequest.JPEG_ORIENTATION }

参数说明:sensorOrientation是由相机特性SENSOR_ORIENTATION给出的固定信息,绝大多数后置是 90,前置是 270;displayRotation则是WindowManager.getDefaultDisplay().getRotation()换算成的 0/90/180/270,不是你 SurfaceView 的旋转角度。如果拍出来的照片在相册里依然横竖不对,建议先做一次实验:固定手机竖屏、拍一张白底黑字图片,然后导出 JPEG 查看其 EXIF 里的Orientation字段。实现过多次之后,你会形成直觉:大部分 Android 设备的后置摄像头 JPEG 方向等于sensorOrientation,前置等于 0 或 180,但华为和三星的若干机型会有差异,只能在真机测试表里记录。

3.3 录像与预览的方向保持:录像出来的视频旋转像没法修正

录像和拍照最大的不同是:视频文件本身没有 EXIF 方向可以存,最终播放器是靠MediaFormat的rotation字段来判断的。因此你在MediaRecorder或者VideoCapture输出时,必须在编码参数里直接设置多少度旋转。CameraX 的VideoCapture会在内部自动读取传感器和 Display 的旋转并写入,但当你使用 Camera2 + MediaCodec 做自定义录制时,需要自己处理。

// MediaRecorder 手动路径的关键设置:方向与预览保持一致 mediaRecorder.setOrientationHint(computeJpegRotation(sensorOrientation, isFront, displayRotation))

这里只说一个常见误用:很多人把setOrientationHint设成了固定 90,在横屏录制时 OK,但竖屏录制时视频在电脑上是躺着的。正确做法是启用一个OrientationEventListener,在屏幕旋转时实时重起 MediaRecorder,或者接受「录制中旋转方向不会中途改变」的限制,至少保证录制开始时是正的。你要是做的是一个需要持续录制几分钟的工具软件,屏幕上最好放一个「旋转锁定」提示,避免录到一半画面翻转成 90 度,处理起来比拍照 EXIF 麻烦得多。

4. 对焦、曝光与灯光控制:把硬件参数的「软件开关」做全

4.1 自动对焦和手动对焦的协同:三种触发模式

「摄像头软件所有功能」里的对焦,不能只写一个CONTROL_AF_MODE_AUTO,因为用户会点屏幕触发对焦,会回到连续对焦模式,会手动拉焦距(数码变焦),还有可能扫码时想锁定对焦。对焦控制本质上是一个三态状态机:连续对焦、单次对焦、锁定对焦。

用户操作AF 模式实现方式
预览启动CONTINUOUS_PICTURECaptureRequest 默认设置
点击屏幕对焦AUTO + AF_TRIGGER_START触发CONTROL_AF_TRIGGER_START,等待AE/AF_STATE收敛
长按锁定对焦AUTO + AF_TRIGGER_CANCEL 后置为 CONTINUOUS 并取消触发设置CONTROL_AF_REGIONS固定到触摸区域
扫码/文档场景MACRO 模式(部分设备支持)先查询CONTROL_AF_AVAILABLE_MODES

在实际的 CameraX 路径里,你用CameraControl.startFocusAndMetering传入一个FocusMeteringAction,它可以同时指定对焦区域和测光区域。这个动作的点是:FocusMeteringAction.Builder支持setAutoCancelDuration,我建议设置成 800ms,意思是点在屏幕上对焦成功后的 0.8 秒内不启动连续对焦,避免一松手画面立刻重新糊掉。不少项目这个参数设置太短,用户对焦还没看清,画面又自动对到别处去了,体验非常糟糕。

4.2 曝光控制:AE 状态机比手动设置感光度更重要

曝光控制里 80% 的坑不在数值设置上,而在「什么时候允许设置、什么时候读回状态」。Camera2 的 AE 是异步收敛状态机,典型的触发序列是:INACTIVE -> SEARCHING -> CONVERGED(或FLASH_REQUIRED)。当你手动设置曝光补偿时,如果不在CONVERGED状态就下发,有些设备会忽略这次设置或者产生闪烁。

// 通过 CaptureResult 检查 AE/AF 收敛状态的推荐路径 if (result.get(CaptureResult.CONTROL_AE_STATE) == CaptureResult.CONTROL_AE_STATE_CONVERGED) { // 此时用户滑动曝光补偿条才实际生效,UI 可以放开亮度调节滑块 }

曝光补偿的范围用CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE查询,通常为 -12 到 +12(步长 1);别硬编码 -10 到 +10,部分设备是 -8 到 +8。还有一个规律:曝光补偿步长是CONTROL_AE_COMPENSATION_STEP,默认是 0.5 EV 或 1 EV,你滑块的每个格都应该是这个值的整数倍,否则相邻两档亮度变化不均。如果你做的是专业相机模式,还需要注意CONTROL_AE_LOCK与手动感光度的互斥——开了 AE_LOCK 再设置SENSOR_EXPOSURE_TIME会被无视,因为 AE 策略还在控制它。

4.3 闪光灯不是「打开这么简单」:AUTO 闪光的失败循环和 LED 手电筒

闪光灯在 CameraX 里有三种状态:FLASH_MODE_AUTO、FLASH_MODE_ON、FLASH_MODE_OFF。很多人忽略了一个致命逻辑:AUTO 闪光模式下,设备是靠 AE 测光结果「事后」决定闪不闪的,也就是说你在暗处按快门会感觉快门延迟变长,然后拍出来的照片往往欠曝或过曝。这是因为CONTROL_AE_MODE_ON_AUTO_FLASH驱动 AE 在闪光前先做一次预闪测光。如果你想做「所见即所得」的抓拍体验,在暗光下建议手动把闪光模式改成ON,同时接受高光溢出的风险。

LED 手电筒则要绕开相机会话:直接用CameraManager.setTorchMode(cameraId, true)。注意这与相机会话是抢独占关系——如果相机已经在预览,手动打开手电筒会要求你的 App 释放 Camera 设备或至少释放预览链路,否则会出现无法打开、提示 Camera in use 的情况。有人想边录像边开手电筒,这是可能的,做法是在录像会话里设置FLASH_MODE_TORCH,而不是调手电筒系统接口。

// 录像同时强制打开 LED:FLASH_MODE_TORCH,注意要在录像启动前设置 val request = cameraDevice.createCaptureRequest(TEMPLATE_RECORD) request.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH)

参数说明:TEMPLATE_RECORD本身会优化为持续录像的配置,比手动TEMPLATE_PREVIEW设置FLASH_MODE_TORCH更稳,因为系统知道后面的 session 会持续使用这个请求。而且用完成后要立刻发一个FLASH_MODE_OFF的新请求,否则到了预览 state 里灯光还亮着,就会有「预览正常、开关无效」的诡异现象。

4.4 数码变焦、缩放和点多摄切换:软件功能的最后一公里

数码变焦的实质是裁剪传感器输出区域。CameraX 的CameraControl.setZoomRatio或 Camera2 的CONTROL_ZOOM_REGION,二者都是设置 crop region。在「所有功能」的视角里,这个特性是最容易做但不一定被做对的:很多人直接在 UI 上让用户拖动zoomRatio,但忘了变焦后的画面清晰度下限、以及变焦后对焦区域需要重新映射。

当前 Android 的多摄是「逻辑摄像头」概念,也就是说相机列表里的 0 号 ID 可能不是物理主摄,而是一个融合了广角 + 主摄 + 长焦逻辑设备。你单纯去遍历CameraManager.getCameraIdList()找LENS_FACING_BACK会漏掉超广角和长焦。正确做法是看CameraCharacteristics.LOGICAL_MULTI_CAMERA和LENS_INFO_AVAILABLE_FOCAL_LENGTHS;如果某个逻辑摄像头包含多种焦距但 physical ids 输出不同,那么你就是通过setPhysicalCameraId(CameraX 的Camera2CameraControlinterop 支持)或者 Camera2 的 capture request 里指定物理 ID 来切换镜头。

5. 摄像头软件功能避坑:预览黑屏、拍照绿图、录像无声、权限反复弹

5.1 预览黑屏:Surface 时序错位和 CameraX unbind 漂移

现象:预览区域一片黑,日志没有致命错误,CameraDevice.StateCallback已经回调到opened,但画面始终不显示。

原因:绝大多数情况是 Surface 在 CapSession 创建前没有被有效配置,或者 Surface 已经失效(Activity 重建)但系统仍持有旧引用。Camerax 在bindToLifecycle时如果传入的PreviewView还没有 attach 到 window,SurfaceProvider拿到的 SurfaceRequest 会一直处于 pending 状态,你的预览自然黑屏。

解决:在onResume之后再执行startCamera,并确保PreviewView宽高已被布局框架测量。调试时用previewView.isLaidOut()做判空。一个习惯很重要:执行unbindAll()后要间隔一个主线程循环再去绑定,CameraX内部释放旧 session 是异步的,连续绑定太快会黑屏一时再恢复。

5.2 拍照绿图/半边花屏:ImageReader 格式或 JPEG 尺寸异常

现象:拍出来的 JPEG 文件打开是绿色的,或者上半部分正常下半部分花屏;用 ImageReader 拿字节数组后转 Bitmap 颜色错乱。

原因:ImageReader的ImageFormat设置成了RAW_SENSOR却按YUV_420_888解析;或者你拿到的Imageplane 数组没有按 rowStride 去复制。另一个常见情况是 JPEG 捕获请求里同时指定了JPEG_QUALITY和JPEG_ORIENTATION但实际流配置里没声明ImageFormat.JPEG用的尺寸,系统只能输出一个代理尺寸,字节长度对不上。

解决:统一使用ImageFormat.JPEG作为 output 格式,ImageReader的宽高明确和StreamConfigurationMap对齐。如果是 YUV 数据,不要用默认的data[0].limit()去拷贝,用rowStride * height计算实际数据长度;遇到MediaCodec的输入 buffer 花屏则还要确认COLOR_FormatYUV420Flexible和实际 plane 的对齐方式。

5.3 录像没有声音:AudioSource 或权限时序被摄像头的 Surface 创建抢先

现象:录像文件播放画面正常,但音轨是空,或录到一半声音突然消失。

原因:MediaRecorder 的setAudioSource和setVideoSource顺序写反是常见翻车事件;更隐蔽的是录音权限在录制启动时才动态弹出,用户点「允许」的那几秒内 MediaRecorder 已经 start,之后虽然授权成功但录音源已经失效。还有一个常见问题:很多国产设备在做「相机与麦克风同时使用」时,需要在 manifest 里声明android.permission.CAMERA和android.permission.RECORD_AUDIO;但 Android 12 以上「麦克风访问」有单独的「仅在使用时允许」开关,如果用户在系统设置里改成了「每次询问」,你的程序进入前台时不会收到权限回调但麦克风实际是关闭的。

解决:动态请求权限时,把 CAMERA 与 RECORD_AUDIO 放到同一个请求列表,等所有授予后再MediaRecorder实例化。录制中途如果检测到MediaRecorder的onInfo(what=MEDIA_RECORDER_INFO_MAX_DURATION_REACHED)或错误回调,应当立即重置 recorder 并告知用户「录制中断」,而不是静默吞掉。

5.4 相机权限已授予但扫码界面依然黑屏:隐私指示器与「摄像头占用」

现象:App 有相机权限、onResume 正常执行、CameraManager 能列出摄像头,但 bindToLifecycle 失败,日志提示LEGACY_CAMERA_DEVICE或CAMERA_DISCONNECTED。

原因:某些 Android 13+ 设备在用户下拉快捷开关里开启了「摄像头访问」禁用;或者另一个 App 持有相机而目标设备不支持并发会话。还有相当一部分情况是安全软件或系统级扫码组件占据了摄像头。

解决:在绑定失败时用CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL进行检查;同时监听ACTION_CAMERA_DISCONNECTED广播(部分设备可用),提示用户关闭其他占用相机的 App。如果硬件级别是LEGACY,请降级到 Camera1 API 或放宽分辨率要求——这是你的 App 与系统性兼容的兜底方案,不是玄学。

5.5 前置摄像头画面左右翻转:镜像是拍照预览的「正确但不合适」

现象:用前置摄像头预览时画面和你照镜子一致,但保存的 JPEG 却是左右反的;或反过来预览是镜像但文件是正的。

原因:Android 系统没有强制前置预览镜像;SCALER_CROP_REGION和传感器读出的方向不会因为前置而自动翻转。Camera2 下你看到的镜像效果是很多 App 额外做的预览层 Matrix 镜像,而不是系统输出。文件方向则只看 JPEG_ORIENTATION 值。

解决:如果产品要求「所见即所得」,请只在预览层做镜像翻转(PreviewView.setScaleX(-1)或TextureView的 Matrix 设置),文件输出保持方向提示正确。千万别尝试在 EXIF 里写镜像标志,因为浏览器和图库的 EXIF 方向支持程度并不一致。这个需求最常出现在「自拍美颜 + 实时预览」方案里,前后测样张确认一次即可。

6. 最后一公里:帧率、延迟与性能指标的验证技巧

功能做到「有画面、能拍照」不难,难的是跟同类型 App 比,你的点击到快门延迟多 200ms、预览帧率掉到 15fps、录像时手机烫得厉害。验证不要靠肉眼,用 adb 与性能分析工具把指标打出来。我会用三个抓手:帧率统计、打开相机的冷启动时长、以及拍照到结果回调的链路耗时。

# 帧率统计:基于 gfxinfo 的输出可以看到 janky frames 与总帧数 adb shell dumpsys gfxinfo <your.package.name> | grep "Total frames" -A 5 # 拍照链路耗时:建议在代码里打点,onCaptureStarted -> onCaptureCompleted 就是真实快门延迟 adb logcat -s CameraPerf | grep "SHUTTER_LATENCY"

另一个被开发者忽略的性能指标是预览流的格式。CameraX 默认预览是YUV_420_888,如果你把 UI 放在了低端机上,setTargetResolution(1280, 720)与PreviewView的PerformanceMode组合可以压出更高的帧率。把Preview.Builder.setPerformanceMode(Preview.PERFORMANCE_MODE_FASTEST)设置上,并且把预览的分辨率控制在 1080p 以下,这是低端机流畅度的关键。

我保持着一个小习惯:每次改完摄像头相关逻辑都会用「冷启动 + 自动对焦 + 连拍三张 + 退出页面」做一轮验证,把dumpsys media.camera的底层日志保存一份。这些日志里包含摄像头打开到关闭的完整记录,也是排查「某机型崩溃」时的第一手证据。希望这篇关于 Android 摄像头软件功能全集的整理,能帮你少走几段弯路,也希望你在真机上验证每一条参数——摄像头这层,真机永远比文档诚实。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进