相机小白自定义Camera实践

背景

机缘巧合,需要自定义相机,几日折腾下来,对相机开发有了一定认识,做个小结。

既然是自定义相机,在设想里,相机和UI是结构是这样的:


相机和UI.png

关键信息有:

  • 由CameraView去接收相机设备所给予的反馈
  • 同时CameraView也是遥控器,外部能通过CameraView操作相机设备

这样做的好处是:

  1. 相机控制,预览,操作等应该是视图无关的。放在任意的页面,做简单的配置便可以使用。
  2. CameraView对外屏蔽了种种使用相机可能造成的隐患,仅需要暴露必要的接口达到安全操控相机的操作即可。
  3. 对于具体UI来说,相机回执到的数据如何接收如何展示,让CameraView自己处理就好了,不关心,我给它一个显示位置就可以了。

通过类似的处理,能让自定义相机成为可复用、易维护、低成本的功能,而不是冗杂的一次性消费。

方案寻找

确定预想之后,就要寻求具体的开发实现了。几经寻找,看到的博文等零碎非常严重,让我痛苦不堪。好在最终找到了Google给出来的开源方案。
Google相机开源方案

对于着急的同学,可以直接拿去用了,给CameraView添加CameraView.Callback基本能达到目的。

    public abstract static class Callback {
        /**
         * 相机打开
         */
        public void onCameraOpened(CameraView cameraView) {
        }

        /**
         * 相机关闭
         */
        public void onCameraClosed(CameraView cameraView) {
        }

        /**
         * 相机拿到静态图片数据
         */
        public void onPictureTaken(CameraView cameraView, byte[] data) {
        }
    }

记得别忘了添加权限以及在合适的生命周期释放相应资源即可。

但是作为头铁大分队小队长,想去看看Google的方案是如何实现的,填了什么坑。万一以后有什么骚操作,也好有个照应。余下的内容均是对于此方案的实现分析,以达到目的的同学可以先撤了。

相机开发须知

分裂的API
Android 5.0以下的API为Camera 而 Android 5.0以上的API为Camera2,并且各大手机厂商对于Camera2的支持程度也不同。对于不支持Camera2的设备来说,需要降级使用Camera.

SurfaceView/TextureView
界面渲染主要涉及到SurfaceView 和 TextureView , 在4.0以上才能使用TextureView 。两者的简要区别如图:

TextureView 和 SurfaceView 区别.png

SurfaceView不受View Hierarchy约束,拥有自己的Surface,可以理解为是另一个Window。因此一些View特性无法使用,但也因此不会影响主线程,可以放到其它的线程进行渲染,性能友好。

TextureView 则与普通的View类似,受View Hierarchy约束,相机发送过来的数据经由SurfaceTexture交接,让TextureView能以硬件加速渲染的方式存在视图树,也因此更耗费性能。

易出问题场景
除了API的分离,设备的支持程度外,面对生命周期的变化,也需谨慎处理。常见有后台场景,锁屏场景。相机是共享资源,其它程序访问使用时容易发生冲突,因此需要正确地释放。

Google CameraView

面对以上问题,CameraView 提出的方案如图


Google CameraView方案.png

图片来源
运转由CameraView来完成。至于具体使用的是哪种视图渲染,由PreviewImpl来决定。与此相似,具体操作哪种相机的API也由CameraViewImpl进行对接。PreviewImpl和CameraViewImpl则提供对外一致的操作接口。对于外部来说,CameraView是外观,使用CameraView开放的操作,即可完成操作,内部变化一无所知。

先看CameraViewImpl

abstract class CameraViewImpl {
    // 相机基础事件回调
    protected final Callback mCallback;
    // 渲染视图    
    protected final PreviewImpl mPreview;
    
    CameraViewImpl(Callback callback, PreviewImpl preview) {
        mCallback = callback;
        mPreview = preview;
    }
    // 获取渲染视图
    View getView() {
        return mPreview.getView();
    }

