1. 前言
这段时间,在使用 natario1/CameraView 来实现带滤镜的预览
、拍照
、录像
功能。
由于CameraView
封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView
的使用进入深水区,逐渐出现满足不了我们需求的情况。
Github
中的issues
中,有些BUG
作者一直没有修复。
那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。
上一篇文章我们已经复现了CameraView
在使用多滤镜MultiFilter
的时候哦度会遇到拍照错乱的BUG
,这篇文章我们来解决这个BUG
。
以下源码解析基于CameraView 2.7.2
implementation("com.otaliastudios:cameraview:2.7.2")
为了在博客上更好的展示,本文贴出的代码进行了部分精简
2. CameraView滤镜预览的流程
关于CameraView
带滤镜预览的流程,我们在Android 相机库CameraView源码解析 (四) : 带滤镜预览中已经详细说明过了,这里我们在来简单说明一下。
- 首先在
CamerView
中,会调用View
生命周期的onAttachedToWindow
,去初始化GlCameraPreview
- 在
GlCameraPreview
的onCreateView
中,会初始化GLSurfaceView
,并调用GLSurfaceView.setRenderer()
将GLSurfaceView
和Renderer
建立关联 - 然后,
GlCameraPreview
会回调onSurfaceCreate()
和onSurfaceChanged()
- 当我们手动调用
requestRender
后,会调用onDrawFrame()
来重新渲染 - 拍照实现了
RendererFrameCallback
回调,会在回调中的onRendererTextureCreated()
、onRendererFilterChanged()
、onRendererFrame()
中,来实现带滤镜拍照功能
3. GLSurfaceView保存的图片尺寸的决定因素
Android
中GLSurfaceView
保存的图片尺寸,是和相机支持的尺寸有关,还是和GLSurfaceView
的尺寸有关呢 ?
GLSurfaceView
是Android
中用于显示OpenGL
渲染的视图,它的大小决定了OpenGL
渲染的区域。
当相机的原始图像被用于OpenGL
渲染时,会根据GLSurfaceView
的尺寸进行缩放或裁剪。
当你从glSurfaceView
中获取或保存图片时,获取到的是OpenGL
渲染在这个视图上的内容,因此图片的尺寸会和GLSurfaceView
的尺寸相同。
4. 预览过程中是怎么确定尺寸的
在预览过程中,也就是在GlCameraPreview
类中,回调onSurfaceChanged()
时,会传入宽高。
- 会调用
gl.glViewport()
确定OpenGL
在窗口中显示的区域范围 - 会调用
Filter.setSize()
将宽高尺寸设置给Filter
滤镜 dispatchOnSurfaceAvailable()
中会调用crop()
确定裁剪、缩放参数
public void onSurfaceChanged(GL10 gl, final int width, final int height) {
gl.glViewport(0, 0, width, height);
mCurrentFilter.setSize(width, height);
if (!mDispatched) {
dispatchOnSurfaceAvailable(width, height);
mDispatched = true;
} else if (width != mOutputSurfaceWidth || height != mOutputSurfaceHeight) {
dispatchOnSurfaceSizeChanged(width, height);
}
}
crop()
中会计算得到mCropping
、mCropScaleX
、mCropScaleY
,从而确定裁剪、缩放参数
protected void crop(@Nullable final CropCallback callback) {
if (mInputStreamWidth > 0 && mInputStreamHeight > 0 && mOutputSurfaceWidth > 0
&& mOutputSurfaceHeight > 0) {
float scaleX = 1f, scaleY = 1f;
AspectRatio current = AspectRatio.of(mOutputSurfaceWidth, mOutputSurfaceHeight);
AspectRatio target = AspectRatio.of(mInputStreamWidth, mInputStreamHeight);
if (current.toFloat() >= target.toFloat()) {
// We are too short. Must increase height.
scaleY = current.toFloat() / target.toFloat();
} else {
// We must increase width.
scaleX = target.toFloat() / current.toFloat();
}
mCropping = scaleX > 1.02f || scaleY > 1.02f;
mCropScaleX = 1F / scaleX;
mCropScaleY = 1F / scaleY;
getView().requestRender();
}
if (callback != null) callback.onCrop();
}
5. 拍照过程中是怎么确定尺寸的
在带滤镜拍照过程中,也就是在SnapshotGlPictureRecorder
中调用take()
方法的时候,会实现RendererFrameCallback
回调接口。
public void take() {
mPreview.addRendererFrameCallback(new RendererFrameCallback() {
@RendererThread
public void onRendererTextureCreated(int textureId) {
SnapshotGlPictureRecorder.this.onRendererTextureCreated(textureId);
}
@RendererThread
@Override
public void onRendererFilterChanged(@NonNull Filter filter) {
SnapshotGlPictureRecorder.this.onRendererFilterChanged(filter);
}
@RendererThread
@Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture,
int rotation, float scaleX, float scaleY) {
mPreview.removeRendererFrameCallback(this);
SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture,
rotation, scaleX, scaleY);
}
});
}
5.1 onRendererTextureCreated
在onRendererTextureCreated()
中,会调用computeCrop
来计算得到适合的尺寸,然后赋值给mResult.size
protected void onRendererTextureCreated(int textureId) {
mTextureDrawer = new GlTextureDrawer(textureId);
// Need to crop the size.
Rect crop = CropHelper.computeCrop(mResult.size, mOutputRatio);
mResult.size = new Size(crop.width(), crop.height());
//...省略了无关代码...
}
5.2 onRendererFilterChanged
在onRendererFilterChanged
中,会调用filter.copy()
,拷贝一份滤镜,然后将拷贝的滤镜设置给GlTextureDrawer
mTextureDrawer.setFilter(filter.copy());
6. Filter.copy
我们再来看一下Filter.copy
的逻辑
6.1 BaseFilter.copy
BaseFilter
中,内部调用了getClass().newInstance()
来反射得到一个新的BaseFilter
,并赋值了Size
,如果实现了OneParameterFilter
或TwoParameterFilter
接口,还会给拷贝相关的参数过来。
protected Size size;
@Override
public void setSize(int width, int height) {
size = new Size(width, height);
}
public final BaseFilter copy() {
BaseFilter copy = onCopy();
if (size != null) {
copy.setSize(size.getWidth(), size.getHeight());
}
if (this instanceof OneParameterFilter) {
((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1());
}
if (this instanceof TwoParameterFilter) {
((TwoParameterFilter) copy).setParameter2(((TwoParameterFilter) this).getParameter2());
}
return copy;
}
protected BaseFilter onCopy() {
try {
return getClass().newInstance();
} catch (IllegalAccessException e) {
throw new RuntimeException("Filters should have a public no-arguments constructor.", e);
} catch (InstantiationException e) {
throw new RuntimeException("Filters should have a public no-arguments constructor.", e);
}
}
6.2 MultiFilter.copy
MultiFilter
中有一个filters
滤镜列表,用来存储多个子滤镜。
setSize
的时候,会赋值给size
变量,并遍历filters
列表调用maybeSetSize()
maybeSetSize()
内部会根据filter
取到state
,如果size
和state.size
不同,就会将size
赋值给state.size
,并调用filter.size()
将size赋值给filter
,确保filter
中的filter
是最新的
copy
的时候- 会新创建一个
MultiFilter
,并调用setSize()
- 遍历
filters
列表,调用filter.copy()
,并调用MultiFilter.addFilter()
将拷贝的filter
添加到MultiFilter
中
- 会新创建一个
final List<Filter> filters = new ArrayList<>();
final Map<Filter, State> states = new HashMap<>();
private Size size = null;
@Override
public void setSize(int width, int height) {
size = new Size(width, height);
synchronized (lock) {
for (Filter filter : filters) {
maybeSetSize(filter);
}
}
}
private void maybeSetSize(@NonNull Filter filter) {
State state = states.get(filter);
if (size != null && !size.equals(state.size)) {
state.size = size;
state.sizeChanged = true;
filter.setSize(size.getWidth(), size.getHeight());
}
}
@Override
public Filter copy() {
synchronized (lock) {
MultiFilter copy = new MultiFilter();
if (size != null) {
copy.setSize(size.getWidth(), size.getHeight());
}
for (Filter filter : filters) {
copy.addFilter(filter.copy());
}
return copy;
}
}
7. 造成多滤镜拍照错乱的原因分析
上篇文章我们总结了下这个BUG
,是跟CameraView
的尺寸和摄像头选取的分辨率匹配有关。
- 使用单个滤镜
- 一切正常
- 使用多个滤镜,预览正常,但是
- 手机选用的摄像头分辨率比
CameraView
分辨率高 : 照片得到的画面会放大 - 手机选用的摄像头分辨率比
CameraView
分辨率低 : 拍照得到的画面会缩小,会有黑边
- 手机选用的摄像头分辨率比
结合我们上面分析了源码,那么为什么会导致这个BUG
呢 ? 我们再来理一下逻辑
- 预览的时候
onSurfaceChanged(width, height)
glViewport(0, 0, width, height)
: 确定OpenGL
窗口的显示范围Filter.setSize(width, height)
: 将宽高设置给Filter
- 带滤镜拍照的时候
onRendererTextureCreated
computeCrop()
: 确定裁剪尺寸,并赋值给mResult.size
onRendererFilterChanged()
filter.copy()
: 拷贝滤镜,并赋值给GlTextureDrawer
- 这个时候拷贝后的
filter
中的尺寸是预览时候的GlSurfaceView
的宽高
- 这个时候拷贝后的
再来打印下日志 (预览摄像头分辨率选用1080*1920
,屏幕分辨率1080*2412
)的情况下
11:02:27.349 I CameraActivity onCreate
11:02:27.351 I CameraActivity onStart
11:02:27.351 I CameraActivity onResume
11:02:27.385 I 屏幕尺寸:width:1080 height:2412
11:02:27.385 I CameraView尺寸:width:1080 height:2412
11:02:27.389 I GlCameraPreview.onSurfaceCreated
11:02:27.389 I GlCameraPreview.onSurfaceChanged width:1080 height:2412
11:02:27.495 I 选取的摄像头预览尺寸(setPreviewStreamSize): 1080x1920
11:02:27.622 I MultiFilter FrameBufferCreated:CrossProcessFilter width:1080 height:2412
11:02:34.688 I CameraActivity ---- 点击拍照(takePictureSnapshot) ----
11:02:34.712 I SnapshotGlPictureRecorder onRendererTextureCreated size:860x1920
11:02:34.712 I SnapshotGlPictureRecorder onRendererFilterChanged copyFilter.size:1080x2412
11:02:34.732 I SnapshotGlPictureRecorder onRendererFrame->takeFrame size:860x1920 rotation:0 scaleX:0.79602 scaleY:1.0
11:02:34.758 I MultiFilter FrameBufferCreated:CrossProcessFilter width:1080 height:2412
11:02:34.820 I MultiFilter maybeDestroyFramebuffer
现在我们可以来解答这个BUG了
7.1 为什么会出现拍照错乱的情况 ?
根据这个逻辑,我们可以推测出,是带滤镜拍照的时候的filter
宽高用的GlSurfaceView
的宽高(比如1080x2316
),而实际上带滤镜拍照的EglSurface
的宽高是mResult.size
(通过computeCrop
估算得到,比如1910x4096
),两者是不一致的,导致最终拍照出现了错乱。
public class CropHelper {
public static Rect computeCrop(@NonNull Size currentSize, @NonNull AspectRatio targetRatio) {
int currentWidth = currentSize.getWidth();
int currentHeight = currentSize.getHeight();
if (targetRatio.matches(currentSize, 0.0005F)) {
return new Rect(0, 0, currentWidth, currentHeight);
}
AspectRatio currentRatio = AspectRatio.of(currentWidth, currentHeight);
int x, y, width, height;
if (currentRatio.toFloat() > targetRatio.toFloat()) {
height = currentHeight;
width = Math.round(height * targetRatio.toFloat());
y = 0;
x = Math.round((currentWidth - width) / 2F);
} else {
width = currentWidth;
height = Math.round(width / targetRatio.toFloat());
y = Math.round((currentHeight - height) / 2F);
x = 0;
}
return new Rect(x, y, x + width, y + height);
}
}
7.2 为什么预览时正常的,拍照才出现这个问题 ?
这个详见我的这篇文章 为什么相机库CameraView预览和拍照的效果不一致 ?,本质是因为在CameraView
中,GlSurfaceView
是专门用来预览,而作者自己实现的EglSurface
是用来拍照时候存储图像的,所以可能会出现预览效果和拍照的实际效果不一致的情况。
7.3 为什么使用单个滤镜的时候,没有这个问题,而使用多个滤镜就有问题了 ?
因为在MultiFilter
中,如果有多个滤镜,需要通过创建一个新的GlTexture
,并传入width
和height
,从而实现多个滤镜叠加。
而单个滤镜的情况下,是不需要多这一步操作的,所以单个滤镜情况下,直接就return
了,没有走后面的逻辑,所以就不会有这个问题。
private void maybeCreateFramebuffer(@NonNull Filter filter, boolean isFirst, boolean isLast) {
State state = states.get(filter);
if (isLast) {
state.sizeChanged = false;
//单个滤镜的情况下,直接return
return;
}
//多个滤镜才会走这里的逻辑
if (state.sizeChanged) {
maybeDestroyFramebuffer(filter);
state.sizeChanged = false;
}
if (!state.isFramebufferCreated) {
state.isFramebufferCreated = true;
state.outputTexture = new GlTexture(GLES20.GL_TEXTURE0,
GLES20.GL_TEXTURE_2D,
state.size.getWidth(),
state.size.getHeight());
state.outputFramebuffer = new GlFramebuffer();
state.outputFramebuffer.attach(state.outputTexture);
}
}
8. 如何解决该BUG
经过上文,我们已经知道 : 预览的时候的 filter
宽高用的 GlSurfaceView
的宽高,而实际上拍照的 EglSurface
的宽高是mResult.size
,两者是不一致的,导致最终拍照出现了错乱。
所以需要在filter.copy()
拷贝滤镜之后,再设置一下 Size
,确保尺寸和 EglSurface
的尺寸一样,就可以解决这个问题了。
解决办法 : 将CameraView
源码中的SnapshotGlPictureRecorder.java
的onRendererFilterChanged
方法
protected void onRendererFilterChanged(@NonNull Filter filter) {
mTextureDrawer.setFilter(filter.copy());
}
修改为如下代码即可
protected void onRendererFilterChanged(@NonNull Filter filter) {
Filter copyFilter = filter.copy();
copyFilter.setSize(mResult.size.getWidth(), mResult.size.getHeight());
mTextureDrawer.setFilter(copyFilter);
}
9. 其他
9.1 CameraView源码解析系列
Android 相机库CameraView源码解析 (一) : 预览-CSDN博客
Android 相机库CameraView源码解析 (二) : 拍照-CSDN博客
Android 相机库CameraView源码解析 (三) : 滤镜相关类说明-CSDN博客
Android 相机库CameraView源码解析 (四) : 带滤镜拍照-CSDN博客
Android 相机库CameraView源码解析 (五) : 保存滤镜效果-CSDN博客
文章评论