1. 前言
这段时间,在使用 natario1/CameraView 来实现带滤镜的预览
、拍照
、录像
功能。
由于CameraView
封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView
的使用进入深水区,逐渐出现满足不了我们需求的情况。
Github
中的issues
中,有些BUG
作者一直没有修复。
那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。
而这篇文章是其中关于CameraView
怎么进行拍照的源码解析。
以下源码解析基于CameraView 2.7.2
implementation("com.otaliastudios:cameraview:2.7.2")
为了在博客上更好的展示,本文贴出的代码进行了部分精简
拍照的入口是cameraView.takePicture()
,我们从这个方法开始解析。
2. CameraEngine.takePicture
cameraView.takePicture()
会调用到mCameraEngine.takePicture()
,
这个PictureResult.Stub
是一个参数封装类,这里重新创建了一个PictureResult.Stub
并传入takePicture()
方法中。
mCameraEngine
是CameraEngine
抽象类,实现类有Camera1Engine
和Camera2Engine
。
public void takePicture() {
PictureResult.Stub stub = new PictureResult.Stub();
mCameraEngine.takePicture(stub);
}
我们这里以Camera2
为例,可以看到这里对stub
参数封装类赋值了一些参数(摄像头ID、图片格式等
),并调用了onTakePicture
public void takePicture(final PictureResult.Stub stub) {
final boolean metering = mPictureMetering;
getOrchestrator().scheduleStateful("take picture", CameraState.BIND,
new Runnable() {
@Override
public void run() {
if (isTakingPicture()) return;
if (mMode == Mode.VIDEO) {
throw new IllegalStateException("Can't take hq pictures while in VIDEO mode");
}
stub.isSnapshot = false;
stub.location = mLocation;
stub.facing = mFacing;
stub.format = mPictureFormat;
onTakePicture(stub, metering);
}
});
}
3. onTakePicture
接着来看onTakePicture()
设置Rotation
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
设置设定好拍照图片尺寸
stub.size = getPictureSize(Reference.OUTPUT);
接着调用mPictureRecorder.take()
,mPictureRecorder
是PictureRecorder
接口,具体实现是Full2PictureRecorder
,专门用来调用Camera2 API
捕获图片。
mPictureRecorder = new Full2PictureRecorder(stub, this, builder,mPictureReader);
mPictureRecorder.take();
来看一下完整的重点代码
@EngineThread
@Override
protected void onTakePicture(@NonNull final PictureResult.Stub stub, boolean doMetering) {
//...省略不重要代码...
//设置Rotation
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
//设置设定好拍照图片尺寸
stub.size = getPictureSize(Reference.OUTPUT);
//...省略不重要代码...
CaptureRequest.Builder builder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
applyAllParameters(builder, mRepeatingRequestBuilder);
mPictureRecorder = new Full2PictureRecorder(stub, this, builder,
mPictureReader);
mPictureRecorder.take();
}
4. Full2PictureRecorder.take
再来看Full2PictureRecorder.take()
@Override
public void take() {
mAction.start(mHolder);
}
这里调用了mAction.start(mHolder)
,来看一下mAction
初始化
4.1 初始化BaseAction
mAction = new BaseAction() {
@Override
protected void onStart(ActionHolder holder) { //省略了代码,这里只看结构 }
@Override
public void onCaptureStarted(ActionHolder holder,CaptureRequest request) { //省略了代码,这里只看结构 }
@Override
public void onCaptureCompleted(ActionHolder holder,CaptureRequest request,TotalCaptureResult result) { //省略了代码,这里只看结构 }
};
mAction
是BaseAction
抽象类,有onStart
、onCaptureStarted
、onCaptureProgressed
、onCaptureCompleted
等方法。mHolder
是构造方法传入过来的Camera2Engine
,实现了ActionHolder
接口。
4.2 BaseAction.onStart
调用了mAction.start(mHolder)
后,mAction
和mHolder
会建立关联,也就是BaseAction
和Camera2Engine
会建立关联,具体代码为Camera2Engine.addAction(BaseAction)
将其添加到Actions
列表中,并在合适的时机回调BaseAction
的onCaptureStarted
、onCaptureProgressed
、onCaptureCompleted
方法。
mAction
和mHolder
建立关联后,会调用onStart
方法,这里是对mPictureBuilder
这个建造者设置了一些值
@Override
protected void onStart(@NonNull ActionHolder holder) {
super.onStart(holder);
//mPictureBuilder是一个建造者,这里给建造者设置一些值
mPictureBuilder.addTarget(mPictureReader.getSurface());
if (mResult.format == PictureFormat.JPEG) {
mPictureBuilder.set(CaptureRequest.JPEG_ORIENTATION, mResult.rotation);
}
mPictureBuilder.setTag(CameraDevice.TEMPLATE_STILL_CAPTURE);
//应用这个建造者
holder.applyBuilder(this, mPictureBuilder);
}
再来看onCaptureStarted
,调用了dispatchOnShutter
来回调
@Override
public void onCaptureStarted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request) {
super.onCaptureStarted(holder, request);
if (request.getTag() == (Integer) CameraDevice.TEMPLATE_STILL_CAPTURE) {
dispatchOnShutter(false);
setState(STATE_COMPLETED);
}
}
4.3 BaseAction.onCaptureCompleted
再来看onCaptureCompleted
,主要是在DNG
格式的时候,做了一些特殊处理。
@Override
public void onCaptureCompleted(ActionHolder holder, CaptureRequest request, TotalCaptureResult result) {
if (mResult.format == PictureFormat.DNG) {
mDngCreator = new DngCreator(holder.getCharacteristics(this), result);
mDngCreator.setOrientation(ExifHelper.getExifOrientation(mResult.rotation));
if (mResult.location != null) {
mDngCreator.setLocation(mResult.location);
}
}
}
结果发现这里不是重点,那么重点在哪里呢 ?
5. 设置OnImageAvailableListener监听
在Full2PictureRecorder
初始化构造方法中,还有这么一句
mPictureReader.setOnImageAvailableListener(this, WorkerHandler.get().getHandler());
在Android
的Camera2 API
中,setOnImageAvailableListener
方法用于注册一个回调监听器,以在每次图像数据可用时接收通知。
5.1 onImageAvailable回调
来看onImageAvailable
回调方法,这里会调用android.media.ImageReader.acquireNextImage()
来获取图像数据。
然后如果是JPEG
格式,则会调用readJpegImage()
方法读取图像数据
最后都会调用dispatchResult
来分发数据。
@Override
public void onImageAvailable(ImageReader reader) {
Image image = null;
try {
image = reader.acquireNextImage();
switch (mResult.format) {
case JPEG: readJpegImage(image); break;
case DNG: readRawImage(image); break;
default: throw new IllegalStateException("Unknown format: " + mResult.format);
}
} catch (Exception e) {
mResult = null;
mError = e;
dispatchResult();
return;
} finally {
if (image != null) {
image.close();
}
}
dispatchResult();
}
5.2 读取JPEG数据
我们先来看下readJpegImage()
方法
private void readJpegImage(@NonNull Image image) {
//从Iamge中读取数据
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
mResult.data = bytes;
//根据Exif设置rotation
mResult.rotation = 0;
ExifInterface exif = new ExifInterface(new ByteArrayInputStream(mResult.data));
int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
mResult.rotation = ExifHelper.getOrientation(exifOrientation);
}
5.3 分发回调
再来看dispatchResult
,最终会调用到CameraView
中的dispatchOnPictureTaken
,这个方法中会遍历mListeners
回调列表,调用onPictureTaken()
@Override
public void dispatchOnPictureTaken(final PictureResult.Stub stub) {
mUiHandler.post(new Runnable() {
@Override
public void run() {
PictureResult result = new PictureResult(stub);
for (CameraListener listener : mListeners) {
listener.onPictureTaken(result);
}
}
});
}
而mListeners
什么时候被添加呢 ? CameraView
中有一个addCameraListener
方法,专门直接添加回调。
public void addCameraListener(CameraListener cameraListener) {
mListeners.add(cameraListener);
}
5.4 设置回调
所以我们只要添加了这个回调,并实现onPictureTaken
方法,就可以在onPictureTaken()
中获取到拍照后的图像信息了。
binding.cameraView.addCameraListener(object : CameraListener() {
override fun onPictureTaken(result: PictureResult) {
super.onPictureTaken(result)
//拍照回调
val bitmap = BitmapFactory.decodeByteArray(result.data, 0, result.data.size)
bitmap?.also {
Toast.makeText(this@Test2Activity, "拍照成功", Toast.LENGTH_SHORT).show()
//将Bitmap设置到ImageView上
binding.img.setImageBitmap(it)
val file = getNewImageFile()
//保存图片到指定目录
ImageUtils.save(it, file, Bitmap.CompressFormat.JPEG)
}
}
})
6. 其他
6.1 CameraView源码解析系列
Android 相机库CameraView源码解析 (一) : 预览-CSDN博客
Android 相机库CameraView源码解析 (二) : 拍照-CSDN博客