    // 启动相机
    abstract boolean start();
    // 暂停相机
    abstract void stop();
    // 相机使用状态
    abstract boolean isCameraOpened();
    // 设置使用哪一个相机,简单如前置相机、后置相机
    abstract void setFacing(int facing);
    // 获取当前相机标识    
    abstract int getFacing();
    // 获取相机支持的预览比例
    abstract Set<AspectRatio> getSupportedAspectRatios();
    // 设置拍摄照片比例
    abstract boolean setAspectRatio(AspectRatio ratio);
    // 获取相机当前摄照片比例
    abstract AspectRatio getAspectRatio();
    // 设置自动聚焦    
    abstract void setAutoFocus(boolean autoFocus);
    // 获取自动聚焦    
    abstract boolean getAutoFocus();
    // 设置闪光状态
    abstract void setFlash(int flash);
    // 获取闪光状态    
    abstract int getFlash();
    // 获取静态图片,即拍照
    abstract void takePicture();
    // 设置相机方向
    abstract void setDisplayOrientation(int displayOrientation);
    // 相机基础回调接口
    interface Callback {
        // 相机已打开
        void onCameraOpened();
        // 相机已关闭
        void onCameraClosed();
        // 相机获取到静态图片
        void onPictureTaken(byte[] data);
    }

}

CameraViewImpl陈列了共性的可能的相机操作,挑一些做说明:
setFacing()
设置使用具体相机,简单的如前置相机、后置相机。在相机更变后,原本的预览视图可能因为Rotate而呈现出了不一样的视图,因此不仅需要更具需求切换到正确的相机,还需将预览视图进行矫正。

getSupportedAspectRatios()
不同的设备支持的预览视图是不同的,常见的预览视图如4:3、16:9等,因此可以支持的预览视图并不是完全相同的,试具体情况而定。而AspectRatioc存有的就是比例信息,并支持了一些比较、匹配操作。

setAutoFocu()
一般来说,相机设备自动聚焦是默认开启的。当然如更多的聚焦模式如固定聚焦、景深、远景、微焦等也是可以另外支持的。当然,对于我等没有摄友的人来说,玩转不来。

setFlash()
闪光灯状态一般有如自动、关闭、拍照、防红眼等。

接着是PreviewImpl

abstract class PreviewImpl {

    interface Callback {
        // surface发生了变动
        void onSurfaceChanged();
    }

    private Callback mCallback;
    // 预览视图高度
    private int mWidth;
    // 预览视图宽度    
    private int mHeight;

    void setCallback(Callback callback) {
        mCallback = callback;
    }

    abstract Surface getSurface();
    // 获取实际的渲染View
    abstract View getView();
    // 输出源
    abstract Class getOutputClass();
    // 预览方向
    abstract void setDisplayOrientation(int displayOrientation);
    // 渲染视图是否达到可用状态    
    abstract boolean isReady();
    //  分发surface 的更变
    protected void dispatchSurfaceChanged() {
        mCallback.onSurfaceChanged();
    }
    // 主要是为了由SurfaceView渲染的情况
    SurfaceHolder getSurfaceHolder() {
        return null;
    }
    // 主要是为了由TextureView渲染的情况
    Object getSurfaceTexture() {
        return null;
    }
    // 设置缓冲区大小
    void setBufferSize(int width, int height) {
    }
    
    void setSize(int width, int height) {
        mWidth = width;
        mHeight = height;
    }

    int getWidth() {
        return mWidth;
    }

    int getHeight() {
        return mHeight;
    }

}

PreviewImpl的主要作用,是提供了必要信息使能接收CameraImpl给予的信息,并进行渲染。

准备好了PreviewImpl和CameraImpl之后,CameraView就可以工作了

