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)上运行。首次启动授予相机权限。

验证步骤:

  1. 启动应用后TextureView显示实时预览画面;
  2. 点击“拍照”按钮,观察Toast提示保存路径;
  3. 使用文件管理器或adb命令检查文件存在且可打开:adb shell ls -l /sdcard/Android/data/<包名>/files/capture_*.jpg;
  4. 用图片查看器打开该文件,确认图像内容与预览场景一致且非黑帧。

判定标准:点击拍照后生成非空JPEG文件,文件大小大于10KB且能正常解码显示,即视为通过。