Android使用Camera2打造自定义相机

从5.0开始(API Level 21),可以完全控制Android设备相机的新api Camera2(android.hardware.Camera2)被引入了进来。在以前的Camera api(android.hardware.Camera)中,对相机的手动控制需要更改系统才能实现,而且api也不友好。不过老的Camera API在5.0上已经过时,在未来的app开发中推荐的是Camera2 API。

1、Camera2介绍

在Camera类中我们多是使用这个类的对象去调用方法,而Camera2则是使用多个类去设置,功能更加强大。

Camera2包架构:

Android使用Camera2打造自定义相机_第1张图片

Google采用了pipeline(管道)的概念,将Camera Device相机设备和Android Device安卓设备连接起来, Android Device通过管道发送CaptureRequest拍照请求给Camera Device,Camera Device通过管道返回CameraMetadata数据给Android Device,这一切建立在一个叫作CameraCaptureSession的会话中。

Android使用Camera2打造自定义相机_第2张图片

基本上我们需要使用的就是这些类啦。其中CameraManager是所有相机设备(CameraDevice)的管理者,要枚举,查询和打开可用的相机设备,就获取CameraManager实例。

单个CameraDevices提供一组静态属性信息,描述硬件设备以及设备的可用设置和输出参数。该信息通过CameraCharacteristics对象提供,可通过getCameraCharacteristics(String)获得。

CameraCharacteristics是CameraDevice的属性描述类,在CameraCharacteristics中可以进行相机设备功能的详细设定(当然了,首先你得确定你的相机设备支持这些功能才行)。

要从相机设备捕获或流式传输图像,应用程序必须首先使用createCaptureSession(List,CameraCaptureSession.StateCallback,Handler)与相机设备一起使用一组输出Surfaces创建摄像机捕获会话。每个Surface必须预先配置适当的大小和格式(如果适用)以匹配相机设备可用的大小和格式。目标Surface可以从各种类获得。

CameraCaptureSession:这是一个非常重要的API,当程序需要预览、拍照时,都需要先通过该类的实例创建Session。而且不管预览还是拍照,也都是由该对象的方法进行控制的,其中控制预览的方法为setRepeatingRequest();控制拍照的方法为capture()。

通常,相机预览图像将发送到SurfaceView或TextureView(通过其SurfaceTexture)。

然后,应用程序需要构建一个CaptureRequest,它定义了相机设备捕获单个映像所需的所有捕获参数。该请求还列出了哪些配置的输出表面应该用作此捕获的目标。 CameraDevice具有用于为给定用例创建请求构建器的工厂方法,针对应用程序正在运行的Android设备进行了优化。

CameraRequest和CameraRequest.Builder:当程序调用setRepeatingRequest()方法进行预览时,或调用capture()方法进行拍照时,都需要传入CameraRequest参数。CameraRequest代表了一次捕获请求,用于描述捕获图片的各种参数设置,比如对焦模式、曝光模式……总之,程序需要对照片所做的各种控制,都通过CameraRequest参数进行设置。CameraRequest.Builder则负责生成CameraRequest对象。

一旦请求被建立,它可以交给主动捕获会话进行单次捕获或无休止地重复使用。处理请求后,相机设备将产生一个TotalCaptureResult对象,该对象包含有关拍摄时相机设备状态的信息以及使用的最终设置。如果需要舍入或解决矛盾的参数,这些请求可能会有所不同。相机设备还会将图像数据帧发送到请求中包括的每个输出表面。这些相对于输出CaptureResult是异步产生的,有时候稍后会产生。

类图中有着三个重要的callback,其中CameraCaptureSession.CaptureCallback将处理预览和拍照图片的工作,需要重点对待。

Android使用Camera2打造自定义相机_第3张图片

Android使用Camera2打造自定义相机_第4张图片