public class CameraView extends FrameLayout{
        public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (isInEditMode()){
            mCallbacks = null;
            mDisplayOrientationDetector = null;
            return;
        }
        // 获取正确的渲染视图
        final PreviewImpl preview = createPreviewImpl(context);
        // 是CamereViewImpl.Callback实现类,接收相机基础事件
        mCallbacks = new CallbackBridge();
        // 根据SDK使用正确的相机API
        if (Build.VERSION.SDK_INT < 21) {
            mImpl = new Camera1(mCallbacks, preview);
        } else if (Build.VERSION.SDK_INT < 23) {
            mImpl = new Camera2(mCallbacks, preview, context);
        } else {
            mImpl = new Camera2Api23(mCallbacks, preview, context);
        }
        // 初始化配置信息,主要是相机参数,包括聚焦、闪光、预览比例、屏幕方向等
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView, defStyleAttr,
                R.style.Widget_CameraView);
        // 自动自动调整预览视图,以和拍摄照片比例一致
        mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false);
        // 更新相机,默认后置
        setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
        // 更新拍摄照片比例,默认为4:3
        String aspectRatio = a.getString(R.styleable.CameraView_aspectRatio);
        if (aspectRatio != null) {
            setAspectRatio(AspectRatio.parse(aspectRatio));
        } else {
            setAspectRatio(Constants.DEFAULT_ASPECT_RATIO);
        }
        // 更新聚焦模式
        setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
        // 更新闪光灯
        setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
        a.recycle();
        // 屏幕方向探测
        mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
            @Override
            public void onDisplayOrientationChanged(int displayOrientation) {
                mImpl.setDisplayOrientation(displayOrientation);
            }
        };
    }
    ...
}

在CameraView的初始化过程,根据实际情况获取相应的PreviewImpl和CameraImpl,具体对相机的执行实现和渲染实现,CameraView,并不关心。createPreviewImpl()获取PreviewImpl的方式也是根据版本信息拿到,不做展开。

有几个细节:

mAdjustViewBounds

此参数是用来调整相机的预览视图达到与拍摄图片一样的尺寸比例。预览视图即你拍摄预览时看到的视图,拍摄出来的图片的尺寸比例和预览视图很可能是不一样的。比如,前者可能是16:9,后者可能是4:3。使用时需要注意,特别是当要对拍摄出的静态图片做二次操作的时候,尺寸信息可能不如我们所想。但当两者比例一致的时候,无此顾虑。

mDisplayOrientationDetector

CameraView内部类,作用是为了探测屏幕方向,横屏竖屏,方便做出调整以免预览视图出现不合适的角度。

拍摄图片比例4:3

这是一个预设值,CameraView并不知道外部的使用情况,且相机能支持多种图片比例,因此需要根据实际需求做调整。

