2. Camera2拍照
任务说明
本案例基于Camera2 API实现一个最小可用的拍照应用:打开后置摄像头,预览实时画面,点击快门按钮捕获单帧静态图像并以JPEG格式保存到应用私有目录。完成目标需覆盖:相机权限声明、预览会话建立、拍照请求触发、图像回调落盘四个环节。
实现方案
使用TextureView作为预览载体,通过CameraDevice创建会话,ImageReader接收JPEG帧。核心流程为:用户点击按钮→调用captureStill()→发送带ImageReader目标Surface的拍照请求→在onImageAvailable回调中将图像写入文件。为聚焦主目标,省略自动对焦、闪光灯控制、旋转校正等增强功能。
代码实现
// AndroidManifest.xml 关键权限
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera2" android:required="true" />
// Camera2CaptureActivity.java
public class Camera2CaptureActivity extends AppCompatActivity {
private TextureView textureView;
private CameraDevice cameraDevice;
private CameraCaptureSession captureSession;
private ImageReader imageReader;
private String cameraId;
private Handler backgroundHandler;
private HandlerThread backgroundThread;
private File outputFile;
private final TextureView.SurfaceTextureListener surfaceListener = new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
openCamera();
}
@Override public void onSurfaceTextureSizeChanged(SurfaceTexture s, int w, int h) {}
@Override public boolean onSurfaceTextureDestroyed(SurfaceTexture s) { return false; }
@Override public void onSurfaceTextureUpdated(SurfaceTexture s) {}
};
private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
cameraDevice = camera;
createPreviewSession();
}
@Override public void onDisconnected(CameraDevice camera) { camera.close(); }
@Override public void onError(CameraDevice camera, int error) { camera.close(); }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_capture);
textureView = findViewById(R.id.texture_view);
findViewById(R.id.btn_capture).setOnClickListener(v -> captureStill());
outputFile = new File(getExternalFilesDir(null), "capture_" + System.currentTimeMillis() + ".jpg");
}
@Override
protected void onResume() {
super.onResume();
startBackgroundThread();
if (textureView.isAvailable()) openCamera();
else textureView.setSurfaceTextureListener(surfaceListener);
}
@Override
protected void onPause() {
closeCamera();
stopBackgroundThread();
super.onPause();
}
private void startBackgroundThread() {
backgroundThread = new HandlerThread("CameraBackground");
backgroundThread.start();
backgroundHandler = new Handler(backgroundThread.getLooper());
}
private void stopBackgroundThread() {
backgroundThread.quitSafely();
try { backgroundThread.join(); } catch (InterruptedException e) { e.printStackTrace(); }
}
private void openCamera() {
CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE);
try {
cameraId = manager.getCameraIdList()[0]; // 默认后置
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.CAMERA}, 100);
return;
}
manager.openCamera(cameraId, stateCallback, backgroundHandler);
} catch (CameraAccessException e) { e.printStackTrace(); }
}
private void createPreviewSession() {
try {
SurfaceTexture texture = textureView.getSurfaceTexture();
texture.setDefaultBufferSize(1280, 720);
Surface previewSurface = new Surface(texture);
// 配置ImageReader:JPEG格式,宽高与预览一致
imageReader = ImageReader.newInstance(1280, 720, ImageFormat.JPEG, 2);
imageReader.setOnImageAvailableListener(reader -> {
try (Image image = reader.acquireLatestImage()) {
if (image != null) {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
fos.write(bytes);
}
runOnUiThread(() -> Toast.makeText(this, "已保存: " + outputFile.getAbsolutePath(), Toast.LENGTH_LONG).show());
}
} catch (IOException e) { e.printStackTrace(); }
}, backgroundHandler);
List<Surface> surfaces = Arrays.asList(previewSurface, imageReader.getSurface());
CameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
captureSession = session;
startPreview(previewSurface);
}
@Override public void onConfigureFailed(CameraCaptureSession session) {}
}, backgroundHandler);
} catch (CameraAccessException e) { e.printStackTrace(); }
}
private void startPreview(Surface surface) {
try {
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
builder.addTarget(surface);
captureSession.setRepeatingRequest(builder.build(), null, backgroundHandler);
} catch (CameraAccessException e) { e.printStackTrace(); }
}
private void captureStill() {
try {
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
builder.addTarget(imageReader.getSurface());
captureSession.capture(builder.build(), null, backgroundHandler);
} catch (CameraAccessException e) { e.printStackTrace(); }
}
private void closeCamera() {
if (captureSession != null) { captureSession.close(); captureSession = null; }
if (cameraDevice != null) { cameraDevice.close(); cameraDevice = null; }
if (imageReader != null) { imageReader.close(); imageReader = null; }
}
}
布局文件
<!-- activity_capture.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextureView
android:id="@+id/texture_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/btn_capture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="拍照" />
</LinearLayout>
落地与验证
运行方式:将代码放入Android Studio项目,在API 21+真机(需支持Camera2)上运行。首次启动授予相机权限。
验证步骤:
- 启动应用后TextureView显示实时预览画面;
- 点击“拍照”按钮,观察Toast提示保存路径;
- 使用文件管理器或adb命令检查文件存在且可打开:
adb shell ls -l /sdcard/Android/data/<包名>/files/capture_*.jpg; - 用图片查看器打开该文件,确认图像内容与预览场景一致且非黑帧。
判定标准:点击拍照后生成非空JPEG文件,文件大小大于10KB且能正常解码显示,即视为通过。