Android 相机CameraX框架
CameraX是Jetpack 支持库,利用的是 camera2 的功能,它具有生命周期的感应,使用更加简单,代码量也减少了不少。可以灵活的录制视频和拍照
官方文档
https://developer.android.google.cn/media/camera/camerax?hl=ro
build.gradle的dependencies中引入框架
java
def cameraxVersion = "1.1.0-alpha05";
implementation "androidx.camera:camera-core:${cameraxVersion}"
implementation "androidx.camera:camera-camera2:${cameraxVersion}"
implementation "androidx.camera:camera-lifecycle:${cameraxVersion}"
//CameraX添加依赖
implementation "androidx.camera:camera-view:1.3.0-alpha07"
implementation 'androidx.camera:camera-video:'
implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
配置CameraXConfig
使用 setAvailableCameraLimiter() 优化启动延迟时间。
使用 setCameraExecutor() 向 CameraX 提供应用执行器。
将默认调度器处理程序替换为 setSchedulerHandler()。
使用 setMinimumLoggingLevel() 更改日志记录级别。
图像预览PreviewView
选择相机并绑定生命周期和用例:
创建 Preview。
指定所需的相机 LensFacing 选项。
将所选相机和任意用例绑定到生命周期。
将 Preview 连接到 PreviewView。
java
void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
Preview preview = new Preview.Builder()
.build();
//设置缩放类型可选FIT_CENTER、FIT_START 和 FIT_END,默认缩放类型是 FILL_CENTER。
preview .setScaleType(PreviewView.ScaleType.FIT_CENTER);
CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
Camera camera = cameraProvider.bindToLifecycle((LifecycleOwner)this, cameraSelector, preview);
}
图像分析ImageAnalysis
设置图片输出尺寸,输出格式,旋转角度...
输出格式:CameraX 可通过 setOutputImageFormat(int) 支持 YUV_420_888 和 RGBA_8888。默认格式为 YUV_420_888。
注意:使用 ProcessCameraProvider.bindToLifecycle() 函数将 ImageAnalysis 绑定到现有的 AndroidX 生命周期
java
imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// .setTargetResolution(Size(1280, 720))设置实际的尺寸
.setTargetAspectRatio(AspectRatio.RATIO_4_3) //设计宽高比
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.build()
//创建分析器
imageAnalysis!!.setAnalyzer(
cameraExecutor!!,
ImageAnalysis.Analyzer { imageProxy: ImageProxy ->
//通过调用 ImageProxy.close() 将 ImageProxy 发布到 CameraX
imageProxy.close()
})
...
cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, imageAnalysis, preview);
图像拍摄输出保存
拍摄的配置参数,例如闪光灯、连续自动对焦、零快门延迟等。
java
imageCapture = new ImageCapture.Builder()
//控制闪光灯模式 FLASH_MODE_ON(拍照时,闪光灯会亮起),
//FLASH_MODE_OFF 闪光灯关闭,FLASH_MODE_AUTO:在弱光环境下拍摄时,自动开启闪光灯。
.setFlashMode(ImageCapture.FLASH_MODE_ON)
//ImageCapture.Builder.setCaptureMode() 可用于配置拍摄照片时所采用的拍摄模式:
//CAPTURE_MODE_MINIMIZE_LATENCY:缩短图片拍摄的延迟时间。
//CAPTURE_MODE_MAXIMIZE_QUALITY:提高图片拍摄的图片质量。
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build();
拍照保存图片的方式有2种,
takePicture(OutputFileOptions, Executor, OnImageSavedCallback):此方法将拍摄的图片保存到提供的文件位置。
java
val name = System.currentTimeMillis().toString() + ".jpg"
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.put(
MediaStore.MediaColumns.RELATIVE_PATH,
FileUtils.getSystemPicDataPath() //图片路径
)
}
//图片输出
val outputFileOptions = OutputFileOptions.Builder(
contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
.build()
imageCapture!!.takePicture(
outputFileOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
//content://media/external/images/media/125
//Pictures/CameraXImage
//aaa图片路径.jpg
printLog(" aaa图片路径--" + FileUtils.getSystemPicDataPath() + name)
CoroutineScope(Dispatchers.IO).launch {
val blo = FileUtils.copyFile(
FileUtils.getSystemPicDataPath() + name,
FileUtils.getAppPicDataPath() + name
)
withContext(Dispatchers.Main) {
printLog("文件复制成功$blo")
if (blo) {
File(FileUtils.getAppPicDataPath() + name).delete()
binding.ivCamera.visibility = View.VISIBLE
FileUtils.showGlidePic(
this@CameraActivity,
FileUtils.getAppPicDataPath() + name,
binding.ivCamera
)
}
}
}
// printLog( Objects.requireNonNull(outputFileResults.savedUri).toString())
}
override fun onError(exception: ImageCaptureException) {
printLog(exception.toString())
}
})
另一种takePicture(Executor, OnImageCapturedCallback):此方法为拍摄的图片提供内存缓冲区。
java
imageCapture!!.takePicture(ContextCompat.getMainExecutor(this), object :
ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image)
// println(image)
// printLog("文件保存成功${image.format}")
if (image.format === ImageFormat.JPEG) {
val planes = image.planes
val buffer = planes[0].buffer
val size = buffer.remaining()
val jpeg = ByteArray(size)
buffer[jpeg, 0, size]
val bitmap: Bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.size);
val path=FileUtils.getAppPicDataPath() + name
//将bitmap保存为图片
val isFlag=FileUtils.bitmapToFile(bitmap,path)
printLog("文件保存成功$isFlag")
if (isFlag){
binding.ivCamera.visibility = View.VISIBLE
//存储图片成功,现实图片
FileUtils.showGlidePic(
this@CameraActivity,
path,
binding.ivCamera
)
}else{
toast("获取图片失败")
}
}
image.close()
}
override fun onError(exception: ImageCaptureException) {
printLog(exception.toString())
}
})
完整代码演示:
xml文件自定义UI
java
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.activity.CameraVideo2Activity">
<androidx.camera.view.PreviewView
android:id="@+id/preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/takeVideoBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="录像"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/takePhotoBtn"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias=".9" />
<Button
android:id="@+id/takePhotoBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="拍照"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/takeVideoBtn"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias=".9" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity代码
java
public class CameraVideo2Activity extends AppCompatActivity {
//按钮
Button takePhotoButton;
Button takeVideoButton;
//预览
PreviewView previewView;
//权限
private static final String[] REQUIRE_PERMISSIONS = new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO};
public static final int REQUEST_CODE_PERMISSIONS = 10;
//capture
ImageCapture imageCapture;
ListenableFuture<ProcessCameraProvider> processCameraProviderListenableFuture;
//录像
VideoCapture videoCapture;
Recording recording;
//executor & imageAnalysis
private ExecutorService cameraExecutor;
private ImageAnalysis imageAnalysis;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera_video2);
//绑定控件
takePhotoButton = findViewById(R.id.takePhotoBtn);
takeVideoButton = findViewById(R.id.takeVideoBtn);
previewView = findViewById(R.id.preview_view);
takePhotoButton.setOnClickListener(v -> takePhoto());
takeVideoButton.setOnClickListener(v -> takeVideo());
//获取权限
if (havePermissions()) {
initCamera();
} else {
ActivityCompat.requestPermissions(this, REQUIRE_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
}
//executor实例
cameraExecutor = Executors.newSingleThreadExecutor();
}
@Override
protected void onDestroy() {
super.onDestroy();
cameraExecutor.shutdown();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSIONS) {
initCamera();
} else {
finish();
}
}
//判断权限是否获取
private boolean havePermissions() {
for (String permission : REQUIRE_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
//初始化Camera
@SuppressLint("UnsafeOptInUsageError")
private void initCamera() {
///实例化(可以设置许多属性)
imageCapture = new ImageCapture.Builder()
//控制闪光灯模式 FLASH_MODE_ON(拍照时,闪光灯会亮起), FLASH_MODE_OFF 闪光灯关闭
.setFlashMode(ImageCapture.FLASH_MODE_ON)
.build();
Recorder recorder = new Recorder.Builder().build();
videoCapture = VideoCapture.withOutput(recorder);
processCameraProviderListenableFuture = ProcessCameraProvider.getInstance(this);
processCameraProviderListenableFuture.addListener(() -> {
try {
//配置预览(https://developer.android.google.cn/training/camerax/preview?hl=zh-cn)
previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
//绑定到生命周期
ProcessCameraProvider processCameraProvider = processCameraProviderListenableFuture.get();
//图片分析
initImageAnalysis();
//设置旋转
setOrientationEventListener();
//剪裁矩形(拍摄之后,对图片进行裁剪)
ViewPort viewPort = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
viewPort = new ViewPort.Builder(
new Rational(100, 100), getDisplay().getRotation()
).build();
} else {
viewPort = new ViewPort.Builder(
new Rational(100, 100), Surface.ROTATION_0
).build();
}
UseCaseGroup useCaseGroup = new UseCaseGroup.Builder()
.addUseCase(preview)
.addUseCase(imageAnalysis)
.addUseCase(imageCapture)
.addUseCase(videoCapture)
.setViewPort(viewPort)
.build();
processCameraProvider.unbindAll();
Camera camera = processCameraProvider.bindToLifecycle(CameraVideo2Activity.this, CameraSelector.DEFAULT_BACK_CAMERA, useCaseGroup);//DEFAULT_BACK_CAMERA 后置摄像头
//Camera
CameraControl cameraControl = camera.getCameraControl();
CameraInfo cameraInfo = camera.getCameraInfo();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, ContextCompat.getMainExecutor(this));
}
//图片分析
@SuppressLint("UnsafeOptInUsageError")
private void initImageAnalysis() {
imageAnalysis = new ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
imageAnalysis.setAnalyzer(cameraExecutor, imageProxy -> {
int rotationDegrees = imageProxy.getImageInfo().getRotationDegrees();
imageProxy.close();
});
}
///旋转
//orientation为北为0,顺时针度数0-360
//Surface.ROTATION_270将拍摄好的图片顺时针旋转270度
private void setOrientationEventListener() {
OrientationEventListener orientationEventListener = new OrientationEventListener(this) {
@Override
public void onOrientationChanged(int orientation) {
int rotation;
if (orientation >= 45 && orientation < 135) {
rotation = Surface.ROTATION_270;
} else if (orientation >= 135 && orientation < 225) {
rotation = Surface.ROTATION_180;
} else if (orientation >= 225 && orientation < 315) {
rotation = Surface.ROTATION_90;
} else {
rotation = Surface.ROTATION_0;
}
imageCapture.setTargetRotation(rotation);
}
};
orientationEventListener.enable();
}
//拍照
private void takePhoto() {
if (imageCapture != null) {
//ContentValues
String name = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.SIMPLIFIED_CHINESE).format(System.currentTimeMillis());
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/CameraXImage");
}
//图片输出
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions
.Builder(getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
.build();
imageCapture.takePicture(outputFileOptions, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
//content://media/external/images/media/125
//Pictures/CameraXImage
//aaa图片路径5.jpg
System.out.println("图片路径--"+name+".jpg");
Log.i("camera", Objects.requireNonNull(outputFileResults.getSavedUri()).toString());
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
Log.e("camera", exception.toString());
}
});
}
}
//录像
private void takeVideo() {
if (videoCapture != null) {
takeVideoButton.setEnabled(false);
if (recording != null) {
recording.stop();
recording = null;
return;
}
String name = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.SIMPLIFIED_CHINESE).format(System.currentTimeMillis()) + ".mp4";
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "Movies/CameraX-Video");
}
MediaStoreOutputOptions mediaStoreOutputOptions = new MediaStoreOutputOptions
.Builder(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build();
Recorder recorder = (Recorder) videoCapture.getOutput();
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, REQUIRE_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
return;
}
recording = recorder.prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
.start(ContextCompat.getMainExecutor(this), videoRecordEvent -> {
if (videoRecordEvent instanceof VideoRecordEvent.Start) {
takeVideoButton.setText("停止");
takeVideoButton.setEnabled(true);
} else if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {
if (((VideoRecordEvent.Finalize) videoRecordEvent).hasError()) {
if (recording != null) {
recording.close();
recording = null;
}
} else {
//视频为content://media/external/video/media/122
//Movies/CameraX-Video/name
String msg = "视频为" + ((VideoRecordEvent.Finalize) videoRecordEvent).getOutputResults().getOutputUri();
Log.i("camera", msg);
Log.i("camera", "视频路径为"+name);
}
takeVideoButton.setEnabled(true);
takeVideoButton.setText("录像");
}
});
}
}
}