🔥利用Camerax实现一个相机应用,爽歪歪~Android

avatar
前端工程师

文前一问,大家最近的基金和股票表现如何呢? 哈哈哈哈哈

image.png

Camerax是什么?

CameraX 是一个 Jetpack 库,主要用于高效的开发相机应用,相比于Camera2与Camera1,减少了心智负担,我曾经用过Camera2与Camera1分别去尝试开发一个就有基础功能的相机应用,耗费了大量时间,相关类太多,且对于不同厂商来说很可能有具有不同的效果,需要做很多兼容处理,而在CameraX上,这些问题发生的概率要更小。 简单总结就是

  • 使用简单,无需过多关注相机的配置(这里就可以解决相机预览变形和旋转的问题)
  • 生命周期绑定,这里可以省去打开关闭相机和对相机进行生命周期管理的工作
  • 兼容性问题要更少
  • 封装合理,减少心智负担

Camerax的使用

使用CameraX自定义相机也可以简单分为以下几步:

  • 权限配置
  • 初始化相机
  • 预览设置
  • 拍照设置

权限配置

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
   android:maxSdkVersion="28" />

其中:android.hardware.camera.any的配置是申明CameraX可以使用设备上的任何一个摄像头,不论是前置还是后置,结合动态申请权限库完成这一步,具体不再阐述。

初始化相机

  // 相机进程提供者
  private ProcessCameraProvider cameraProvider;
  // 相机进程提供者 Future,相机初始化完成后返回相机进程提供者实例化对象
  private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
  
  // 初始化方法
  private void initProcessCameraProvider(){
        // 获取相机进程提供者Future
        cameraProviderFuture = ProcessCameraProvider.getInstance(context);
        cameraProviderFuture.addListener(() -> {
            try {
                cameraProvider = cameraProviderFuture.get();
                // 绑定预览界面
                bindPreview(cameraProvider);
            } catch (ExecutionException | InterruptedException e) {
                // No errors need to be handled for this Future.
                // This should never be reached.
            }
        }, ContextCompat.getMainExecutor(context));
    }

此处主要是获取相机进程提供者,这是非常重要的,因为Camerax自己已经封装好了生命周期的管理,需要将其他Camerax的相关组件绑定到ProcessCameraProvider上。

预览设置

  // 预览组件
  private PreviewView previewView;
  // 相机模组选择器
  private CameraSelector cameraSelector;
  // 预览对象
  private Preview preview;
  // 相机对象
  private Camera camera;

  private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
        // 初始化相机模组选择器 并选择后主摄像头
        cameraSelector = new CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build();
        // 初始化预览界面 
        initPreView();
        // 设定 全局比例
        ...
        setRatio(videoWidth, videoHeight)
  }
  // 初始化预览界面
   private void initPreView() {
        previewView = new PreviewView(getContext());
        addView(previewView);
        previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
        ...
   }
   // 设定预览界面 与 输出预览图像的比例
   private void setPreviewRatio(int width,  int height, Size size) {
        LayoutParams frameLayout = new FrameLayout.LayoutParams(width, height);
        frameLayout.gravity = Gravity.CENTER;
        previewView.setLayoutParams(frameLayout);
        preview = new Preview.Builder()
                // 设定预览比例输出的预览图像比例
                .setTargetResolution(size)
                .build();
                               preview.setSurfaceProvider(previewView.getSurfaceProvider());
    }
   // 绑定预览对象 输出预览画面
    camera = cameraProvider.bindToLifecycle((LifecycleOwner) getContext(), cameraSelector, preview, imageCapture);
    imageCapture 是什么呢???????请看下文
    

此处所做处理主要是以下几步

  • 指定预览相机
  • 设定预览界面大小(或者比例)
  • 绑定cameraProvider到ProcessCameraProvider

拍照设置

   // 画面捕捉对象
   private ImageCapture imageCapture;
   // 出事化捕捉界面 改改参数试试
   private void initImageCapture(Size size) {
        imageCapture = new ImageCapture.Builder()
                //优化捕获速度,可能降低图片质量
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
                .setTargetResolution(size)
                //设置初始的旋转角度
                .setTargetRotation(Surface.ROTATION_0)
                .build();

    }
    // 公开的拍照方法
    public void takePicture() {
        // 这是我自己写的工具类,不重要
        File file = new File(ToolsFile.createImagePathUri(getContext()));
        ImageCapture.OutputFileOptions outputFileOptions =
                new ImageCapture.OutputFileOptions.Builder(file)
                        .build();
        imageCapture.takePicture(outputFileOptions, (Executor) cameraExecutor,
                new ImageCapture.OnImageSavedCallback() {
                    @Override
                    public void onImageSaved(ImageCapture.OutputFileResults outputFileResults) {
                        try {
                            Uri contentUri = outputFileResults.getSavedUri();
                            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                                ApplicationData.globalContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, contentUri));
                            } else {

                                MediaStore.Images.Media.insertImage(ApplicationData.globalContext.getContentResolver(),
                                        file.getAbsolutePath(), file.getName(), null);

                            }
                            onFileSaved(contentUri);
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void onError(ImageCaptureException error) {


                    }
                }
        );
    }