CameraView需要在onMeasure()根据配置情况做测量

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (isInEditMode()){
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }
        // 需要自动预览视图和照片比例一致
        if (mAdjustViewBounds) {
            if (!isCameraOpened()) {
                mCallbacks.reserveRequestLayoutOnOpen();
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                return;
            }
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
                final AspectRatio ratio = getAspectRatio();
                assert ratio != null;
                int height = (int) (MeasureSpec.getSize(widthMeasureSpec) * ratio.toFloat());
                if (heightMode == MeasureSpec.AT_MOST) {
                    height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
                }
                super.onMeasure(widthMeasureSpec,
                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
            } else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
                final AspectRatio ratio = getAspectRatio();
                assert ratio != null;
                int width = (int) (MeasureSpec.getSize(heightMeasureSpec) * ratio.toFloat());
                if (widthMode == MeasureSpec.AT_MOST) {
                    width = Math.min(width, MeasureSpec.getSize(widthMeasureSpec));
                }
                super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                        heightMeasureSpec);
            } else {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            }
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
        
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        AspectRatio ratio = getAspectRatio();
        // 根据屏幕方向调整预览视图
        if (mDisplayOrientationDetector.getLastKnownDisplayOrientation() % 180 == 0) {
            ratio = ratio.inverse();
        }
        assert ratio != null;
        if (height < width * ratio.getY() / ratio.getX()) {
            mImpl.getView().measure(
                    MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(width * ratio.getY() / ratio.getX(),
                            MeasureSpec.EXACTLY));
        } else {
            mImpl.getView().measure(
                    MeasureSpec.makeMeasureSpec(height * ratio.getX() / ratio.getY(),
                            MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        }
    }

代码虽长,但主要做了两件事情:

  • 根据自动调整状态和测量模式对尺寸进行测量
  • 对屏幕方向进行响应

之前说过,CameraView对外部暴露了一定的接口间接地使用相机设备,具体的实现情况基本是转发到CameraImpl和PreViewImpl,不贴出。此外,CameraView是View,因此需要处理恢复视图的情况。

    @Override
    protected Parcelable onSaveInstanceState() {
        SavedState state = new SavedState(super.onSaveInstanceState());
        state.facing = getFacing();
        state.ratio = getAspectRatio();
        state.autoFocus = getAutoFocus();
        state.flash = getFlash();
        return state;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        setFacing(ss.facing);
        setAspectRatio(ss.ratio);
        setAutoFocus(ss.autoFocus);
        setFlash(ss.flash);
    }

SavedState存储里相机配置和图片配置信息,不做纠缠。

CameraView的工作大致如此:

  • 获取合适的CameraImpl和PreViewImpl,间接完成操作相机以及渲染视图工作
  • 对外部操作做出响应,也对相机的使用配置情况做出响应
  • 协调CameraImpl和PreViewImpl

虽然知道了CameraView的处理过程,但是具体的实现过程还很模糊,现在,通过对Camera2的使用,抛砖引玉,一探究竟。

Camera2

从CameraView里看到,版本>=21情况下,CameraImpl实际为Camera2,Camera2自然实现了相应的接口。对于使用相机来说,最感兴趣的无非开启、关闭、拍摄以及更变配置等操作。在看具体过程之前,先了解一些知识点,有个概念即可。

  • CameraManager: 摄像头管理器,用于打开和关闭系统摄像头
  • CameraCharacteristics:摄像头的各种特性描述
  • CameraDevice: 系统摄像头
  • CameraCaptureSeesion: 在操作摄像头时,需要透过此类会话
  • CaptureRequest: 操作请求
  • CaptureResult: 操作结果

具体操作流程如图

Camera2拍照流程-1.png

图片来源
在与摄像头建立会话后,操作请求会发送到CameraDevice,CameraDevice处理完后将操作结果发送出去,将Image Buffer发送到Surface。

开启相机

一切,从启动相机开始

    @Override
    boolean start() {
        // 获取摄像头
        if (!chooseCameraIdByFacing()) {
            return false;
        }
        // 获取相机信息    
        collectCameraInfo();
        // 配置接收源信息,即想拿到什么样的静态图片
        prepareImageReader();
        // 启动相机
        startOpeningCamera();
        return true;
    }

在能获取到摄像头后,才能有之后的操作

    private boolean chooseCameraIdByFacing() {
        try {
            // 获取当前的所需要的摄像头, 默认为后置
            int internalFacing = INTERNAL_FACINGS.get(mFacing);
            // 获取所有的摄像头id
            final String[] ids = mCameraManager.getCameraIdList();
            // 没有摄像头的情况
            if (ids.length == 0) { 
                throw new RuntimeException("No camera available.");
            }
            // 遍历查找摄像头
            for (String id : ids) {
                CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
                // 获取硬件等级,即相机的兼容性
                Integer level = characteristics.get(
                        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
                // 获取不到硬件等级或向后兼容模式,不符合        
                if (level == null ||
                        level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                    continue;
                }
                // 获取该摄像头处于的屏幕方向
                Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (internal == null) {
                    throw new NullPointerException("Unexpected state: LENS_FACING null");
                }
                // 与所需要的相机方向一致,为合适的摄像头
                if (internal == internalFacing) {
                    mCameraId = id;
                    mCameraCharacteristics = characteristics;
                    return true;
                }
            }
            // 没有找到一致的摄像头,取第一个,之后的过程与上面类似
            mCameraId = ids[0];
            mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
            Integer level = mCameraCharacteristics.get(
                    CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
            if (level == null ||
                    level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                return false;
            }
            Integer internal = mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
            if (internal == null) {
                throw new NullPointerException("Unexpected state: LENS_FACING null");
            }
            for (int i = 0, count = INTERNAL_FACINGS.size(); i < count; i++) {
                if (INTERNAL_FACINGS.valueAt(i) == internal) {
                    mFacing = INTERNAL_FACINGS.keyAt(i);
                    return true;
                }
            }
            
            // 到这里说明用了外设,当作后置摄像头处理
            mFacing = Constants.FACING_BACK;
            return true;
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to get a list of camera devices", e);
        }
}

选取摄像头的过程,正常情况下出现三种情况:

  1. 选取到指定的摄像头
  2. 选取不到指定的摄像头,默认使用系统下发的第一个摄像头
  3. 选取到外设摄像头

选取到合适的摄像头之后,需要收集摄像头的信息

    private void collectCameraInfo() {
        // 获取设备支持的stream配置
        StreamConfigurationMap map = mCameraCharacteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (map == null) {
            throw new IllegalStateException("Failed to get configuration map: " + mCameraId);
        }
        mPreviewSizes.clear();
        // 获取输出配置   
        for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) {
            int width = size.getWidth();
            int height = size.getHeight();
            if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
                // 收集输出配置,即支持的预览视图比例
                mPreviewSizes.add(new Size(width, height));
            }
        }
        mPictureSizes.clear();
        // 收集拍照图片比例, 默认区格式为JPEG的拍照比例
        collectPictureSizes(mPictureSizes, map);
        // 移除不合适的预览视图比例
        for (AspectRatio ratio : mPreviewSizes.ratios()) {
            if (!mPictureSizes.ratios().contains(ratio)) {
                mPreviewSizes.remove(ratio);
            }
        }
        
        // 如果当前预览视图比例不被支持的话,取第一个
        if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
            mAspectRatio = mPreviewSizes.ratios().iterator().next();
        }
    }

