一、简介
最近CameraX发布了第一个beta版本,相较于alpha版本的api疯狂改动慢慢趋于稳定。
本篇文章主要内容包含CameraX
的简单拍照保存、图像分析(可用于二维码识别等用途)、缩放、对焦等相关内容
注:
- 当前本文使用的
CameraX
版本为1.0.0-beta01
。 - 修改相机比例、切换摄像头、二维码识别等以及最新版本使用请点击底部链接查看Demo。
- Camera-View Version 1.0.0-alpha19开始PreviewView被标注了final,无法继承,故触摸事件改为了setOnTouchListener,详细改动可以查看底部demo
二、基础使用
-
gradle依赖
def camerax_version = "1.0.0-beta01" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-view:1.0.0-alpha08" implementation "androidx.camera:camera-lifecycle:${camerax_version}"
-
xml布局
<!-- ... --> <androidx.camera.view.PreviewView android:id="@+id/view_finder" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <!-- ... -->
-
构建图像捕获用例
private void initImageCapture() { // 构建图像捕获用例 mImageCapture = new ImageCapture.Builder() .setFlashMode(ImageCapture.FLASH_MODE_AUTO) .setTargetAspectRatio(AspectRatio.RATIO_4_3) .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) .build(); // 旋转监听 OrientationEventListener orientationEventListener = new OrientationEventListener((Context) this) { @Override public void onOrientationChanged(int orientation) { int rotation; // Monitors orientation values to determine the target rotation value 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; } mImageCapture.setTargetRotation(rotation); } }; orientationEventListener.enable(); }
-
构建图像分析用例(可用于二维码识别等用途)
注意:Analyzer回掉方法中如果不调用
image.close()
将不会获取到下一张图片private void initImageAnalysis() { mImageAnalysis = new ImageAnalysis.Builder() // 分辨率 .setTargetResolution(new Size(1280, 720)) // 仅将最新图像传送到分析仪,并在到达图像时将其丢弃。 .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build(); mImageAnalysis.setAnalyzer(executor, image -> { int rotationDegrees = image.getImageInfo().getRotationDegrees(); LogUtils.e("Analysis#rotationDegrees", rotationDegrees); ImageProxy.PlaneProxy[] planes = image.getPlanes(); ByteBuffer buffer = planes[0].getBuffer(); // 转为byte[] // byte[] b = new byte[buffer.remaining()]; // LogUtils.e(b); // TODO: 分析完成后关闭图像参考,否则会阻塞其他图像的产生 // image.close(); }); }
-
初始化相机
private Executor executor; ... private void initCamera() { executor = ContextCompat.getMainExecutor(this); cameraProviderFuture = ProcessCameraProvider.getInstance(this); cameraProviderFuture.addListener(() -> { try { ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); // 绑定预览 bindPreview(cameraProvider); } catch (ExecutionException | InterruptedException e) { // No errors need to be handled for this Future. // This should never be reached. } }, executor); }
-
绑定预览
private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) { Preview preview = new Preview.Builder() .build(); preview.setSurfaceProvider(mViewFinder.getPreviewSurfaceProvider()); CameraSelector cameraSelector = new CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK) .build(); // mImageCapture 图像捕获用例 // mImageAnalysis 图像分析用例 Camera camera = cameraProvider.bindToLifecycle(this, cameraSelector, mImageCapture, mImageAnalysis, preview); mCameraInfo = camera.getCameraInfo(); mCameraControl = camera.getCameraControl(); initCameraListener(); }
-
拍照保存图片
public void saveImage() { File file = new File(getExternalMediaDirs()[0], System.currentTimeMillis() + ".jpg"); ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(file).build(); mImageCapture.takePicture(outputFileOptions, executor, new ImageCapture.OnImageSavedCallback() { @Override public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) { String msg = "图片保存成功: " + file.getAbsolutePath(); showMsg(msg); LogUtils.d(msg); Uri contentUri = Uri.fromFile(new File(file.getAbsolutePath())); Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, contentUri); sendBroadcast(mediaScanIntent); } @Override public void onError(@NonNull ImageCaptureException exception) { String msg = "图片保存失败: " + exception.getMessage(); showMsg(msg); LogUtils.e(msg); } } ); }
三、自定义PreviewView实现手势事件(点击、双击、缩放…)
-
回掉接口
public interface CustomTouchListener { /** * 放大 */ void zoom(); /** * 缩小 */ void ZoomOut(); /** * 点击 */ void click(float x, float y); /** * 双击 */ void doubleClick(float x, float y); /** * 长按 */ void longClick(float x, float y); }
-
手势监听代码
public class CameraXCustomPreviewView extends PreviewView { private GestureDetector mGestureDetector; /** * 缩放相关 */ private float currentDistance = 0; private float lastDistance = 0; ... 省略部分构造方法 public CameraXCustomPreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mGestureDetector = new GestureDetector(context, onGestureListener); mGestureDetector.setOnDoubleTapListener(onDoubleTapListener); // mScaleGestureDetector = new ScaleGestureDetector(context, onScaleGestureListener); // 解决长按屏幕无法拖动,但是会造成无法识别长按事件 // mGestureDetector.setIsLongpressEnabled(false); } @Override public boolean onTouchEvent(MotionEvent event) { // 接管onTouchEvent return mGestureDetector.onTouchEvent(event); } GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() { @Override public boolean onDown(MotionEvent e) { LogUtils.i("onDown: 按下"); return true; } @Override public void onShowPress(MotionEvent e) { LogUtils.i("onShowPress: 刚碰上还没松开"); } @Override public boolean onSingleTapUp(MotionEvent e) { LogUtils.i("onSingleTapUp: 轻轻一碰后马上松开"); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { LogUtils.i("onScroll: 按下后拖动"); // 大于两个触摸点 if (e2.getPointerCount() >= 2) { //event中封存了所有屏幕被触摸的点的信息,第一个触摸的位置可以通过event.getX(0)/getY(0)得到 float offSetX = e2.getX(0) - e2.getX(1); float offSetY = e2.getY(0) - e2.getY(1); //运用三角函数的公式,通过计算X,Y坐标的差值,计算两点间的距离 currentDistance = (float) Math.sqrt(offSetX * offSetX + offSetY * offSetY); if (lastDistance == 0) {//如果是第一次进行判断 lastDistance = currentDistance; } else { if (currentDistance - lastDistance > 10) { // 放大 if (mCustomTouchListener != null) { mCustomTouchListener.zoom(); } } else if (lastDistance - currentDistance > 10) { // 缩小 if (mCustomTouchListener != null) { mCustomTouchListener.ZoomOut(); } } } //在一次缩放操作完成后,将本次的距离赋值给lastDistance,以便下一次判断 //但这种方法写在move动作中,意味着手指一直没有抬起,监控两手指之间的变化距离超过10 //就执行缩放操作,不是在两次点击之间的距离变化来判断缩放操作 //故这种将本次距离留待下一次判断的方法,不能在两次点击之间使用 lastDistance = currentDistance; } return true; } @Override public void onLongPress(MotionEvent e) { LogUtils.i("onLongPress: 长按屏幕"); if (mCustomTouchListener != null) { mCustomTouchListener.longClick(e.getX(), e.getY()); } } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { LogUtils.i("onFling: 滑动后松开"); currentDistance = 0; lastDistance = 0; return true; } }; GestureDetector.OnDoubleTapListener onDoubleTapListener = new GestureDetector.OnDoubleTapListener() { @Override public boolean onSingleTapConfirmed(MotionEvent e) { LogUtils.i("onSingleTapConfirmed: 严格的单击"); if (mCustomTouchListener != null) { mCustomTouchListener.click(e.getX(), e.getY()); } return true; } @Override public boolean onDoubleTap(MotionEvent e) { LogUtils.i("onDoubleTap: 双击"); if (mCustomTouchListener != null) { mCustomTouchListener.doubleClick(e.getX(), e.getY()); } return true; } @Override public boolean onDoubleTapEvent(MotionEvent e) { LogUtils.i("onDoubleTapEvent: 表示发生双击行为"); return true; } }; }
四、双指滑动/双击缩放、点击对焦
private void initCameraListener() {
LiveData<ZoomState> zoomState = mCameraInfo.getZoomState();
float maxZoomRatio = zoomState.getValue().getMaxZoomRatio();
float minZoomRatio = zoomState.getValue().getMinZoomRatio();
LogUtils.e(maxZoomRatio);
LogUtils.e(minZoomRatio);
mViewFinder.setCustomTouchListener(new CameraXCustomPreviewView.CustomTouchListener() {
@Override
public void zoom() {
float zoomRatio = zoomState.getValue().getZoomRatio();
if (zoomRatio < maxZoomRatio) {
mCameraControl.setZoomRatio((float) (zoomRatio + 0.1));
}
}
@Override
public void ZoomOut() {
float zoomRatio = zoomState.getValue().getZoomRatio();
if (zoomRatio > minZoomRatio) {
mCameraControl.setZoomRatio((float) (zoomRatio - 0.1));
}
}
@Override
public void click(float x, float y) {
// 1.0.0-beta08 createMeteringPointFactory()方法已删除,Demo改为使用getMeteringPointFactory()
MeteringPointFactory factory = mBinding.viewFinder.createMeteringPointFactory(mCameraSelector);
MeteringPoint point = factory.createPoint(x, y);
FocusMeteringAction action = new FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF)
// auto calling cancelFocusAndMetering in 3 seconds
.setAutoCancelDuration(3, TimeUnit.SECONDS)
.build();
mFocusView.startFocus(new Point((int) x, (int) y));
ListenableFuture future = mCameraControl.startFocusAndMetering(action);
future.addListener(() -> {
try {
FocusMeteringResult result = (FocusMeteringResult) future.get();
if (result.isFocusSuccessful()) {
mFocusView.onFocusSuccess();
} else {
mFocusView.onFocusFailed();
}
} catch (Exception e) {
}
}, executor);
}
@Override
public void doubleClick(float x, float y) {
// 双击放大缩小
float zoomRatio = zoomState.getValue().getZoomRatio();
if (zoomRatio > minZoomRatio) {
mCameraControl.setLinearZoom(0f);
} else {
mCameraControl.setLinearZoom(0.5f);
}
}
@Override
public void longClick(float x, float y) {
}
});
}
五、闪光灯
switch (mImageCapture.getFlashMode()) {
case ImageCapture.FLASH_MODE_AUTO:
mImageCapture.setFlashMode(ImageCapture.FLASH_MODE_ON);
mBtnLight.setText("闪光灯:开");
break;
case ImageCapture.FLASH_MODE_ON:
mImageCapture.setFlashMode(ImageCapture.FLASH_MODE_OFF);
mBtnLight.setText("闪光灯:关");
break;
case ImageCapture.FLASH_MODE_OFF:
mImageCapture.setFlashMode(ImageCapture.FLASH_MODE_AUTO);
mBtnLight.setText("闪光灯:自动");
break;
}
六、Demo
Demo:https://github.com/sdwfqin/AndroidQuick/tree/4.x/app