一个黑边 Bug 修了两版:自己算矩阵直接黑屏,借库重建只用了一行 setZoom

收起侧边面板想"更爽地看画面",结果右侧凭空多出一块 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 确认了调用逻辑:

  1. applyAllTransforms() 只在手势缩放 / 镜像 / 旋转时触发,并不会监听容器尺寸变化;
  2. 应用层现有的"刷新"方法,只更新了画点 / 瞄准中心的换算参数(CenterParametersInstance),不重算画面尺寸
  3. 因此,当左侧面板收起、容器变宽时,库不会自动重新铺满,画面停留在旧尺寸------正是日志里看到的现象。

💡 补充一个细节:矩阵里的 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 放大------它消除了黑边(用户核心诉求),具体是显示更多缓冲区域还是等比裁剪,取决于库内置的适配策略,与我们的目标(无黑边、铺满)一致。

📝 七、经验沉淀

  1. 先埋点,再动手 :布局类诡异 bug,第一手日志比十次猜测值钱。本次靠 viewW 前后对比 + transform 矩阵快照,直接锁定根因。
  2. 外部库 setTransform 是禁区:应用层越俎代庖覆盖矩阵的代价是黑屏。正确姿势是"借库自己的能力重建",而非自己算。
  3. 注意隐藏状态(翻转 / 镜像) :修复"铺满"问题时,矩阵里可能夹带方向信息(scaleX/Y = -1),改尺寸逻辑必须保留这些符号,否则画面反向。
  4. 轻量触发优于重写 :用 setZoom(getZoom()) 这种"以不变应万变"的触发,既达到重建目的,又不引入新的不变量管理负担。

🎬 写在最后

回头看,这个 bug 的难度不在"想明白",而在"忍住不自己动手"。

日志已经把根因写在了脸上------矩阵没重算;但第一版方案还是败给了"应用层兜底"的冲动,把黑边修成了黑屏。真正上岸,靠的是信任库自己的重建能力 ,外加一行看似"什么都没做"的 setZoom(getZoom())

复杂的从来不是问题本身,而是你愿不愿意先看清、再动手。


如果这篇排障复盘对你有帮助,点个 👍 收藏一下,也欢迎评论区聊聊你被外部 SDK / 预编译库"坑"过的经历~

相关推荐
丑过三八线1 小时前
001 -【FastAPI 入门教程】 FastAPI Helloword
前端·chrome·fastapi
禁止摆烂_才浅1 小时前
Axios 高频面试题
前端·面试·axios
何时梦醒1 小时前
TypeScript 工具类型一篇讲透:Pick、Omit、Partial、Exclude、Record、ReturnType、keyof
前端·面试·typescript
用户2181697049301 小时前
Flutter (十九) Tabbar
前端
默_笙1 小时前
🍳 受控组件和非受控组件,我纠结了一整天,最后用"房东和租客"讲明白了
前端·javascript
缓冲中请稍后1 小时前
前端HTTP请求完全指南:从基础到LLM接口调用实战
前端·面试
半个落月1 小时前
React 受控组件与非受控组件详解:从输入框到表单校验
前端·react.js
BreezeJiang1 小时前
写了 display:flex,为什么三栏布局还没完成?
前端·css
枢影Kernel1 小时前
Android CLI 与 Android Skills 最佳实践:把 AI Agent 接入可验证的 Android 开发流程
android