经过collectCameraInfo() 之后,拥有了预览视图比例集合和拍照视图比例集合,在进行拍照后,拿到的静态图片比例,与当前支持的预览视图比例一致。

而之后的prepareImageReader()和startOpeningCamera()就不贴了,很清楚,说一下要点。
ImageReader会接受到相机设备给出图像信息,在ImageReader.OnImageAvailableListener能拿到byte[], 再将此数据发送给CameraViewImpl.Callback。

开启相机则通过 mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, null)进行,其中mCameraId是之前选取摄像头拿到的摄像头id,mCameraDeviceCallback是用来接受CameraDevice的执行状态 。

  public static abstract class CameraDevice.StateCallback{
        // 相机设备打开
        public abstract onOpened(@NonNull CameraDevice camera);
        // 相机设备关闭
        public void onClosed(@NonNull CameraDevice camera);
        // 相机设备脱离连接
        public abstract onDisconnected(@NonNull CameraDevice camera);
        // 错误
        public abstract onError(@NonNull CameraDevice camera, int error);
     ...
    };

当前的操作是打开相机,因此,收到相机设备打开回调

private final CameraDevice.StateCallback mCameraDeviceCallback
            = new CameraDevice.StateCallback() {

        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            // 记录相机设备
            mCamera = camera;
            // 回调CamereView.Impl    
            mCallback.onCameraOpened();
            // 开启相机会话
            startCaptureSession();
        }
        ....
    };

见开启相机会话

    void startCaptureSession() {
        // 有CameraDevice,Preview达到可用,Imagereader可用
        if (!isCameraOpened() || !mPreview.isReady() || mImageReader == null) {
            return;
        }
        // 获取合适的预览视图尺寸
        Size previewSize = chooseOptimalSize();
    // 设置image buffer 尺寸
        mPreview.setBufferSize(previewSize.getWidth(), previewSize.getHeight());
        Surface surface = mPreview.getSurface();
        try {
            // 构造相机预览请求
            mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
         // 提供surface , 此数据用于渲染
            mPreviewRequestBuilder.addTarget(surface);
         // 提供surface, 此数据用于其它作用
            mCamera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                    mSessionCallback, null);
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to start camera session");
        }
    }

