Android Camera开发详解
一、Camera 发展历史与三代架构
1.1 三代架构总览
| Camera1(已废弃) | Camera2(现行标准) | CameraX(Google 推荐) | |
|---|---|---|---|
| 推出时间 | Android 2.3(API 1) | Android 5.0(API 21) | Android 10+(API 24+) |
| 封装层级 | 高度封装,简单但受限 | 低级 API,全功能但复杂 | 高级封装,简单且统一 |
| 回调方式 | Camera.PreviewCallback |
ImageReader + CaptureSession |
Observer 模式 |
| 生命周期 | 需手动管理 | 需手动管理 | 自动绑定 LifecycleOwner |
| 配置复用 | 困难 | 手动配置每个请求 | 自动复用配置 |
| 厂商适配 | 厂商负责 | 厂商负责(碎片化严重) | Google 官方兜底适配 |
| 视频拍摄 | MediaRecorder |
MediaRecorder / ImageReader |
VideoCapture |
二、Camera1 API
2.1 核心流程
undefined
SurfaceView / TextureView
↓
Camera.open() → Camera
↓
Camera.setPreviewDisplay() / setPreviewTexture()
↓
Camera.startPreview()
↓
Camera.takePicture() / Camera.PreviewCallback
↓
Camera.stopPreview()
↓
Camera.release()
2.2 完整代码示例
java
public class Camera1Activity extends AppCompatActivity
implements TextureView.SurfaceTextureListener {
private Camera mCamera;
private TextureView mTextureView;
private boolean mPreviewRunning = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mTextureView = new TextureView(this);
mTextureView.setSurfaceTextureListener(this);
setContentView(mTextureView);
}
// TextureView 准备好后打开摄像头
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int w, int h) {
mCamera = Camera.open(); // 默认打开后置摄像头
try {
Camera.Parameters params = mCamera.getParameters();
// 设置预览尺寸
List<Camera.Size> supportedSizes = params.getSupportedPreviewSizes();
Camera.Size optimalSize = getOptimalPreviewSize(supportedSizes, w, h);
params.setPreviewSize(optimalSize.width, optimalSize.height);
// 设置拍照尺寸
List<Camera.Size> supportedPicSizes = params.getSupportedPictureSizes();
Camera.Size picSize = supportedPicSizes.get(0); // 通常最大尺寸在最后
params.setPictureSize(picSize.width, picSize.height);
// 设置对焦模式(自动对焦)
List<String> focusModes = params.getSupportedFocusModes();
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
}
mCamera.setParameters(params);
mCamera.setPreviewTexture(surface);
mCamera.startPreview();
mPreviewRunning = true;
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int w, int h) {}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
if (mCamera != null) {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
return true;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {}
// ════════════════ 拍照 ════════════════
public void takePicture() {
if (mCamera != null) {
mCamera.takePicture(null, null, (data, camera) -> {
// data 即 JPEG 字节数组,保存到文件
FileOutputStream fos = null;
try {
fos = new FileOutputStream("/sdcard/photo.jpg");
fos.write(data);
fos.close();
Log.d("Camera1", "Photo saved: " + data.length + " bytes");
} catch (IOException e) {
e.printStackTrace();
}
// 拍完继续预览
camera.startPreview();
});
}
}
// ════════════════ 预览回调(实时帧处理)═══════════════
public void startPreviewCallback() {
mCamera.setPreviewCallback((data, camera) -> {
// data 是 NV21/YV12 格式的原始预览帧
// 可以在这里做实时分析(如人脸检测、色值统计)
processFrame(data);
});
}
// ════════════════ 计算最佳预览尺寸 ════════════════
private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
final double TARGET_RATIO = (double) w / h;
Camera.Size optimal = null;
double minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - TARGET_RATIO) < minDiff) {
optimal = size;
minDiff = Math.abs(ratio - TARGET_RATIO);
}
}
return optimal;
}
@Override
protected void onPause() {
super.onPause();
if (mCamera != null) {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
}
2.3 Camera1 参数配置
java
Camera.Parameters params = mCamera.getParameters();
// ─── 预览尺寸 ───
params.setPreviewSize(width, height);
params.setPreviewFormat(ImageFormat.NV21); // Camera1 默认格式
// ─── 拍照尺寸 ───
params.setPictureSize(width, height);
params.setPictureFormat(ImageFormat.JPEG);
// ─── 对焦模式 ───
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); // 自动对焦
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); // 持续对焦
params.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY); // 无限远(适合远景)
params.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED); // 固定焦距(眼镜/工控场景)
// ─── 闪光灯 ───
params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
params.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
params.setFlashMode(Camera.Parameters.FLASH_MODE_ON);
// ─── 场景模式 ───
params.setSceneMode(Camera.Parameters.SCENE_MODE_NIGHT); // 夜景模式
params.setSceneMode(Camera.Parameters.SCENE_MODE_ACTION); // 运动模式(缩短曝光)
// ─── 曝光补偿 ───
params.setExposureCompensation(value); // value范围由 getMinExposureCompensation() 和 getMaxExposureCompensation() 确定
三、Camera2 API
3.1 架构核心概念
Camera2 是对硬件抽象层(HAL1)的重新设计,引入了**管道(Pipeline)**模型:
undefined
应用层
↓ CaptureRequest
CameraCaptureSession(会话)
↓
CameraDevice(设备)
↓
CameraCharacteristics(摄像头特性)
↓
Camera HAL(硬件抽象层)
↓
CMOS Sensor(实际硬件)
四个关键对象:
CameraManager:摄像头管理器(系统服务),用于枚举和打开摄像头CameraDevice:摄像头设备,代表一个物理摄像头CameraCaptureSession:一次拍摄会话,管理输入/输出 SurfaceCaptureRequest:一次具体的拍摄请求(预览/拍照/录像)
3.2 完整预览 + 拍照代码
kotlin
class Camera2Activity : AppCompatActivity() {
private lateinit var textureView: TextureView
private lateinit var cameraManager: CameraManager
private var cameraDevice: CameraDevice? = null
private var captureSession: CameraCaptureSession? = null
private var imageReader: ImageReader? = null
private var backgroundHandler: Handler? = null
private var cameraId: String = "0" // 0=后置, 1=前置
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textureView = TextureView(this)
setContentView(textureView)
cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
startBackgroundThread()
}
// ════════════════ 打开摄像头 ════════════════
private fun openCamera() {
try {
// 获取后置摄像头ID
for (id in cameraManager.cameraIdList) {
val characteristics = cameraManager.getCameraCharacteristics(id)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
cameraId = id
break
}
}
// 检查相机权限
if (checkSelfPermission(Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.CAMERA), 101)
return
}
// 打开摄像头
cameraManager.openCamera(cameraId, stateCallback, backgroundHandler)
} catch (e: CameraAccessException) {
e.printStackTrace()
}
}
private val stateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
cameraDevice = camera
createPreviewSession() // 摄像头打开后创建预览会话
}
override fun onDisconnected(camera: CameraDevice) {
camera.close()
cameraDevice = null
}
override fun onError(camera: CameraDevice, error: Int) {
camera.close()
cameraDevice = null
}
}
// ════════════════ 创建预览会话 ════════════════
private fun createPreviewSession() {
val texture = textureView.surfaceTexture ?: return
texture.setDefaultBufferSize(1280, 720) // 预览分辨率
// ImageReader 用于接收拍照数据
imageReader = ImageReader.newInstance(
1280, 720,
ImageFormat.JPEG, 2
).apply {
setOnImageAvailableListener({ reader ->
val image = reader.acquireLatestImage()
image?.let {
val buffer = it.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
savePhoto(bytes) // 保存 JPEG
it.close()
}
}, backgroundHandler)
}
val surfaces = listOf(
Surface(texture),
imageReader!!.surface
)
// 创建 CaptureSession(这里传入所有需要用到的 Surface)
cameraDevice?.createCaptureSession(
surfaces,
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
captureSession = session
startPreview() // 开始预览
}
override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e("Camera2", "Session配置失败")
}
},
backgroundHandler
)
}
// ════════════════ 开始预览 ════════════════
private fun startPreview() {
val texture = textureView.surfaceTexture ?: return
// 构建预览请求(Template)
val previewRequest = cameraDevice?.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW // 预览模板
)?.apply {
// 添加预览 Surface
addTarget(Surface(texture))
// 自动对焦
set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
// 自动曝光
set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
}?.build()
previewRequest?.let {
captureSession?.setRepeatingRequest(it, null, backgroundHandler)
}
}
// ════════════════ 拍照 ════════════════
fun takePicture() {
val captureBuilder = cameraDevice?.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE // 拍照模板
)?.apply {
addTarget(imageReader!!.surface) // 输出到 ImageReader
set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
set(CaptureRequest.JPEG_ORIENTATION, 90) // 旋转角度
}?.build()
captureBuilder?.let {
captureSession?.capture(it, object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest, result: TotalCaptureResult
) {
Log.d("Camera2", "拍照完成")
// 拍完后回到预览
startPreview()
}
}, backgroundHandler)
}
}
private fun savePhoto(data: ByteArray) {
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"photo_${System.currentTimeMillis()}.jpg")
FileOutputStream(file).use { it.write(data) }
}
// ════════════════ 后台线程 ════════════════
private fun startBackgroundThread() {
val thread = HandlerThread("CameraBackground").also { it.start() }
backgroundHandler = Handler(thread.looper)
}
}
3.3 Camera2 核心 API 对照表
| 任务 | Camera2 API |
|---|---|
| 枚举摄像头 | CameraManager.getCameraIdList() + getCameraCharacteristics() |
| 打开摄像头 | CameraManager.openCamera() |
| 查询能力 | CameraCharacteristics.get() |
| 创建 Session | CameraDevice.createCaptureSession() |
| 预览请求 | CameraDevice.createCaptureRequest(TEMPLATE_PREVIEW) |
| 拍照请求 | CameraDevice.createCaptureRequest(TEMPLATE_STILL_CAPTURE) |
| 设置对焦 | CaptureRequest.CONTROL_AF_MODE |
| 设置曝光 | CaptureRequest.CONTROL_AE_MODE / CONTROL_AE_EXPOSURE_COMPENSATION |
| 设置闪光灯 | CaptureRequest.FLASH_MODE |
| 设置白平衡 | CaptureRequest.CONTROL_AWB_MODE |
| 设置ISO | CaptureRequest.SENSOR_SENSITIVITY(需手动曝光模式) |
| 设置快门时间 | CaptureRequest.SENSOR_EXPOSURE_TIME(纳秒,需手动曝光) |
| 设置缩放 | CaptureRequest.SCALER_CROP_REGION |
| 获取帧数据 | ImageReader + Surface |
| 录像 | MediaRecorder(搭配 Surface input) |
3.4 Camera2 关键参数查询
kotlin
// ─── 获取摄像头特性 ───
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
// ─── 支持的 Stream 配置 ───
val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
// ─── 支持的预览/拍照尺寸 ───
val previewSizes = map.getOutputSizes(SurfaceTexture::class.java)
val pictureSizes = map.getOutputSizes(ImageFormat.JPEG)
// ─── 支持的对焦模式 ───
val afModes = characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)
// CONTROL_AF_MODE_OFF, CONTROL_AF_MODE_AUTO, CONTROL_AF_MODE_CONTINUOUS_PICTURE ...
// ─── 支持的闪光灯 ───
val flashAvailable = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE)
// ─── 对焦测光区域 ───
val afRegion = MeteringRectangle(
x, y, width, height,
MeteringRectangle.METERING_WEIGHT_MAX // 权重
)
// 在 CaptureRequest 中设置:set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(afRegion))
// 同时需要:set(CONTROL_AE_REGIONS, ...) + set(CONTROL_AF_MODE, CONTROL_AF_MODE_AUTO)
// ─── 缩放范围 ───
val zoomRange = characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_ZOOM_RATIO_RANGE)
// zoomRatioRange.min / max(1.0x ~ 10.0x+)
四、CameraX API
4.1 为什么选择 CameraX
CameraX 是 Google 在 2019 年推出的新 API,解决了 Camera2 的三个核心痛点:
- 碎片化适配:Camera2 各厂商实现不一致,CameraX 统一兜底
- 生命周期管理:Camera2 需手动管理,CameraX 自动绑定 LifecycleOwner
- 配置复用:CameraX 自动管理 Session 配置的复用和重建
4.2 CameraX 完整代码(预览 + 拍照 + 分析)
kotlin
class CameraXActivity : AppCompatActivity() {
private lateinit var cameraController: ProcessCameraProvider
private lateinit var imageAnalysis: ImageAnalysis
private val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) // 或 CAPTURE_MODE_MINIMIZE_LATENCY
.build()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewFinder = findViewById<PreviewView>(R.id.viewFinder)
// ─── 绑定用例 ───
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// ─── Preview ───
val preview = Preview.Builder()
.setTargetResolution(Size(1280, 720))
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
// ─── ImageAnalysis(实时帧分析)══════════════
imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(640, 480))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(cameraExecutor) { imageProxy ->
// imageProxy 是 YUV_420_888 格式
processImage(imageProxy)
imageProxy.close() // 必须关闭,否则 CameraX 阻塞
}
}
// ─── 绑定到 Lifecycle ───
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, // ← 自动管理生命周期
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageCapture,
imageAnalysis
)
} catch (e: Exception) {
Log.e("CameraX", "Use case binding failed", e)
}
}, ContextCompat.getMainExecutor(this))
}
// ════════════════ 拍照 ════════════════
private val cameraExecutor = Executors.newSingleThreadExecutor()
fun takePhoto() {
val outputOptions = ImageCapture.OutputFileOptions.Builder(
File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "photo.jpg"
)
).build()
imageCapture.takePicture(
outputOptions,
cameraExecutor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
Log.d("CameraX", "Photo saved: ${output.savedUri}")
}
override fun onError(exception: ImageCaptureException) {
Log.e("CameraX", "Photo capture failed", exception)
}
}
)
}
// ════════════════ 实时帧分析 ════════════════
@androidx.annotation.OptIn(ExperimentalGetImage::class)
private fun processImage(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null) {
// 方法1:直接用 Image
val buffer = imageProxy.planes[0].buffer
val data = ByteArray(buffer.remaining())
buffer.get(data)
// 方法2:转 Bitmap(注意性能)
val bitmap = imageProxy.toBitmap()
// 方法3:用 ML Kit 做图像分析(示例)
// val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
// run inference here
}
}
// ════════════════ 切换前后摄像头 ════════════════
fun switchCamera() {
val cameraSelector = if (currentLens == CameraSelector.LENS_FACING_BACK) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
// 重新绑定即可
}
}
4.3 CameraX 核心配置
kotlin
// ─── ImageCapture 配置 ───
ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) // 高质量(适合拍照)
// .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // 低延迟(适合连拍)
.setTargetResolution(Size(1280, 720))
.setTargetRotation(windowManager.defaultDisplay.rotation)
.setFlashMode(ImageCapture.FLASH_MODE_AUTO / FLASH_MODE_ON / FLASH_MODE_OFF)
.build()
// ─── Preview 配置 ───
Preview.Builder()
.setTargetResolution(Size(1280, 720))
.setTargetRotation(windowManager.defaultDisplay.rotation)
.build()
// ─── ImageAnalysis 配置 ───
ImageAnalysis.Builder()
.setTargetResolution(Size(640, 480))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// STRATEGY_KEEP_ONLY_LATEST: 只保留最新帧,丢弃旧帧(适合实时分析)
// STRATEGY_BLOCK_PRODUCER: 阻塞生产者直到消费者处理完(保证不丢帧,但可能卡顿)
.build()
// ─── 选择摄像头 ───
CameraSelector.DEFAULT_BACK_CAMERA // 后置
CameraSelector.DEFAULT_FRONT_CAMERA // 前置
CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
4.4 CameraX 录像(VideoCapture)
kotlin
// ─── 录像绑定 ───
private var videoCapture: VideoCapture<Recorder>? = null
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HD))
.build()
videoCapture = VideoCapture.withOutput(recorder)
cameraProvider.bindToLifecycle(
this,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
videoCapture
)
// ─── 开始录像 ───
fun startRecording() {
val file = File(outputDir, "video_${System.currentTimeMillis()}.mp4")
val outputOptions = FileOutputOptions.Builder(file).build()
videoCapture?.startRecording(
outputOptions,
cameraExecutor,
object : VideoCapture.OnVideoSavedCallback {
override fun onVideoSaved(output: VideoCapture.OutputFileResults) {
Log.d("CameraX", "Video saved: ${file.absolutePath}")
}
override fun onError(code: Int, message: String) {
Log.e("CameraX", "Video save error: $message")
}
}
)
}
// ─── 停止录像 ───
fun stopRecording() {
videoCapture?.stopRecording()
}
五、三代 Camera API 横向对比
5.1 逐维度对比
| 维度 | Camera1 | Camera2 | CameraX |
|---|---|---|---|
| 代码行数(预览+拍照) | ~80行 | ~200行 | ~50行 |
| 生命周期管理 | 手动 | 手动 | 自动绑定 |
| 横竖屏切换 | 需重建 | 需重建 | 自动处理 |
| 多摄像头切换 | Camera.open(id) |
CameraManager.openCamera() |
bindToLifecycle 重建 |
| 手动曝光(ISO/快门) | 部分支持 | 完全支持 | 有限支持 |
| Raw 格式输出 | 不支持 | 支持 | 不支持 |
| ZSL(零快门延迟) | 原生 | 手动配置 | 自动 |
| 厂商适配成本 | 低 | 高(碎片化) | 低 |
| 最低 Android 版本 | API 1 | API 21(大部分设备 API 26+) | API 24(部分 API 21) |
5.2 选型建议
undefined
┌─────────────────────────────────────────────────────┐
│ 如何选择 Camera API? │
├─────────────────────────────────────────────────────┤
│ 只需简单拍照/预览,新项目 / 快速上线 │
│ → CameraX(Google 推荐,代码量最少) │
├─────────────────────────────────────────────────────┤
│ 需要完全控制:ISO/快门/对焦/Raw输出/多摄像头 │
│ → Camera2(最底层,功能最全) │
├─────────────────────────────────────────────────────┤
│ 旧项目 / 维护已有 Camera1 代码 │
│ 临时支持 / 简单场景 │
│ → Camera1(仅作为过渡) │
├─────────────────────────────────────────────────────┤
│ 智能眼镜 / 工控设备 / 嵌入式 Android │
│ 需要 Sensor 寄存器级控制、定制 ISP 参数 │
│ → Camera2(必须,CameraX 无法满足) │
└─────────────────────────────────────────────────────┘
六、常见问题与解决方案
Q1:预览变形(拉伸/压缩)
原因: 预览 TextureView 比例与摄像头输出比例不一致。
CameraX 解决:
kotlin
// 方案1:设置 ScaleType(最简单)
viewFinder.scaleType = PreviewView.ScaleType.FIT_CENTER
// FIT_CENTER: 等比缩放,多余区域黑边(推荐预览)
// FILL_CENTER: 拉伸填满(预览OK,录像不建议)
// FIT_START/FIT_END: 等比,贴合一边
// 方案2:让 TextureView 尺寸与 Camera 输出比例一致
val cameraController = cameraProvider.bindToLifecycle(...)
val bindContext = cameraController.cameraInfo
val targetRatio = 16f / 9f
viewFinder.post {
val params = viewFinder.layoutParams
params.width = viewFinder.width
params.height = (viewFinder.width / targetRatio).toInt()
viewFinder.layoutParams = params
}
Camera1/Camera2 解决:
java
// 在 onPreviewFrame 回调中手动缩放
canvas.drawBitmap(bitmap,
null, // 源区域(全帧)
new RectF(0, 0, viewWidth, viewHeight), // 目标区域
new Paint(Paint.FILTER_BITMAP_FLAG)
);
Q2:预览方向错误(旋转90°/270°)
Camera1:
java
Camera.Parameters params = mCamera.getParameters();
String orientation = params.get("orientation");
if (orientation != null) {
params.set("orientation", "portrait"); // 前置摄像头
mCamera.setParameters(params);
}
// Android 4.0+ 用 setDisplayOrientation
mCamera.setDisplayOrientation(90); // 前置:270;后置:90
Camera2/CameraX:
kotlin
val rotation = windowManager.defaultDisplay.rotation
// rotation: 0=竖屏, 90=横屏(右), 180=倒竖, 270=横屏(左)
// Camera2
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION,
ORIENTATIONS.get(rotation))
// CameraX 自动处理,但录像时需要设置
val preview = Preview.Builder()
.setTargetRotation(rotation)
.build()
Q3:前置摄像头镜像
需求: 前置摄像头预览时文字是正的(镜像效果),拍照存的文件是镜像还是正的?
kotlin
// CameraX:预览镜像
viewFinder.scaleX = -1f // 水平镜像
// Camera2:预览时应用 Transform
val matrix = Matrix()
matrix.setScale(-1f, 1f, viewFinder.width / 2f, viewFinder.height / 2f)
textureView.setTransform(matrix)
// 关键点:镜像只影响预览,不影响 JPEG 拍照数据
// JPEG 内容永远是摄像头原始方向(前置默认镜像存储)
// 如需保存"正"的照片,需手动翻转字节数组
Q4:Camera2 无法打开(CameraAccessException)
错误类型排查:
undefined
CAMERA_ERROR_EVICTED = 2 → 摄像头被其他应用占用
CAMERA_ERROR_SERVER_DIED = 100 → Camera 服务崩溃,重建 CameraManager
CAMERA_ERROR_MAX_CAMERAS_IN_USE = 4 → 超过最大摄像头数量
CAMERA_DISABLED = 1 → 摄像头被系统禁用(安全策略)
try {
cameraManager.openCamera(cameraId, callback, handler)
} catch (e: CameraAccessException) {
when (e.reason) {
CameraAccessException.CAMERA_ERROR_EVICTED -> {
// 摄像头被占用,提示用户关闭其他应用
}
CameraAccessException.CAMERA_ERROR_SERVER_DIED -> {
// Camera 服务崩溃,等待系统恢复后重试
Handler(Looper.getMainLooper()).postDelayed({
cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
openCamera()
}, 1000)
}
}
}
Q5:CameraX bindToLifecycle 失败(IllegalArgumentException)
常见原因: LifecycleOwner 状态不对(如 Fragment 已 onDestroy 后尝试绑定)。
kotlin
// ❌ 错误:在 onDestroyView 中绑定
override fun onDestroyView() {
// 某些框架版本中,这里 still 有生命周期
cameraProvider.unbindAll() // 先解绑
super.onDestroyView()
}
// ✅ 正确:绑定和解绑都基于 lifecycle
// bindToLifecycle(this as LifecycleOwner)
// this 是 AppCompatActivity(实现了 LifecycleOwner)
// 在 Fragment 中使用
lifecycleScope.launch {
val cameraProvider = ProcessCameraProvider.getInstance(requireContext()).get()
cameraProvider.bindToLifecycle(
viewLifecycleOwner, // ← 用 viewLifecycleOwner,不用 this
cameraSelector,
preview,
imageCapture
)
}
Q6:预览帧处理卡顿 / 丢帧
CameraX ImageAnalysis 丢帧原因与解决:
kotlin
// 原因1:分析器处理太慢,CameraX 丢弃旧帧
// STRATEGY_KEEP_ONLY_LATEST:自动丢弃旧帧,不阻塞(推荐实时分析)
// 原因2:图片格式转换耗时
@androidx.annotation.OptIn(ExperimentalGetImage::class)
private fun processImage(imageProxy: ImageProxy) {
// ❌ 不要在这里做复杂计算
// ❌ 不要在这里转 Bitmap(太慢)
// ✅ 在后台线程池处理
val buffer = imageProxy.planes[0].buffer
val data = ByteArray(buffer.remaining())
buffer.get(data)
cameraExecutor.submit {
// 在独立线程处理,不阻塞 ImageAnalysis
runInference(data)
}
}
// 原因3:分辨率过高
imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(320, 240)) // ← 降低分析分辨率(预览用高清,分析用低分辨率)
.build()
Q7:Camera1 在 Android 8.0+ 前置摄像头无法自动对焦
问题: Camera1 FOCUS_MODE_CONTINUOUS_PICTURE 在部分设备上对前置摄像头无效。
java
// 解决1:降级到 FOCUS_MODE_AUTO(需要点击触发)
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
// 解决2:手动设置焦点区域
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
// 焦点区域(需要 Camera1 支持 FOCUS_MODE_AUTO)
// MeteringRectangle 格式:x, y, width, height, weight
Camera.Area focusArea = new Camera.Area(new Rect(-300, -300, 300, 300), 1000);
params.setFocusAreas(Arrays.asList(focusArea));
params.setMeteringAreas(Arrays.asList(focusArea));
mCamera.setParameters(params);
// 解决3:直接迁移到 Camera2(Camera1 的对焦 API 在部分厂商实现不完整)
Q8:CameraX 在 Android 10 以下无法保存照片
原因: Android 10(API 29)开始Scoped Storage,外部存储权限收紧。
kotlin
// 方案1:使用 MediaStore(Android Q+ 推荐)
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "photo_${System.currentTimeMillis()}.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
}
val outputOptions = ImageCapture.OutputFileOptions.Builder(
contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
).build()
// 方案2:使用应用私有目录(无需权限)
val outputOptions = ImageCapture.OutputFileOptions.Builder(
File(requireContext().filesDir, "photos").also { it.mkdirs() },
"photo_${System.currentTimeMillis()}.jpg"
).build()
// 方案3:Legacy 兼容模式(在 AndroidManifest 中关闭 Scoped Storage)
// <application android:requestLegacyExternalStorage="true">
// 注意:这个属性在 Android 11+ 无效
Q9:Camera2 预览正常但拍照报 "No such file"
原因: Camera2 拍照用 ImageReader,如果 ImageReader 的 Surface 销毁后再拍照会失败。
kotlin
// 错误写法:onResume 中 openCamera,在 onPause 中 release
// 如果 Activity 生命周期管理不好,ImageReader 可能已被回收
// 正确做法:使用 Lifecycle
// ProcessCameraProvider.bindToLifecycle 自动管理 ImageReader 生命周期
// Camera2 中也要确保 ImageReader 在 Session 重建时同步重建
// 检查 imageReader 是否已关闭
if (imageReader?.surface?.isValid != true) {
Log.e("Camera2", "ImageReader surface 无效,重新创建")
recreateImageReader()
}
Q10:CameraX 切换摄像头后预览卡死
原因: 切换摄像头时旧的 Session 资源未完全释放。
kotlin
// 完整切换流程
fun switchCamera() {
cameraProvider.unbindAll() // ← 先解绑所有用例
// 延迟一点再绑定(让底层资源完全释放)
Handler(Looper.getMainLooper()).postDelayed({
currentLens = if (currentLens == CameraSelector.DEFAULT_BACK_CAMERA) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
cameraProvider.bindToLifecycle(
this,
currentLens,
preview,
imageCapture,
imageAnalysis
)
}, 300)
}
七、遇到的高频问题
Q1:Camera1 和 Camera2 的核心区别是什么?
参考答案:
Camera1 是 Android 早期 API,封装层次高,简单场景够用,但无法访问很多硬件能力(如手动曝光 Raw 输出)。Camera2 是完全重新设计的 API,采用管道模型(CameraDevice → CaptureSession → CaptureRequest),可以控制每一个细节(ISO、快门时间、对焦区域、闪光灯策略等),但代码复杂且厂商碎片化严重。Camera2 是 Camera1 的替代品,Camera1 已废弃(Android 10 后不再维护)。
Q2:CameraX 相比 Camera2 最大的优势是什么?有什么场景 CameraX 做不了?
参考答案:
CameraX 最大优势:①生命周期自动管理,不用担心 Activity 重建时摄像头泄漏;②Google 统一适配,解决厂商碎片化;③API 简洁,代码量减少 60%+。CameraX 做不到的事:①Raw 格式输出(需要 Camera2);②Sensor 级手动控制(手动 ISO/快门时间需要 Camera2);③完全自定义预览处理链(CameraX 是黑盒);④同时预览两个不同摄像头的画面(CameraX 底层仍是单 CameraDevice)。
Q3:Camera2 的 CaptureRequest 中的 CONTROL_AF_MODE 有哪些值?分别适用什么场景?
参考答案:
OFF:完全手动对焦,手动设置镜头位置(几乎不用)AUTO:单次自动对焦,对焦一次后锁定(点击对焦场景)CONTINUOUS_PICTURE:持续自动对焦,适用于拍照预览(推荐)CONTINUOUS_VIDEO:持续对焦,优化为视频平滑对焦(录像模式)EDOF(Extended Depth of Field):景深扩展(无需对焦,近远景都清晰,用于低端设备)智能眼镜/工控设备通常用
CONTINUOUS_PICTURE或FIXED(焦距固定,不自动对焦)。
Q4:CameraX 的 ImageAnalysis 在处理 YUV 数据时要注意什么?
参考答案:
① 必须调用
imageProxy.close(),否则 CameraX 阻塞,无法继续下一帧;② YUV_420_888 格式,Y 平面(亮度)数据在 planes0,UV 数据在 planes1 和 planes2;③toBitmap()非常耗时(涉及格式转换),实时分析不要用;④setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)确保只处理最新帧,避免队列堆积;⑤ 分析器运行在 CameraX 内部线程,不能直接操作UI,需切到主线程。
Q5:Camera1 的 setPreviewCallback 和 Camera2 的 ImageReader 接收帧数据有什么区别?
参考答案:
Camera1 的
PreviewCallback回调的 data 是 NV21/YV12 格式的字节数组 ,直接可用但每次都拷贝一份,GC 压力大。Camera2 用ImageReader+Surface,数据以ByteBuffer形式提供,可以 zero-copy 传递给处理链(ImageReader 直接绑定到 CaptureSession 输出),效率更高。Camera2 的ImageReader可以配置多种格式(JPEG/YUV_420_888/Raw),而 Camera1 只能被动接收 Sensor 默认格式。
Q6:Camera2 中如何实现点击屏幕指定位置对焦?
参考答案:
① 获取点击坐标;② 转换为 Camera2 的
MeteringRectangle(以(0,0)-(1000,1000)为归一化坐标系);③ 构建CaptureRequest,设置CONTROL_AF_REGIONS+CONTROL_AE_REGIONS(曝光也要一起测光);④ 设置CONTROL_AF_TRIGGER为TRIGGER_START_CAMERA开始对焦;⑤ 对焦完成回调onAutoFocusFinished后重新切换回CONTROL_AF_MODE_CONTINUOUS_PICTURE。CameraX 可直接用CameraX.letFocusOnPoint()一行搞定。
Q7:CameraX 的 PreviewView 有哪几种 ScaleType?如何选择?
参考答案:
FIT_CENTER:等比缩放,画面完整,多余区域填黑边(预览推荐)FILL_CENTER:拉伸填满,画面填满但可能变形(不适合拍远景文字)FIT_START/FIT_END:等比缩放,贴合某一边(适合特定布局)CENTER_INSIDE:和 FIT_CENTER 类似但不放大拍近景文字/文档时建议
FILL_CENTER(防止黑边),但会变形;FIT_CENTER文字最真实但有黑边。
Q8:在 Android 10(API 29)的 Scoped Storage 限制下,如何保存 CameraX 拍的照片?
参考答案:
三种方案:①
MediaStore:用ContentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)方式创建输出流,跨应用可访问;② 应用私有目录context.filesDir:无需权限,但只有本应用能访问;③ 声明requestLegacyExternalStorage="true":仅 Android 10 临时有效,Android 11+ 无效。最推荐方案是 MediaStore,兼容性好且无需存储权限(Android Q+ MediaStore 写入不需要 WRITE_EXTERNAL_STORAGE 权限)。