这两幅对Camera2接口使用的流程介绍我们综合起来看会有更深的理解。

  1. 可以看出调用openCamera方法后会回调CameraDevice.StateCallback这个方法,在该方法里重写onOpened函数。
  2. 在onOpened方法中调用createCaptureSession,该方法又回调CameraCaptureSession.StateCallback方法。
  3. 在CameraCaptureSession.StateCallback中重写onConfigured方法,设置setRepeatingRequest方法(也就是开启预览)。
  4. setRepeatingRequest又会回调 CameraCaptureSession.CaptureCallback方法。
  5. 重写CameraCaptureSession.CaptureCallback中的onCaptureCompleted方法,result就是未经过处理的元数据了。

顺便提一下CameraCaptureSession.CaptureCallback中的onCaptureProgressed方法很明显是在Capture过程中的,也就是在onCaptureCompleted之前,所以,在这之前想对图像干什么就看你的了,像美颜等操作就可以在这个方法中实现了。

可以看出Camera2相机使用的逻辑还是比较简单的,其实就是3个Callback函数的回调,先说一下:setRepeatingRequest和capture方法其实都是向相机设备发送获取图像的请求,但是capture就获取那么一次,而setRepeatingRequest就是不停的获取图像数据,所以呢,使用capture就想拍照一样,图像就停在那里了,但是setRepeatingRequest一直在发送和获取,所以需要连拍的时候就调用它,然后在onCaptureCompleted中保存图像就行了。(注意了,图像的预览也是用的setRepeatingRequest,只是你不处理数据就行了)。

通过上面对Camera2的API的分析,我们可以知道控制拍照的大致步骤为:

调用CameraManager的openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler)方法打开指定摄像头。该方法的第一个参数代表要打开的摄像头ID;第二个参数用于监听摄像头的状态;第三个参数代表执行callback的Handler,如果程序希望直接在当前线程中执行callback,则可将handler参数设为null。
当摄像头被打开之后,程序即可获取CameraDevice—即根据摄像头ID获取了指定摄像头设备,然后调用CameraDevice的createCaptureSession(List outputs, CameraCaptureSession. StateCallback callback,Handler handler)方法来创建CameraCaptureSession。该方法的第一个参数是一个List集合,封装了所有需要从该摄像头获取图片的Surface,第二个参数用于监听CameraCaptureSession的创建过程;第三个参数代表执行callback的Handler,如果程序希望直接在当前线程中执行callback,则可将handler参数设为null。
不管预览还是拍照,程序都调用CameraDevice的createCaptureRequest(int templateType)方法创建CaptureRequest.Builder,该方法支持TEMPLATE_PREVIEW(预览)、TEMPLATE_RECORD(拍摄视频)、TEMPLATE_STILL_CAPTURE(拍照)等参数。
通过第3步所调用方法返回的CaptureRequest.Builder设置拍照的各种参数,比如对焦模式、曝光模式等。
调用CaptureRequest.Builder的build()方法即可得到CaptureRequest对象,接下来程序可通过CameraCaptureSession的setRepeatingRequest()方法开始预览,或调用capture()方法拍照。

2、自定义相机

经过上面的说明,相信大家对Camera2的接口已经有了一定的了解,不是很清楚不要紧,实践出真知,我们就开始上代码啦。


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextureView
        android:id="@+id/textureView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true" />

    <FrameLayout
        android:id="@+id/control"
        android:layout_width="match_parent"
        android:layout_height="112dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentStart="true"
        android:background="@color/control_background">

        <Button
            android:id="@+id/picture"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="@string/picture" />

    FrameLayout>

RelativeLayout>

既然只是示例,我们的布局就简单一些就好,就下来我们先为TextureView设置好它的回调:

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    setupCamera();
    openCamera();
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}

我们这个案例主要是为了介绍如何用Camera2实现拍照,所以关于尺寸大小适配的处理就不多做了,所以我们就在onSurfaceTextureSizeChanged()中设置并打开Camera。

