这段时间,在使用 natario1/CameraView 来实现带滤镜的预览
、拍照
、录像
功能。
由于CameraView
封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView
的使用进入深水区,逐渐出现满足不了我们需求的情况。
Github
中的issues
中,有些BUG
作者一直没有修复。
那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。
上一篇文章我们已经复现了CameraView
在使用多滤镜MultiFilter
的时候哦度会遇到拍照错乱的BUG
,这篇文章我们来解决这个BUG
。
以下源码解析基于CameraView 2.7.2
implementation("com.otaliastudios:cameraview:2.7.2")
为了在博客上更好的展示,本文贴出的代码进行了部分精简
关于CameraView
带滤镜预览的流程,我们在Android 相机库CameraView源码解析 (四) : 带滤镜预览中已经详细说明过了,这里我们在来简单说明一下。
CamerView
中,会调用View
生命周期的onAttachedToWindow
,去初始化GlCameraPreview
GlCameraPreview
的onCreateView
中,会初始化GLSurfaceView
,并调用GLSurfaceView.setRenderer()
将GLSurfaceView
和Renderer
建立关联GlCameraPreview
会回调onSurfaceCreate()
和onSurfaceChanged()
requestRender
后,会调用onDrawFrame()
来重新渲染RendererFrameCallback
回调,会在回调中的onRendererTextureCreated()
、onRendererFilterChanged()
、onRendererFrame()
中,来实现带滤镜拍照功能Android
中GLSurfaceView
保存的图片尺寸,是和相机支持的尺寸有关,还是和GLSurfaceView
的尺寸有关呢 ?
GLSurfaceView
是Android
中用于显示OpenGL
渲染的视图,它的大小决定了OpenGL
渲染的区域。
当相机的原始图像被用于OpenGL
渲染时,会根据GLSurfaceView
的尺寸进行缩放或裁剪。
当你从glSurfaceView
中获取或保存图片时,获取到的是OpenGL
渲染在这个视图上的内容,因此图片的尺寸会和GLSurfaceView
的尺寸相同。
在预览过程中,也就是在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();
}
在带滤镜拍照过程中,也就是在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);
}
});
}
在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());
//...省略了无关代码...
}
在onRendererFilterChanged
中,会调用filter.copy()
,拷贝一份滤镜,然后将拷贝的滤镜设置给GlTextureDrawer
mTextureDrawer.setFilter(filter.copy());
我们再来看一下Filter.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);
}
}
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;
}
}
上篇文章我们总结了下这个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了
根据这个逻辑,我们可以推测出,是带滤镜拍照的时候的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);
}
}
这个详见我的这篇文章 为什么相机库CameraView预览和拍照的效果不一致 ?,本质是因为在CameraView
中,GlSurfaceView
是专门用来预览,而作者自己实现的EglSurface
是用来拍照时候存储图像的,所以可能会出现预览效果和拍照的实际效果不一致的情况。
因为在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);
}
}
Android 相机库CameraView源码解析 (一) : 预览-CSDN博客
Android 相机库CameraView源码解析 (二) : 拍照-CSDN博客
Android 相机库CameraView源码解析 (三) : 滤镜相关类说明-CSDN博客
Android 相机库CameraView源码解析 (四) : 带滤镜拍照-CSDN博客
Android 相机库CameraView源码解析 (五) : 保存滤镜效果-CSDN博客