首先,需要获取合适的image buffer尺寸,一般为预览视图尺寸,因为源Image buffer很大,因此需要提供合适的尺寸已达到满足预览需求即可。

其次,这里提供了两个surface,为何如此?参考之前给的拍照流程图就明白了,其中的一个surface所接受到的相机发来的图像数据,最终是要传到PreviewImpl的具体渲染视图进行渲染的;而另一个surface将拍摄请求结果的图像数据传递给Imagereader,可做他用,比如生成静态图片。看起来,像下图


渲染surface 和 拍摄surface.png

现在相机已经正常工作了,现在进行正常拍照。

    @Override
    void takePicture() {
        if (mAutoFocus) {
            lockFocus();
        } else {
            captureStillPicture();
        }
    }

根据是否自动聚焦,执行到不同动作

    // 自动聚焦执行
    private void lockFocus() {
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CaptureRequest.CONTROL_AF_TRIGGER_START);
        try {
            mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING);
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Failed to lock focus.", e);
        }
    }
    // 非自动聚执行
    void captureStillPicture() {
        try {
            CaptureRequest.Builder captureRequestBuilder = mCamera.createCaptureRequest(
                    CameraDevice.TEMPLATE_STILL_CAPTURE);
            captureRequestBuilder.addTarget(mImageReader.getSurface());
            captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE));
            switch (mFlash) {
                case Constants.FLASH_OFF:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON);
                    captureRequestBuilder.set(CaptureRequest.FLASH_MODE,
                            CaptureRequest.FLASH_MODE_OFF);
                    break;
                case Constants.FLASH_ON:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
                    break;
                case Constants.FLASH_TORCH:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON);
                    captureRequestBuilder.set(CaptureRequest.FLASH_MODE,
                            CaptureRequest.FLASH_MODE_TORCH);
                    break;
                case Constants.FLASH_AUTO:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                    break;
                case Constants.FLASH_RED_EYE:
                    captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
                            CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
                    break;
            }
            /**
            * 根据相机传感器参数,计算出得出的图片的方向,并结合相机位置进行调整
            **/ 
            @SuppressWarnings("ConstantConditions")
            int sensorOrientation = mCameraCharacteristics.get(
                    CameraCharacteristics.SENSOR_ORIENTATION);
            captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,
                    (sensorOrientation +
                            mDisplayOrientation * (mFacing == Constants.FACING_FRONT ? 1 : -1) +
                            360) % 360);
            // 取消之前的拍摄操作
            mCaptureSession.stopRepeating();
            // 发送拍摄请求
            mCaptureSession.capture(captureRequestBuilder.build(),
                    new CameraCaptureSession.CaptureCallback() {
                        @Override
                        public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                @NonNull CaptureRequest request,
                                @NonNull TotalCaptureResult result) {
                            // 处理完结果后, 重启开启相机        
                            unlockFocus();
                        }
                    }, null);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Cannot capture a still picture.", e);
        }
    }

不管是在自动聚焦还是不是自动聚焦,都需根据所要求的相机配置给captureRequestBuilder即CaptureRequest.Builder设置相应的标志,而自动聚焦使用了打多少默认标志,因此不再配置。而当不是自动聚焦的情况时,需要配置闪光灯的标志,且需要在拍摄请求完成后解锁自动聚焦,并重启相机,释放。在准备好各项参数后,通过相机会话mCaptureSession发送拍照请求。

