收起侧边面板想"更爽地看画面",结果右侧凭空多出一块 420px 的黑边。日志一打就锁定了根因,但第一版修复方案直接把画面修成了黑屏------外部库的 setTransform,是应用层的禁区。
📌 先放结论
一句话版:
影像预览区的变换矩阵基于旧容器尺寸计算,容器变宽后矩阵没有跟着重算,新增区域被映射到了纹理之外 → 黑边。
而且,修复它最忌讳的方式是"应用层自己算矩阵再 setTransform"------我第一版就这么干的,结果整个画面黑屏,全部回退。最终方案只用了一行 setZoom(getZoom()),借外部库自己的能力重建矩阵,干净上岸。
🎯 一、问题背景
在一款基于 Android 全站仪主机的测量软件中,点测量(影像瞄准)界面是典型的横屏双栏布局:
- 左侧:信息栏(点名、坐标、角度读数等)
- 右侧:USB 摄像头影像预览区
交互上,用户点左上角按钮可以把左侧信息栏收起,让影像区占满更宽的空间------这本应"更爽地看画面"。但实际表现是:
收起左侧面板后,影像画面停留在收起前的尺寸,右侧新空出来的区域一片漆黑,没有铺满。
用户期望的最终效果:影像随容器变宽而等比缩放填满(即 center-crop,必要时裁剪一点画面,但绝不留黑边)。
🔍 二、先埋点,再动手(铁律)
这类"布局变了画面没变"的问题,最忌讳上来就猜着改。我们严格遵守一条经验铁律:连续 2 次修复仍失败,立即停手加日志,而不是继续猜。
我们在影像刷新入口处加临时埋点,打印收起前后关键状态:
plaintext
// 收起前
viewW=784 viewH=502 bufW=1280 bufH=720 scaleX=-1.0 scaleY=-1.0 transX=784.0 transY=502.0
// 收起后
viewW=1204 viewH=502 bufW=1280 bufH=720 scaleX=-1.0 scaleY=-1.0 transX=784.0 transY=502.0
关键观察:
- 容器宽度从 784 变成 1204(变宽了 420);
- 但
scaleX / scaleY / transX / transY完全没变------影像的几何变换矩阵没有跟着容器重算; scaleX = -1.0、scaleY = -1.0说明当前处于**正倒镜翻转(旋转 180°)**状态,平移量transX = 784 = 2 × 392是基于旧宽度算出来的。
由此几乎可以锁定:画面黑边来自**"变换矩阵基于旧尺寸、容器却变大了"**,新增的 [784, 1204] 区域被映射到了纹理之外。
🧠 三、根因分析
影像预览区是一个自定义的 TextureView 子类(来自预编译的外部相机库,源码不可改 )。它的绘制靠 applyAllTransforms() 方法,内部用 setTransform(Matrix) 把摄像头帧绘制到视图上。
我们反编译外部库的 sources jar 确认了调用逻辑:
applyAllTransforms()只在手势缩放 / 镜像 / 旋转时触发,并不会监听容器尺寸变化;- 应用层现有的"刷新"方法,只更新了画点 / 瞄准中心的换算参数(
CenterParametersInstance),不重算画面尺寸; - 因此,当左侧面板收起、容器变宽时,库不会自动重新铺满,画面停留在旧尺寸------正是日志里看到的现象。
💡 补充一个细节:矩阵里的
scaleX/Y = -1是正倒镜翻转,修复时必须保留这个翻转方向,否则画面会左右 / 上下反掉。
💥 四、失败的第一版方案(黑屏教训)
最初我们想"应用层兜底自己算":读取当前矩阵的翻转符号,按 center-crop 公式(scale = max(viewW/bufW, viewH/bufH))自算一个 Matrix,在 textureView.post { setTransform(matrix) } 里设置。
结果运行后整个画面黑屏,看不到任何摄像头内容。
考虑到 TextureView 的 transform 与底层 native 层的 SurfaceTexture 渲染存在强耦合,自算矩阵很容易和库的渲染管线冲突(尺寸来源不一致、画面被推到可视区外等)。这条直接 setTransform 的路子风险太高,全部回退。
⚠️ 教训:外部库的
setTransform不该由应用层越俎代庖去覆盖,要借库自己的能力重建。
✅ 五、最终方案:借库的"重建"能力
既然 applyAllTransforms() 能基于当前 view 尺寸正确重建翻转 + 缩放矩阵,我们只要**"再触发它一次"**即可,且必须保留翻转方向。
查阅库 API 后发现:setZoom(value) 内部会走到 setScaleFactor → applyAllTransforms(),而它**基于 getWidth()/getHeight()(新容器尺寸)**重新构建矩阵。于是方案非常轻量:
kotlin
fun refreshTextureView() {
usbCameraHelper.refreshTextureView()
// 容器尺寸变化(收起/展开侧边面板)后,触发库基于新尺寸重建 transform,
// 让影像重新铺满容器、不露黑边。库的 transform 记录的是基于旧尺寸的翻转+平移,
// 尺寸变化后不会自动更新,需重新走一遍 applyAllTransforms()。
val tv = usbCameraHelper.getCurrentTextureView() ?: return
tv.post {
if (cameraPrepared) {
usbCameraHelper.setZoom(usbCameraHelper.getZoom())
}
}
}
要点:
- 用
usbCameraHelper.setZoom(usbCameraHelper.getZoom())------传入当前已生效的缩放值,不改动用户已有的缩放级别,只借它触发一次"基于新尺寸重建矩阵"; tv.post { }确保在布局完成(拿到新viewW/viewH)后再执行,避免拿到旧尺寸;if (cameraPrepared)守卫:相机没就绪时不应触发重建;- 全程不碰
setTransform、不读getCurrentCameraSize、不自己算矩阵,完全复用库自身逻辑,因此不黑屏、也保留了正倒镜翻转。
📊 六、效果与边界
- 收起 / 展开左侧面板后,影像都会重新铺满预览区,不再露黑边;
- 正倒镜翻转方向被库自动保留,画面方向正确;
- 收起后用户若再手势缩放 / 倒镜,库会基于新尺寸重建矩阵(里面已经包含本修复),行为一致;
- 当前方案是"让画面重新铺满",并非额外的 center-crop 放大------它消除了黑边(用户核心诉求),具体是显示更多缓冲区域还是等比裁剪,取决于库内置的适配策略,与我们的目标(无黑边、铺满)一致。
📝 七、经验沉淀
- 先埋点,再动手 :布局类诡异 bug,第一手日志比十次猜测值钱。本次靠
viewW前后对比 + transform 矩阵快照,直接锁定根因。 - 外部库
setTransform是禁区:应用层越俎代庖覆盖矩阵的代价是黑屏。正确姿势是"借库自己的能力重建",而非自己算。 - 注意隐藏状态(翻转 / 镜像) :修复"铺满"问题时,矩阵里可能夹带方向信息(
scaleX/Y = -1),改尺寸逻辑必须保留这些符号,否则画面反向。 - 轻量触发优于重写 :用
setZoom(getZoom())这种"以不变应万变"的触发,既达到重建目的,又不引入新的不变量管理负担。
🎬 写在最后
回头看,这个 bug 的难度不在"想明白",而在"忍住不自己动手"。
日志已经把根因写在了脸上------矩阵没重算;但第一版方案还是败给了"应用层兜底"的冲动,把黑边修成了黑屏。真正上岸,靠的是信任库自己的重建能力 ,外加一行看似"什么都没做"的 setZoom(getZoom())。
复杂的从来不是问题本身,而是你愿不愿意先看清、再动手。
如果这篇排障复盘对你有帮助,点个 👍 收藏一下,也欢迎评论区聊聊你被外部 SDK / 预编译库"坑"过的经历~