Android CameraX入门、双指/双击缩放、点击对焦、切换比例、二维码识别...

6,047 阅读4分钟

一、简介

最近CameraX发布了第一个beta版本,相较于alpha版本的api疯狂改动慢慢趋于稳定。 本篇文章主要内容包含CameraX的简单拍照保存、图像分析(可用于二维码识别等用途)、缩放、对焦等相关内容

注:

  1. 当前本文使用的CameraX版本为1.0.0-beta01
  2. 修改相机比例、切换摄像头、二维码识别等以及最新版本使用请点击底部链接查看Demo。

二、基础使用

  1. 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}"
    
  2. 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" />
    <!-- ... -->
    
  3. 构建图像捕获用例

    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();
    }
    
  4. 构建图像分析用例(可用于二维码识别等用途)

    注意: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();
    	});
    }
    
  5. 初始化相机

    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);
    
    }
    
  6. 绑定预览

    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();
    }
    
  7. 拍照保存图片

    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实现手势事件(点击、双击、缩放...)

  1. 回掉接口

    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);
    }
    
  2. 手势监听代码

    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:github.com/sdwfqin/And…