2. Camera2基础

任务说明

在Camera2预览界面添加一个切换按钮,点击后动态切换前后摄像头。切换过程中需要正确关闭当前CameraCaptureSession和CameraDevice,释放资源后重新打开目标摄像头并重建预览。

核心实现

// Camera2SwitchFragment.java
public class Camera2SwitchFragment extends Fragment {
    private static final int REQUEST_CAMERA_PERMISSION = 200;
    private String cameraId;
    private CameraDevice cameraDevice;
    private CameraCaptureSession captureSession;
    private ImageReader imageReader;
    private CaptureRequest.Builder previewRequestBuilder;
    private CaptureRequest previewRequest;
    private HandlerThread backgroundThread;
    private Handler backgroundHandler;
    private TextureView textureView;
    private boolean isFrontCamera = false;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_camera2_switch, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        textureView = view.findViewById(R.id.texture_view);
        view.findViewById(R.id.btn_switch).setOnClickListener(v -> switchCamera());
    }

    @Override
    public void onResume() {
        super.onResume();
        startBackgroundThread();
        if (textureView.isAvailable()) {
            openCamera();
        } else {
            textureView.setSurfaceTextureListener(surfaceTextureListener);
        }
    }

    private TextureView.SurfaceTextureListener surfaceTextureListener =
            new TextureView.SurfaceTextureListener() {
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
            openCamera();
        }

        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}

        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; }

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
    };

    private void switchCamera() {
        isFrontCamera = !isFrontCamera;
        closeCamera();
        openCamera();
    }

    private void openCamera() {
        if (ContextCompat.checkSelfPermission(requireContext(),
                Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.CAMERA},
                    REQUEST_CAMERA_PERMISSION);
            return;
        }
        CameraManager manager = (CameraManager) requireContext()
                .getSystemService(Context.CAMERA_SERVICE);
        try {
            cameraId = getCameraId(manager);
            if (cameraId == null) return;
            if (ActivityCompat.checkSelfPermission(requireContext(),
                    Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            manager.openCamera(cameraId, stateCallback, backgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private String getCameraId(CameraManager manager) throws CameraAccessException {
        for (String id : manager.getCameraIdList()) {
            CameraCharacteristics chars = manager.getCameraCharacteristics(id);
            Integer facing = chars.get(CameraCharacteristics.LENS_FACING);
            if (facing != null) {
                if (isFrontCamera && facing == CameraCharacteristics.LENS_FACING_FRONT) {
                    return id;
                }
                if (!isFrontCamera && facing == CameraCharacteristics.LENS_FACING_BACK) {
                    return id;
                }
            }
        }
        return null;
    }

    private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            cameraDevice = camera;
            createPreviewSession();
        }

        @Override
        public void onDisconnected(CameraDevice camera) {
            camera.close();
            cameraDevice = null;
        }

        @Override
        public void onError(CameraDevice camera, int error) {
            camera.close();
            cameraDevice = null;
        }
    };

    private void createPreviewSession() {
        try {
            SurfaceTexture texture = textureView.getSurfaceTexture();
            if (texture == null) return;
            texture.setDefaultBufferSize(1280, 720);

            Surface surface = new Surface(texture);
            imageReader = ImageReader.newInstance(1280, 720,
                    ImageFormat.JPEG, 1);

            previewRequestBuilder = cameraDevice
                    .createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            previewRequestBuilder.addTarget(surface);

            cameraDevice.createCaptureSession(Arrays.asList(surface,
                    imageReader.getSurface()),
                    new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(CameraCaptureSession session) {
                    if (cameraDevice == null) return;
                    captureSession = session;
                    previewRequestBuilder.set(
                            CaptureRequest.CONTROL_AF_MODE,
                            CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                    previewRequest = previewRequestBuilder.build();
                    try {
                        captureSession.setRepeatingRequest(previewRequest,
                                null, backgroundHandler);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void onConfigureFailed(CameraCaptureSession session) {}
            }, 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;
        }
    }

    private void startBackgroundThread() {
        backgroundThread = new HandlerThread("CameraBackground");
        backgroundThread.start();
        backgroundHandler = new Handler(backgroundThread.getLooper());
    }

    private void stopBackgroundThread() {
        if (backgroundThread != null) {
            backgroundThread.quitSafely();
            try {
                backgroundThread.join();
                backgroundThread = null;
                backgroundHandler = null;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void onPause() {
        closeCamera();
        stopBackgroundThread();
        super.onPause();
    }
}

布局文件

<!-- fragment_camera2_switch.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextureView
        android:id="@+id/texture_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:id="@+id/btn_switch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center_horizontal"
        android:layout_marginBottom="48dp"
        android:text="切换摄像头" />
</FrameLayout>

AndroidManifest配置

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

关键流程说明

切换按钮点击后,switchCamera()方法先翻转isFrontCamera标志位,然后调用closeCamera()关闭当前会话和设备,最后调用openCamera()重新打开目标摄像头。资源释放顺序为:先关闭CameraCaptureSession,再关闭CameraDevice,最后关闭ImageReader。打开新摄像头时通过getCameraId()根据当前标志位选择前置或后置摄像头ID。

验证方法

  1. 在真机上运行应用,授予相机权限后应看到后置摄像头预览画面。
  2. 点击“切换摄像头”按钮,预览画面应切换为前置摄像头。
  3. 再次点击按钮,应切回后置摄像头,且切换过程无黑屏或崩溃。
  4. 连续快速点击切换按钮10次,应用不应崩溃,最终画面与最后一次点击对应的摄像头一致。
  5. 切换过程中观察Logcat,不应出现CameraAccessException或IllegalStateException异常。

判定标准:前后摄像头切换流畅,每次切换后预览画面正常显示,无资源泄漏导致的ANR或闪退。