这部分主要是做了这几步来实现捕捉画面,存储至相册的能力

  • 设置捕捉对象
  • 设置拍照保存方法

由上,我们基于Camerax就实现了一个基础相机,但是在我们的印象中相机最基本的功能里还得有点击聚焦才算是完整的基础的可用的相机。 跟着我的步骤一起来完成最后的收尾工作

  • previewView.setOnTouchListener,监听previewView触摸
  • 封装一个手势类,处理预览窗口的触摸事件
  • 设置焦点,展示焦点窗口

监听previewView触摸

  // 预览画面事件监听
  private void initListener() {
        previewView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                LiveData<ZoomState> zoomState = camera.getCameraInfo().getZoomState();
                float maxZoomRatio = zoomState.getValue().getMaxZoomRatio();
                float minZoomRatio = zoomState.getValue().getMinZoomRatio();
                // 自定义预览界面touch类
                return new CameraXGestureDetector(getContext()) {
                    @Override
                    void zoom() {
                        float zoomRatio = zoomState.getValue().getZoomRatio();
                        if (zoomRatio < maxZoomRatio) {
                            camera.getCameraControl().setZoomRatio((float) (zoomRatio + 0.1));
                        }
                    }

                    @Override
                    void zoomOut() {
                        float zoomRatio = zoomState.getValue().getZoomRatio();
                        if (zoomRatio > minZoomRatio) {
                            camera.getCameraControl().setZoomRatio((float) (zoomRatio - 0.1));
                        }
                    }

                    @Override
                    void click(float x, float y) {
                        Log.e("click", "click");

                    }

                    @Override
                    void doubleClick(float x, float y) {
                        Log.e("doubleClick", "doubleClick");
                        float zoomRatio = zoomState.getValue().getZoomRatio();
                        if (zoomRatio > minZoomRatio) {
                            camera.getCameraControl().setLinearZoom(0f);
                        } else {
                            camera.getCameraControl().setLinearZoom(0.5f);
                        }
                    }

                    @Override
                    void longClick(float x, float y) {
                        showTapView(x, y);
                    }
                }.onTouchEvent(event);
            }
        });
    }  
    
    // 相机手势事件
    public abstract class CameraXGestureDetector {
    private GestureDetector mGestureDetector;
    /**
     * 缩放相关
     */
    private float currentDistance = 0;
    private float lastDistance = 0;
    public  CameraXGestureDetector(Context context) {
        mGestureDetector = new GestureDetector(context, onGestureListener);
        mGestureDetector.setOnDoubleTapListener(onDoubleTapListener);
    }
    public boolean onTouchEvent(MotionEvent event){
        return  mGestureDetector.onTouchEvent(event);
    }
  

手势事件封装

  GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
        @Override
        public boolean onDown(MotionEvent e) {
            Log.i("","onDown: 按下");
            return true;
        }

        @Override
        public void onShowPress(MotionEvent e) {
            Log.i("","onShowPress: 刚碰上还没松开");
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.i("","onSingleTapUp: 轻轻一碰后马上松开");
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.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) {
                        // 放大
                         zoom();
                    } else if (lastDistance - currentDistance > 10) {
                        // 缩小
                         zoomOut();
                    }
                }
                //在一次缩放操作完成后,将本次的距离赋值给lastDistance,以便下一次判断
                //但这种方法写在move动作中,意味着手指一直没有抬起,监控两手指之间的变化距离超过10
                //就执行缩放操作,不是在两次点击之间的距离变化来判断缩放操作
                //故这种将本次距离留待下一次判断的方法,不能在两次点击之间使用
                lastDistance = currentDistance;
            }
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            Log.i("","onLongPress: 长按屏幕");
            longClick(e.getX(), e.getY());
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.i("","onFling: 滑动后松开");
            currentDistance = 0;
            lastDistance = 0;
            return true;
        }
    };

    GestureDetector.OnDoubleTapListener onDoubleTapListener = new GestureDetector.OnDoubleTapListener() {
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            Log.i("click","onSingleTapConfirmed: 严格的单击");
            click(e.getX(), e.getY());
            return true;
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            Log.i("","onDoubleTap: 双击");
            doubleClick(e.getX(), e.getY());
            return true;
        }

        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            Log.i("","onDoubleTapEvent: 表示发生双击行为");
            return true;
        }
    };

    /**
     * 放大
     */
    abstract void zoom();

    /**
     * 缩小
     */
    abstract void zoomOut();

    /**
     * 点击
     */
    abstract void click(float x, float y);

    /**
     * 双击
     */
    abstract  void doubleClick(float x, float y);

    /**
     * 长按
     */
    abstract void longClick(float x, float y);
}

实际效果

如此我们就得到了一个完整的基础的相机应用,让我们来看看效果吧

image.png WechatIMG667.jpg

完整代码已经上传,请点击下面的项目地址查收

项目地址github.com/runner-up-j…

*文章预告:前端和android之间传递流数据,你让我传个图哇,我怎么能传个图?

主要是梳理一下android下webview和native直接文件传递的方案,并输出一个多文件分片传流的demo*

文章初版已完成,希望大家能够提供意见帮助小弟完善一下,文章地址juejin.cn/post/726003…

同时推荐我同事写的一篇好文:juejin.cn/post/726014…