如果相机通过unlockFocus()重启,则mPreviewRequestBuilder会根据相应的配置信息,重制到合适的状态,各项参数通过mPreviewRequestBuilder.set()以key value 形式设置,详情见源码 updateAutoFocus() 和 updateFlash() 不列出,仅需要知道,在二者里设置了拍照回调监听。

// 使拍照请求能不断执行
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
                            mCaptureCallback, null);

所有的拍摄回调会由mCaptureCallback监听,并控制状态。

private static abstract class PictureCaptureCallback
            extends CameraCaptureSession.CaptureCallback {
            
        static final int STATE_PREVIEW = 0;  
        static final int STATE_LOCKING = 1;  
        static final int STATE_LOCKED = 2;    
        static final int STATE_PRECAPTURE = 3;
        static final int STATE_WAITING = 4;
        static final int STATE_CAPTURING = 5;

        private int mState;

        PictureCaptureCallback() {
        }

        void setState(int state) {
            mState = state;
        }

        @Override
        public void onCaptureProgressed(@NonNull CameraCaptureSession session,
                @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
            process(partialResult);
        }

        @Override
        public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
            process(result);
        }

        ......
}

拍照会话请求过程会回调到PictureCaptureCallback,PictureCaptureCallback所做的,保证整个拍照过程状态正确切换,主要处理自动聚焦情况,感兴趣的可以自理PictureCaptureCallBack.process()。这里仅放出状态切换图


拍摄状态控制.png

哪如何拿到拍摄的静态图片数据呢?
根据之前的分析,拍照会话请求时,ImageReader提供了Surface以接收静态图片数据,而这项数据最终会由ImageReader.OnImageAvailableLisetner接收到


    private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
            = new ImageReader.OnImageAvailableListener() {

        @Override
        public void onImageAvailable(ImageReader reader) {
            try (Image image = reader.acquireNextImage()) {
                Image.Plane[] planes = image.getPlanes();
                if (planes.length > 0) {
                    ByteBuffer buffer = planes[0].getBuffer();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    mCallback.onPictureTaken(data);
                }
            }
        }
    };

如果确实拿到能用的图片数据的话,通过 mCallback.onPictureTaken(data) 将数据发送给外部,当前情况,则是CameraView.CallbackBridge,而CallBackBridge会继续分发个CameraView.Callback。 因此,具体UI添加CameraView.Callback监听,即可拿到静态图片数据。

关闭操作就不解析了。

总结

  • 根据具体设备的摄像头配置,收集摄像头的主要特性信息
  • 通过CaptureRequest.Builder构建相机会话请求,并保存相应相机操作配置
  • 需要考虑相机位置和拍摄图片的关系,预览视图和拍摄视图的关系
  • 通过ImageReader拿到拍摄图片数据

对于Camera2的实现分析大致如此,Camera就自行阅读了。当然,以上所说均是皮毛而已,有很多实现还云里雾里,要明白了解透彻,还有很长的路要走,励之勉之。


参考
Android Camera2 简介
Android Camera2 使用总结
Android平台Camera开发实践指南
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 前言 年底公司赶项目,忙得不亦乐乎,博客也很久没更新了。公司项目里用到了自定义摄像头的模块,也参考了Google开...
    管弦_阅读 14,070评论 2 32
  • 标签(空格分隔):Android Camera 相机 图像方向 图像大小 【注】本文所提到的 Camera 均为 ...
    koguma阅读 17,441评论 5 47
  • 上一篇介绍了如何使用系统相机简单、快速的进行拍照,本篇将介绍如何使用框架提供的API直接控制摄像机硬件。 你还在为...
    Xiao_Mai阅读 7,142评论 4 18
  • 导语:我觉得最好的东西,都是纯粹的,不掺杂其他东西。创造更多更好、简单的产品是需要信心和勇气,去抵制想要增加更多东...
    空城_00bf阅读 423评论 0 0
  • 这个世界上如果存在好人,就一定会存在坏人。这个时代的坏人或许和过去的坏人不太一样,过去的坏人杀人放火,强抢民女。现...
    我是范老师阅读 171评论 0 0