返回博客

微信小程序吸顶触发偏晚:把元素移出 scroll-view 后阈值没跟着更新

在一个 C 端首页里,原本搜索栏放在 scroll-view 内部随内容一起滚动,后来产品要求把它固定到顶部、不参与滚动。改完布局之后,下方 Tab 的吸顶效果立刻出问题了:用户向下滚动,明显感觉 Tab 已经滚出屏幕好一截才突然吸上去,有明显的"滞后感"和位置跳变。

现象

  • Tab 吸顶触发时机比预期晚约 60px(恰好是搜索栏的高度)
  • 滚动回来时又正常了,但向下再滚一次依然偏晚
  • 代码逻辑没动,仅仅把搜索栏的 DOM 位置从 scroll-view 内部移到了外部

根因

问题出在 pageScroll 里的阈值比较逻辑:

// 旧代码(错误)
if (rect.top <= statusBarHeight) {
  // 触发吸顶
}

这里的 statusBarHeight 只代表状态栏高度。搜索栏在 scroll-view 内部时,scroll-view 的顶端就紧贴状态栏,所以这个阈值是正确的。

但把搜索栏提到 scroll-view 外部之后,页面结构变成了:

┌─────────────────────┐
│ 状态栏               │ ← statusBarHeight(约 44px)
├─────────────────────┤
│ 搜索栏(固定在外部)  │ ← 约 60px,新增!
├─────────────────────┤
│ scroll-view         │
│   Banner            │
│   Tab ←─────────────│── 吸顶阈值仍是 statusBarHeight ✗
│   内容瀑布           │
└─────────────────────┘

scroll-view 的起始位置下移了 60px,而代码里的阈值还是旧的 statusBarHeight,导致 Tab 的 rect.top 要再多走 60px 才能满足条件。

坑:把元素移出 scroll-view 后,吸顶阈值不会自动更新

吸顶阈值 = scroll-view 实际顶端在视口中的 Y 坐标,不是固定值。只要 scroll-view 上方还有其他元素(不论是 fixed 还是 flow),阈值就得重新量。用硬编码的 statusBarHeight 只在 scroll-view 顶部恰好贴着状态栏时才正确。

正解

createSelectorQuery 动态量出头部元素的 bottom,把它作为吸顶阈值。rect.bottom 直接给出头部底边的视口 Y 坐标,正好等于 scroll-view 的起始位置。

第一步:给头部元素加 id

<view id="search-header" class="search-row" style="padding-top:{{statusBarHeight}}px">
  <!-- 搜索栏内容 -->
</view>

第二步:在 onLoad 里量一次,存到实例变量

onLoad: function () {
  var that = this;
  that.setData({ statusBarHeight: that.store.data.topLift });
 
  setTimeout(() => {
    wx.createSelectorQuery()
      .select('#search-header')
      .boundingClientRect((rect) => {
        if (rect) {
          that._stickyThreshold = rect.bottom; // 头部底边 = scroll-view 顶端
        } else {
          that._stickyThreshold = that.store.data.topLift + 60; // 布局未就绪时的兜底值
        }
      }).exec();
  }, 500); // 等 layout 稳定,也可改用 onReady
},

第三步:pageScroll 用动态阈值替换硬编码

pageScroll: function (e) {
  var that = this;
  var threshold = that._stickyThreshold || (that.store.data.topLift + 60);
  wx.createSelectorQuery().in(this)
    .select('#waterfall-tab')
    .boundingClientRect((rect) => {
      if (rect.top <= threshold) {
        if (!that.data.tabFix) that.setData({ tabFix: true });
      } else {
        that.setData({ tabFix: false });
      }
    }).exec();
},
正解:用 rect.bottom 量头部底边,不要手算高度

rect.bottom 自动包含 padding、margin、状态栏偏移——不需要手算"statusBarHeight + 搜索栏高度 + 边距",也不会在不同机型上偏差。提供一个硬编码兜底值应对 query 在 layout 前执行的极端情况。

一句话外卖

scroll-view 上方每多一个元素,吸顶阈值就得重量一次——用 createSelectorQueryrect.bottom 取头部底边,永远比手算精确。