private void setupCamera() {
    //获取摄像头的管理者CameraManager
    CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
    try {
        //遍历所有摄像头
        for (String id : manager.getCameraIdList()) {
            CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
            //默认打开后置摄像头
            if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT)
                continue;
            //获取StreamConfigurationMap,它是管理摄像头支持的所有输出格式和尺寸
            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

            // 对于静态图像捕获,我们使用最大的可用尺寸。
            mPreviewSize = Collections.max(
                               Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
            new Comparator() {
                @Override
                public int compare(Size lhs, Size rhs) {
                    return Long.signum(lhs.getWidth() * lhs.getHeight()
                                       - rhs.getHeight() * rhs.getWidth());
                }
            });
            mCameraId = id;
            break;
        }
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

我们这里就启用后置摄像头,setupCamera()我们就是设置图像尺寸并获得摄像头ID,方便我们在openCamera()中使用。

private void openCamera() {
    //获取摄像头的管理者CameraManager
    CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
    //检查权限
    try {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        //打开相机,第一个参数指示打开哪个摄像头,第二个参数stateCallback为相机的状态回调接口,第三个参数用来确定Callback在哪个线程执行,为null的话就在当前线程执行
        manager.openCamera(mCameraId, stateCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

这样我们算是完成了第一步,按照流程图接下来就是启用我们设备的回调开始预览:

private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
    @Override
    public void onOpened(CameraDevice camera) {
        mCameraDevice = camera;
        //开启预览
        startPreview();
    }

    @Override
    public void onDisconnected(CameraDevice camera) {

    }

    @Override
    public void onError(CameraDevice camera, int error) {

    }
};

mCameraDevice是我设置的CameraDevice对象,现在给它初始化,我们知道CameraDevice相当于旧的Camera,所以我们就得到了这个摄像头。

private void startPreview() {
    SurfaceTexture mSurfaceTexture = mPreviewView.getSurfaceTexture();

    //设置TextureView的缓冲区大小
    mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

    //获取Surface显示预览数据
    Surface mSurface = new Surface(mSurfaceTexture);

    setupImageReader();

    //获取ImageReader的Surface
    Surface imageReaderSurface = mImageReader.getSurface();

    try {
        //创建CaptureRequestBuilder,TEMPLATE_PREVIEW比表示预览请求
        mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        //设置Surface作为预览数据的显示界面
        mPreviewBuilder.addTarget(mSurface);
        //创建相机捕获会话,第一个参数是捕获数据的输出Surface列表,第二个参数是CameraCaptureSession的状态回调接口,当它创建好后会回调onConfigured方法,第三个参数用来确定Callback在哪个线程执行,为null的话就在当前线程执行
        mCameraDevice.createCaptureSession(Arrays.asList(mSurface, imageReaderSurface), mSessionStateCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

这个方法就是我们实现预览的关键,我们设置好了Surface就把它与CaptureRequestBuilder对象关联,然后就是设置会话开始捕获画面。

private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(CameraCaptureSession session) {
        try {
            //创建捕获请求
            mCaptureRequest = mPreviewBuilder.build();
            mPreviewSession = session;
            //设置反复捕获数据的请求,这样预览界面就会一直有数据显示
            mPreviewSession.setRepeatingRequest(mCaptureRequest, mSessionCaptureCallback, mHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onConfigureFailed(CameraCaptureSession session) {

    }
};

最后的回调CameraCaptureSession.CaptureCallback就给我们设置预览完成的逻辑处理:

private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {

    @Override
    public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
        super.onCaptureCompleted(session, request, result);
        //重启预览
        restartPreview();
    }
};
private void restartPreview() {
    try {
        //执行setRepeatingRequest方法就行了,注意mCaptureRequest是之前开启预览设置的请求
        mPreviewSession.setRepeatingRequest(mCaptureRequest, null, mHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

这样就创建好了,但是要注意的是因为Camera2没有onPictureTaken()方法,所以我们不能直接获得图像数据,这里我们要用的是ImageReader:

private void setupImageReader() {

    //前三个参数分别是需要的尺寸和格式,最后一个参数代表每次最多获取几帧数据,本例的2代表ImageReader中最多可以获取两帧图像流
    mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
                                           ImageFormat.JPEG, 2);

    //监听ImageReader的事件,当有图像流数据可用时会回调onImageAvailable方法,它的参数就是预览帧数据,可以对这帧数据进行处理
    mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            mHandler.post(new ImageSaver(reader.acquireNextImage()));
        }
    }, mHandler);
}

在处理ImageReader我们可以用handler来做:

public class ImageSaver implements Runnable {

        private Image mImage;
        private File mFile;

        public ImageSaver(Image image) {
            this.mImage = image;
        }

        @Override
        public void run() {
            ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);
            FileOutputStream output = null;

            SimpleDateFormat sdf = new SimpleDateFormat(
                "yyyyMMdd_HHmmss",
                Locale.US);

            String fname = "IMG_" +
                           sdf.format(new Date())
                           + ".jpg";
            mFile = new File(getApplication().getExternalFilesDir(null), fname);

            try {
                output = new FileOutputStream(mFile);
                output.write(bytes);
            } catch (IOException e) {
                e.printStackTrace();
            }
            finally {
                mImage.close();
                if (null != output) {
                    try {
                        output.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
}

这个Run()方法里做的就是把从Image中获得的帧数据输出到指定的文件里,文件名我们用当前时间来生成。

这样我们就做好所有的拍照前的设置了,现在只要处理点击按钮时进行拍照即可。

private HandlerThread mThreadHandler;
private TextureView mPreviewView;
private Handler mHandler = new Handler();
private CaptureRequest.Builder mPreviewBuilder;
private Button mButton;
private ImageReader mImageReader;
private String mCameraId;
private Size mPreviewSize;
private CameraDevice mCameraDevice;
private CaptureRequest mCaptureRequest;
private CameraCaptureSession mPreviewSession;

private static final SparseIntArray ORIENTATION = new SparseIntArray();

static {
    ORIENTATION.append(Surface.ROTATION_0, 90);
    ORIENTATION.append(Surface.ROTATION_90, 0);
    ORIENTATION.append(Surface.ROTATION_180, 270);
    ORIENTATION.append(Surface.ROTATION_270, 180);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_test);
    requestCameraPermission();

    mThreadHandler = new HandlerThread("CAMERA2");
    mThreadHandler.start();
    mHandler = new Handler(mThreadHandler.getLooper());
    mPreviewView = (TextureView) findViewById(textureView);
    mPreviewView.setSurfaceTextureListener(this);
    mButton = (Button) findViewById(R.id.picture);
    mButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {

            try {
                //获取屏幕方向
                int rotation = getWindowManager().getDefaultDisplay().getRotation();
                //设置CaptureRequest输出到mImageReader
                //CaptureRequest添加imageReaderSurface,不加的话就会导致ImageReader的onImageAvailable()方法不会回调
                mPreviewBuilder.addTarget(mImageReader.getSurface());
                //设置拍照方向
                mPreviewBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(rotation));
                //聚焦
                mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                                    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

                //停止预览
                mPreviewSession.stopRepeating();
                //开始拍照,然后回调上面的接口重启预览,因为mPreviewBuilder设置ImageReader作为target,所以会自动回调ImageReader的onImageAvailable()方法保存图片
                mPreviewSession.capture(mPreviewBuilder.build(), mSessionCaptureCallback, null);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }
    });
}

这里要注意的是给ImageReader的surface的设置必须放在拍照这里,否则再预览的时候就会不断的执行handler,将图像保存下来。

Android使用Camera2打造自定义相机_第5张图片

Camera2还有很多的功能,谷歌在给我们提供强大类的时候也让我们的学习量增大了,所以大家不要认为基本了解Camera2的工作流程就是掌握了Camera2,只有能将其运用到我们的开发中去才算掌握了,所以如果你是看到这篇博客才了解了Camera2,那这只是你的第一步而已,让我们彼此共勉,最后附上GitHub的开源项目。

结束语:本文仅用来学习记录,参考查阅。

你可能感兴趣的:(Android进阶学习,android)