文章目录
- 布局文件
- 请求有关权限
- 加载布局,设置surface。避免画面拉升等问题
学习Camera2 时,开发一个简单自定义相机。具体流程看代码
布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<SurfaceView android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="100"/>
<LinearLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/btnTakePhoto"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="5dp"
android:text="拍照"/>
<Button android:id="@+id/btnSwitch"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="切换"/>
<Button android:id="@+id/record"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="录像"/>
<Button android:id="@+id/stop"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="停止"/>
</LinearLayout>
</LinearLayout>
请求有关权限
因为使用的是Android 11的手机,有些权限需要动态申请。
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
代码中
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//动态获取权限
List<String> permissionList = new ArrayList<>();
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.CAMERA);
}
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.RECORD_AUDIO);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
if (!permissionList.isEmpty()) {
String[] permissions = permissionList.toArray(new String[permissionList.size()]);
ActivityCompat.requestPermissions(MainActivity.this, permissions, 1);
} else {
initView();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0) {
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "拒绝权限无法使用", Toast.LENGTH_SHORT).show();
finish();
}
}
initView();
} else {
Toast.makeText(this, "发生未知错误", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
}
}
加载布局,设置surface。避免画面拉升等问题
同时为几个按钮设置点击事件。
private void initView() {
surfaceView = findViewById(R.id.surfaceView);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
Log.d("预览尺寸", "surfaceCreated: " + surfaceView.getHeight() + ".." + surfaceView.getWidth());
//打开相机
openCamera();
int height = surfaceView.getHeight();
int width = surfaceView.getWidth();
if (height > width) {
float justH = width * 4.f / 3;
//设置View在水平方向的缩放比例,保证宽高比为3:4
surfaceView.setScaleX(height / justH);
} else {
float justW = height * 4.f / 3;
surfaceView.setScaleY(width / justW);
}
Log.d("变更后预览尺寸", "surfaceCreated: " + surfaceView.getHeight() + ".." + surfaceView.getWidth());
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
//关闭相机释放资源
closeCamera();
}
});
findViewById(R.id.btnTakePhoto).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//点击按钮拍照的事件
takePhoto();
}
});
findViewById(R.id.btnSwitch).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//点击按钮切换摄像头的事件
switchCamera();
}
});
findViewById(R.id.record).setOnClickListener(new View.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.R)
@Override
public void onClick(View v) {
if (Environment.isExternalStorageManager()) {
Log.d("有权限", "onClick: ");
}
/* config();
startRecorder();*/
videoRecorderUtils.startRecord(MainActivity.this, handler);
}
});
findViewById(R.id.stop).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
/* stopRecorder();*/
videoRecorderUtils.stopRecord();
startPreview();
}
});
//获取摄像头管理,并开启子线程
initCameraManager();
}
这个方法内包含了所有的过程,拆分为不同的方法
1.打开相机,首先获取cameraManager 对象,并且创建子线程。然后使用CameraManager获取使用哪一个摄像头,和此摄像头的具体参数。比如可以输出的照片尺寸。完事后检查权限,调用CameraManager的openCamera方法打开相机。
private void initCameraManager() {
cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
handlerThread = new HandlerThread("Camera2");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void openCamera() {
try {
//获取摄像头属性描述
CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(String.valueOf(currentCameraId));
//获取摄像头支持的配置属性
StreamConfigurationMap map = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Log.d("摄像头支持属性", "openCamera: " + map.toString());
Integer level = cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
Log.d("等级", "openCamera: "+level);
//获取预览画面的输出尺寸,使用SurfaceView做预览
previewSize = getMaxsize(map.getOutputSizes(SurfaceHolder.class));
Log.d("输出预览尺寸", "openCamera: " + previewSize.getHeight() + ".." + previewSize.getWidth());
initImageReader();
//打开相机,先获取权限
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
//打开摄像头
cameraManager.openCamera(String.valueOf(currentCameraId), stateCallback, handler);
} catch (CameraAccessException exception) {
exception.printStackTrace();
}
}
中间的initImageReader方法,是初始化,接受拍摄请求时图像的数据,在onImageAvailable中对接受的数据进行操作,比如保存等。
private void initImageReader() {
imageReader = ImageReader.newInstance(1920, 1080, ImageFormat.JPEG, 2);
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
handler.post(new ImageSaver(reader.acquireNextImage()));
}
}, handler);
}
在OpenCamera方法中有一个回调,
/**
* 打开相机的回调,
*/
private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
//打开相机后开启预览
cameraDevice = camera;
//打开照相机时初始化,videoRecord
videoRecorderUtils = new VideoRecorderUtils();
videoRecorderUtils.create(surfaceView, cameraDevice, VideoRecorderUtils.WH_720X480);
startPreview();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
camera.close();
cameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
camera.close();
cameraDevice = null;
finish();
}
};
此回调为打开相机的状态回调,在onOpen方法中可获取CameraDevice对象,即代表当前连接的设备。在此方法中可以开启预览。videoRecorderUtils是一个录像的工具类,这里将CameraDevice对象传过去,后续是设置录像的一些操作。在别的回调方法中执行错误处理等操作。
接下来开启预览
private void startPreview() {
try {
//构建预览请求
previewBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//设置预览输出的界面
previewBuilder.addTarget(surfaceHolder.getSurface());
//创建相机的会话Session
cameraDevice.createCaptureSession(Arrays.asList(surfaceHolder.getSurface(), imageReader.getSurface()), sessionStateCallback, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* session的回调
*/
private CameraCaptureSession.StateCallback sessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
//会话已经建立,可以开始预览了
cameraCaptureSession = session;
//设置自动对焦
previewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//发送预览请求
try {
cameraCaptureSession.setRepeatingRequest(previewBuilder.build(), null, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
//关闭会话
session.close();
cameraCaptureSession = null;
cameraDevice.close();
cameraDevice = null;
}
};
2.开启预览,首先通过CameraDevice对象获得一个CaptureRequest.Builder对象,方法createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW),CameraDevice.TEMPLATE_PREVIEW 表示构建的请求为预览请求,此外还有拍照请求。录像请求等。
参数 | 功能 |
TEMPLATE_PREVIEW | 适用于配置预览的模板 |
TEMPLATE_RECORD | 适用于视频录制的模板 |
TEMPLATE_STILL_CAPTURE | 适用于拍照的模板 |
TEMPLATE_VIDEO_SNAPSHOT | 适用于在录制视频过程中支持拍照的模板。 |
TEMPLATE_MANUAL | 适用于希望自己手动配置大部分参数的模板。 |
之后需要设置预览数据的输出界面。然后通过CameraDevice创建createCaptureSession,传递三个参数,第一个为数据接受的surface列表,第二个为Session创建状态的回调。第三个参数用于指定在哪一个线程中执行。在session的回调中,onConfigured方法表示会话建立成功。可以获取到这个CaptureSession,然后通过其setRepeatingRequest方法发送一个预览的请求,三个参数,第一个是之前构建的CaptureRequest.Builder 对象.build(),第二个是回调,第三个还是用于指定在那个线程中执行。当然在建立失败方法中执行一些错误处理的方法。只此,预览流程已经完成。
接下里时拍照流程。
3.拍照流程。
/**
* 拍照
*/
private void takePhoto() {
try {
//创建拍照请求的Request
captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
//设置拍照的画面
captureBuilder.addTarget(imageReader.getSurface());
//自动对焦
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//自动曝光
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
//获取手机方向
int rotation = getWindowManager().getDefaultDisplay().getRotation();
//根据设备方向计算设置照片的方向
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));
//停止预览
cameraCaptureSession.stopRepeating();
//拍照
cameraCaptureSession.capture(captureBuilder.build(), captureCallback, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private final CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
super.onCaptureProgressed(session, request, partialResult);
}
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
try {
//自动对焦
captureBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
//重新打开预览
cameraCaptureSession.setRepeatingRequest(previewBuilder.build(), null, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
super.onCaptureFailed(session, request, failure);
cameraCaptureSession.close();
cameraCaptureSession = null;
cameraDevice.close();
cameraDevice = null;
}
};
首先,还是使用之前获得的CameraDevice对象 创建CaptureRequest.Builder,只不过,这里的参数为, cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE),是一个拍照请求。然后设置这个拍照请求,比如画面。对焦,曝光等。之后先关闭预览,然后通过
CameraCaptureSession的Capture方法发送一个拍照的请求。
cameraCaptureSession.capture(captureBuilder.build(), captureCallback, handler);有三个参数,第一个为之前构建的CaptureRequest.Builder对象.build,第二个同样是一个请求状态的回调,第三个参数为指定在哪一个线程执行。CameraCaptureSession.CaptureCallback在此状态回调中,拍照完成方法onCaptureCompleted()里,重新打开预览。那么我们的图像到哪里去了呢?
就是之前设置的 imageReader.setOnImageAvailableListener(),在里面的onImageAvailable可拿到拍照的数据。至此拍照流程完成。
3.录像流程,第一步还是CameraDevice对象 创建CaptureRequest.Builder,这里参数变为CameraDevice.TEMPLATE_RECORD,代表录像,之后初始化MediaRecorder对象。然后设置MediaRecorder对象的一系列属性,然后拿到MediaRecorder对象的surface,CaptureRequest.Builder添加两个surface,一个为MediaRecorder的,还有一个为surfaceView的surface,之后还是通过CameraDevice的createCaptureSession,创建会话,传递三个参数,在创建成功方法里,发送重复捕获请求,将这次创建的CaptureRequest.Builder对象传递进去。开始录像时调用MediaRecorder的start方法即可。 关闭调用mediaRecorder.release();
public class VideoRecorderUtils {
private MediaRecorder mediaRecorder;
private SurfaceHolder.Callback callback;
private SurfaceView surfaceView;
private CameraDevice mCameraDevice;
private int height;
private int width;
List<Surface> surfaces = new ArrayList<>();
public static Size WH_720X480 = new Size(720, 480);
private CaptureRequest.Builder mPreviewBuilder;
private CaptureRequest mCaptureRequest;
private CameraCaptureSession mPreviewSession;
public void create(SurfaceView surfaceView, CameraDevice cameraDevice, Size size) {
this.surfaceView = surfaceView;
mCameraDevice = cameraDevice;
//创建录制的session会话中的请求
try {
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
} catch (CameraAccessException e) {
e.printStackTrace();
}
height = size.getHeight();
width = size.getWidth();
mediaRecorder = new MediaRecorder();
}
public void stopRecord() {
mediaRecorder.release();
mediaRecorder = null;
mediaRecorder = new MediaRecorder();
surfaces.clear();
}
public void stop() {
if (mediaRecorder != null) {
mediaRecorder.release();
}
}
public void destroy() {
if (mediaRecorder != null) {
mediaRecorder.release();
mediaRecorder = null;
}
}
/**
* @param mainActivity
*/
@RequiresApi(api = Build.VERSION_CODES.R)
public void startRecord(MainActivity mainActivity, Handler handler) {
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mediaRecorder.setVideoSize(width, height);
mediaRecorder.setVideoFrameRate(24);
mediaRecorder.setVideoEncodingBitRate(700 * 1024);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setOrientationHint(90);
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
String fname = "video_" + sdf.format(new Date()) + ".mp4";
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + File.separator + fname);
Log.d("xxxxxx", "startRecord: "+file.getAbsolutePath());
mediaRecorder.setOutputFile(file);
try {
mediaRecorder.prepare();
mediaRecorder.start();
} catch (IOException e) {
e.printStackTrace();
}
review(handler);
}
public void review(Handler handler) {
Surface previewSurface = surfaceView.getHolder().getSurface();
surfaces.add(previewSurface);
mPreviewBuilder.addTarget(previewSurface);
Surface recorderSurface = mediaRecorder.getSurface();
surfaces.add(recorderSurface);
mPreviewBuilder.addTarget(recorderSurface);
try {
mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
try {
//创建捕获请求
mCaptureRequest = mPreviewBuilder.build();
mPreviewSession = session;
//设置反复捕获数据的请求,这样预览界面就会一直有数据显示
mPreviewSession.setRepeatingRequest(mCaptureRequest, null, handler);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
}
}, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
//清除预览Session
private void closePreviewSession() {
if (mPreviewSession != null) {
mPreviewSession.close();
mPreviewSession = null;
}
}
}
前面漏了两个方法。
/**
* 关闭相机
*/
private void closeCamera() {
//关闭捕捉会话
if (cameraCaptureSession != null) {
cameraCaptureSession.close();
cameraCaptureSession = null;
}
//关闭相机
if (cameraDevice != null) {
cameraDevice.close();
cameraDevice = null;
}
//关闭拍照处理器
if (imageReader != null) {
imageReader.close();
imageReader = null;
}
}
public class ImageSaver implements Runnable {
private Image mImage;
private File mFile;
public ImageSaver(Image image) {
this.mImage = image;
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public void run() {
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
FileOutputStream output = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
String fname = "IMG_" + sdf.format(new Date()) + ".jpg";
mFile = new File(getApplication().getExternalFilesDir(null), fname);
try {
output = new FileOutputStream(mFile);
output.write(bytes);
} catch (IOException e) {
e.printStackTrace();
} finally {
mImage.close();
if (null != output) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 把file里面的图片插入到系统相册中
try {
MediaStore.Images.Media.insertImage(getApplication().getContentResolver(),
mFile.getAbsolutePath(), fname, null);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 通知相册更新
getApplication().sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mFile)));
}
}
首次打开可能会出现没有预览画面的情况,完全关闭后,在次打开就有图像显示了。
源码:https://github.com/dqx-eterning/CustomCamera CSDN资源里也有。javascript:void(0)
部分转载自javascript:void(0)