Android WebView Checkbox 点击导致页面滚动问题分析

问题描述

在 Android WebView 中,点击列表底部的隐藏复选框时,页面会突然向上滚动。同样的代码在 iOS 上正常运行,且另一个相似的页面也没有这个问题。

根本原因分析

时序竞争问题

0ms:     用户触摸
5ms:     <input> 获得焦点
10ms:    Android WebView 触发自动滚动(确保元素可见)
300ms:   自定义指令 (debounceTime) 才执行用户代码
         此时滚动已完成,恢复操作已太晚

关键点:Android WebView 会在 <input> 聚焦时自动调用 scrollIntoView(),而 iOS 不会。

事件绑定方式的影响

问题代码:事件直接绑定在组件上

<app-checkbox-round (appxxxClick)="onSelectedChange(item)">
  <!-- 内部包含 <input type="checkbox" class="sr-only"> -->
</app-checkbox-round>

→ 直接点击 <input>,触发聚焦 → 浏览器滚动

正常代码:事件委托给外层容器

<div (appxxxClick)="onSelectedChange(item)">
  <app-checkbox-round><!-- ... --></app-checkbox-round>
</div>

→ 点击容器而非 <input>,浏览器不会强制聚焦 → 无滚动

为什么 (click)appxxxClick 更有效

appxxxClick 使用 debounceTime(300) 防抖,代码执行时机晚到来不及阻止滚动。原生 (click) 事件立即响应,能在浏览器默认行为前插手。

解决方案

方案1:原生 click 事件 + preventDefault(推荐)

onSelectedChange(event: Event, item: Item) {
  event.preventDefault();    // 阻止 input 聚焦
  event.stopPropagation();
  
  // 业务逻辑
  this.handleItemToggle(item);
}
<app-checkbox-round (click)="onSelectedChange($event, item)">
</app-checkbox-round>

方案2:事件委托到容器

<div (appxxxClick)="onSelectedChange(item)">
  <app-checkbox-round></app-checkbox-round>
</div>

不是很准确这个方案

两边代码“看起来一样”但行为仍然不同,说明真正的差异不在 Checkbox 那一层,而在页面容器的结构和滚动上下文。关键点有两个:

- `a-list` 的滚动容器是 `grid` 布局里用 `calc(100vh - …)` 算出来的高度,有底部固定按钮、再加上外层 `overflow:hidden` 的结构。这种布局会让 Android WebView 在计算“元素是否在可视区”时更敏感,一旦它想聚焦到隐藏的 `<input>`,滚动保护就被触发。

- `b-result` 则是整个页面 `position:fixed` + `flex-column`,可滚动区是简单的 `flex:1`。浏览器更容易判断“已在视口中”,所以没有触发同样的滚动。再加上它本地直接修改 `isChecked`,重渲染发生得更早,进一步压盖了浏览器的默认行为。

所以,即便 Checkbox 周围的 DOM 结构你已经对齐了,滚动容器的布局方式仍然不同,Android WebView 的焦点逻辑仍然能被触发。要彻底避免这个问题,只能在事件上主动 `preventDefault()`/`stopPropagation()`,或者改成原生 `(click)` 并阻止默认行为,提前终止浏览器那条“聚焦→滚动”的链路,这是唯一不依赖外部布局细节的确定性方案。

关键洞察

  1. 事件目标很重要:直接与 <input> 交互 vs 与容器交互,WebView 的处理逻辑完全不同。

  2. 指令延迟带来的风险:自定义指令的 300ms 防抖让我们无法及时拦截浏览器的默认行为,原生事件更可控。

  3. 隐藏元素仍可聚焦<input class="sr-only"> 虽然不可见,但浏览器仍会在聚焦时滚动。

  4. 平台差异需测试:同样的代码在不同平台表现迥异,需在真实设备上验证关键交互。

最佳实践

  • 对包含隐藏表单元素的组件,优先使用原生事件而非自定义防抖指令。
  • 在事件处理函数开始立即调用 preventDefault(),确定性地阻止浏览器默认行为。
  • 将交互事件绑定到"稳定"的容器(不会被聚焦的 div),而不是表单元素。
  • 在 Android 低端设备上充分测试,这些问题往往更明显。

结论:理解浏览器与 WebView 的焦点和滚动机制,选择合适的事件绑定方式和时序控制,是解决跨平台兼容问题的关键。