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。
验证方法
- 在真机上运行应用,授予相机权限后应看到后置摄像头预览画面。
- 点击“切换摄像头”按钮,预览画面应切换为前置摄像头。
- 再次点击按钮,应切回后置摄像头,且切换过程无黑屏或崩溃。
- 连续快速点击切换按钮10次,应用不应崩溃,最终画面与最后一次点击对应的摄像头一致。
- 切换过程中观察Logcat,不应出现
CameraAccessException或IllegalStateException异常。
判定标准:前后摄像头切换流畅,每次切换后预览画面正常显示,无资源泄漏导致的ANR或闪退。