Android Camera2 API预览 拍照和录像
code链接
git clone https://github.com/jzlhll/AndroidCam2Demo.git
原因
最近学习Camera2,发现Camera2的架构体系变化非常大。想要直接看懂google的android-Camera2Basic 和 android-Camera2Video,在架构体系上,做的不是很好,session,request写的比较绕。
于是,我使用了设计模式,让代码看上去更加有条理性。我想应该是入门最好的帖子了。
- 优点1:一个APP,按照不同的场景,设计了3种状态,预览,拍照和录像;
- 优点2:架构清晰,简单,很容易掌握到camera2的设计理念;
- API:Camera2,MediaRecorder,TextureView/SurfaceView,ImageReader。
代码架构
View & Activity
使用了Material design风格,Layout中,传入一个自定义的CamSurfaceView或者CamTextureView:
<com.allan.androidcam2api.view.CamTextureView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="512dp"
tools:ignore="MissingConstraints"
tools:layout_editor_absoluteX="9dp"
tools:layout_editor_absoluteY="0dp" />
在CamSurfaceView中初始化的时候,设置监听回调,getHolder().addCallback(this),在回调函数:
public interface MyCallback { //在Activity中注册CamView的监听
void pleaseStart();
void pleaseStop();
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
if (mCallback != null) mCallback.pleaseStart();
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
if (mCallback != null) mCallback.pleaseStop();
return false;
}
类似的,CamTextureView setSurfaceTextureListener(this)并调用MyCallback来达到兼容的目的。
Activity设置callback略。
@Override
public void pleaseStart() {
MyCameraManager.me.get().init(mView.getContext(), mView, mViewDecorator);
MyCameraManager.me.get().addModChanged(this);
MyCameraManager.me.get().openCamera();
}
@Override
public void pleaseStop() {
MyCameraManager.me.get().closeCamera();
}
开始进行单例类MyCameraManager的初始化过程。
MyCameraManager & StateBase & Camera2
之所以Camera2不受大家欢迎的主要原因是,它的所有操作基本都是传入回调方法,等待回调。让代码绕来绕去。
经过我的设计以后,希望能减少大家的困惑。
首先我们来了解大致的流程:
1. 使用MyCameraManager保存cameraDevice对象;也就是getSystemService()拿到api cameraManager后,open传入CameraDevice.StateCallback回调回来以后,保存下来;
2. camera2比较绕的地方,是需要创建一个requestBuilder,给它添加addTarget surface(送显或者录制使用),可以多个;然后拿到request去创建session;
3. session创建成功的回调onConfigured()就可以保存下来,setRepeatingRequest进去,代表着某一类的显示已经成功。
那么,基于这种架构,我设计了一个抽象类+Handler消息处理机制来流转状态和控制显示模式。
首先,如上述流程图中,StateBase类所支持的能力:
1. 抽象了需要创建的surface个数:protected abstract void createSurfaces();
2. 抽象了需要贴入的surface:protected abstract void addTarget();
3. 抽象了显示模式:
protected int getTemplateType() {return CameraDevice.TEMPLATE_PREVIEW;}
4. 封装了createCaptureSession的动作,因为这一步不论是拍照模式录像模式等等都是一致的只是surface,和显示模式不一样;
5. 给createcaptureSession的回调构建抽象方法,子类创建这个回调用于处理具体业务 protected abstract CameraCaptureSession.StateCallback createStateCallback();
State+Handler模式
这种设计模式,其实是android framework的StateMachine机制。大概我的意图是这个方向,可能具体有些差异,不过能达到的目的是一致的控制状态的流转,防止太多同时的操作导致状态的异常。
理解了google的demo以后,我知道了State有N种(实际中,2种即可):
1. 纯Preview的State;
2. Preview+Picture的State,注意它并不是拍照瞬间而是一直存在的,takePhoto可以看做一个额外的能力function;
3. Preview+Picture+Record的State, 注意它在进入录制状态就切换到这个State,而停止录制必须回调State2;
4. Preview+Record的State,略。
这个的区分,以Surface的个数和是否需要重新CreateSession为标准
。
回归到代码,第一步,openCamera() -> mCamHandler.sendEmptyMessage(OPEN);
打开以后得到回调后的mCameraDevice;紧接着,transmitModPicturePreview() ->则切换到preview+picture模式
;
case MOD_PREVIEW_PIC: {
if (mCurrentSt == null) {
openCameraFirst(msg); //达到了状态机,自动进入的目的
return;
}
if (mCurrentSt.getId() == 0x011) {
MyToast.toastNew(getContext(), mCamView, "Already in this mod");
return;
}
notifyModChange("Picture&Preview");
mCurrentSt.closeSession(); //关闭session
mCurrentSt = new StatePicAndPreview(MyCameraManager.this); //创建某种State
try {
mCurrentSt.createSession(new StatePicAndPreview.StateTakePicCb() {
@Override
public void onToken(String path) {
CamLog.d("onToken in path" + path);
}
@Override
public void onPreviewSuc() {
CamLog.d("onPreviewSuc in myacmera");
}
@Override
public void onPreviewErr() {
CamLog.d("onPreviewErr in myacmera");
}
});
} catch (Exception e) {
CamLog.e("start preview err0");
e.printStackTrace();
}
}
这仅仅是创建了一种显示模式,而,拍照只是session下的一个动作。
case TAKE_PIC: {
if ((mCurrentSt.getId() & StateBase.PIC) == StateBase.PIC) {
if (mCurrentSt instanceof StatePicAndPreview) {
StatePicAndPreview spp = (StatePicAndPreview) mCurrentSt;
TakePhotoFunc.ObjStruct objStruct = (TakePhotoFunc.ObjStruct) msg.obj;
spp.takePicture(objStruct.dir, objStruct.name, objStruct.func);
} else if (mCurrentSt instanceof StatePicAndRecAndPreview) { //TODO record pciture preview
StatePicAndRecAndPreview sppr = (StatePicAndRecAndPreview) mCurrentSt;
TakePhotoFunc.ObjStruct objStruct = (TakePhotoFunc.ObjStruct) msg.obj;
sppr.takePicture(objStruct.dir, objStruct.name, objStruct.func);
}
} else { //如果没有Picture属性则报错
MyToast.toastNew(getContext(), mCamView, "No Picture Mod");
return;
}
}
只要当前State包含拍照的能力,即可调用它内部的方法。
阅读代码,关注几个transmit的Handler处理流程,并注意Start-Rec其实就是一种特殊的切换State模式即可理解我设计的状态机
的流转了。下面一副low low的图,主要展示出几个状态的切换。他们内部具有的能力,在继承关系上可以体现出来。
下书就是类图,灰色,我没有实现。比如你的需求是录制中不需要拍照,则可以实现这个类StatePreviewRecord类。
录像
录像的过程,跟拍照在传统的Camera1的思想是不一致的。Camera1的时候我们使用MediaRecord的时候,没有设置过surface或者设置过surface,其实是framework进行过转换。这里我们可以显式地看出,addTarget的时候,将preview和record的surface都贴入了request。
其他
utils包里面,有几个重要的东西,android6.0以上Perssion申请的代码,这里引用了网上的东西,再此致谢;
Singleton设计,在我的其他文章中有讲;
里面穿插了很多的func接口,这是我写代码喜欢的一种方式,类似IOS上的block设计。在调用方法处比如takePicture(callback), 那么,可以很轻松的在callback中写处理逻辑。代码架构很清晰。
遗憾
有些旋转rotation,闪光灯没有详细写完;不过都留了TODO;
其实搭这个架构写代码没花太多时间,而在研究动态适配Camera Surface显示分辨率,比如对着一个圆进行浏览的时候,这个圆是否正。这方面百度和google都未能找到满意的答案,因为Camera2相关的介绍太少,基本都是基于google demo在复述。自我夸奖下,能重构到我这个地步,没看到~
很遗憾,在研究了很久很久好几天,setAspectRatio,通过设置可见区域onMesure()和setDefaultBufferSize对texture配置显示比例。都未能达到十分满意的效果。对size我的建议是,一般来说,应用需要找到一个合理的显示比例,固定下来。然后分辨率寻找一个16:9或者4:3定下来。
有兴趣的这部分可以继续,代码中已经留下来了setPreviewSize内很多的策略。
总体来说,如果对Camera2的使用,如果认为查看google demo不清晰的,可以看看我的代码,从架构上很好的剥离了复杂的逻辑。更容易理解其中的关键。