0%

CameraX入门、双指/双击缩放、点击对焦

一、简介

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

注:

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

二、基础使用

  1. gradle依赖

    1
    2
    3
    4
    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布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- ... -->
    <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. 构建图像捕获用例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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()将不会获取到下一张图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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. 初始化相机

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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. 绑定预览

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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. 拍照保存图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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. 回掉接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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. 手势监听代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    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;
    }
    };
    }

四、双指滑动/双击缩放、点击对焦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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) {
// TODO 对焦
MeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(1.0f, 1.0f);
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) {

}
});
}

五、闪光灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

六、源码

源码:https://github.com/sdwfqin/AndroidQuick/tree/4.x/app