支付宝原生小程序实现「滚动消失、停顿出现」底部订阅提示

一块钉在屏幕下方的订阅提示,滚动时悄悄藏起来,松手后再悄悄露出来——需求很短,但藏着的细节不少。

一、需求场景

在支付宝小程序里,有一块固定在屏幕下方的订阅提示区域,希望达到这样的交互:

  • 用户主动滑动页面时,提示区域隐藏
  • 用户停止滑动后,提示区域重新显示

效果如下图所示(动图演示滚动与停顿时的状态切换):
scroll-tip-demo-new

二、思路拆解

把需求翻译成技术语言,其实要做的事情就是两件:

  1. 监听页面滚动,滚动发生时把区域隐藏
  2. 监听滚动停止,停止后再把区域显示回来。

听起来很简单,但支付宝小程序(其实是各大小程序都一样)并没有原生提供"滚动结束"事件。翻官方文档,唯一与滚动相关的 API 是:

事件名 触发时机 回调参数
onPageScroll 页面滚动时触发 scrollHeight(页面总高度)、scrollTop(滚动距离顶部高度)

官方文档示意图:

image

📌 这个回调只告诉你"现在滚到哪里了",并不会告诉你"用户什么时候停下来"

所以核心问题变成了:怎么自己识别"用户停手了"?

业界主流方案有两种:

  • 节流(throttle):滚动过程中反复触发,但保证每隔 N ms 至少执行一次。
  • 防抖(debounce):等用户停止操作 N ms 后才触发一次。

我们要的恰好是防抖的语义——"停下来再显示"。所以基本思路就清晰了:

每次滚动先把区域藏起来,并清掉之前的定时器、开一个新的定时器;定时器在 N ms 后执行"显示"动作。因为只有真正停下来时定时器才不会被清掉,所以定时器触发的那一刻,就是用户停手的时刻。

三、代码实现

下面把 axml、js 一次给全,方便直接照着抄。

3.1 模板(axml)

只需要用 a:if 把这块区域包起来,控制显示与隐藏即可:

<!-- pages/demo/index.axml -->
<view class="page">
  <!-- 页面正文 -->
  <view class="content">...</view>

  <!-- 底部订阅提示区域 -->
  <view class="subscribe-remind" a:if="{{subscribeRemindVisible}}">
    <text>打开通知,第一时间收到新内容</text>
  </view>
</view>

对应的 acss(可选):

.subscribe-remind {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 20rpx 32rpx;
  background: #ffffff;
  box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.06);
}

3.2 原作者的原始实现(js)

data 里准备三个状态:

Page({
  data: {
    subscribeRemindVisible: true,  // 是否展示底部区域(布尔值)
    isScrolling: false,            // 标记是否处于滚动中
    timer: null,                   // 定时器句柄
  },

  onPageScroll() {
    // 正在滚动 → 立刻隐藏区域,并把"滚动中"标记置为 true
    this.setData({
      subscribeRemindVisible: false,
      isScrolling: true,
    });

    // 清掉上一轮可能还没执行的定时器
    if (this.data.timer) {
      clearInterval(this.data.timer);
    }

    // 起一个 100ms 的轮询定时器
    this.data.timer = setInterval(() => {
      // 第一次执行(100ms):把标记清回 false
      // 第二次执行(200ms):若标记依然为 false,说明这段时间没有新的 onPageScroll,
      //                    即用户真的停了 → 显示区域
      if (!this.data.isScrolling) {
        clearInterval(this.data.timer);
        this.setData({ subscribeRemindVisible: true });
      }
      // 每次 tick 末尾都重置标记,给下一次比较用
      this.setData({ isScrolling: false });
    }, 100);
  },
});

这段代码的核心思路就是"两段式轮询"

  • 第 100ms 那个 tick:把 isScrolling 置成 false
  • 第 200ms 那个 tick:如果 isScrolling 还是 false(中间没有新事件进来),就显示区域。

只有真正停下来时,定时器才能跨过这两轮、把区域显示出来。

⚠️ 这版写法能 work,但有两个不优雅的地方:

  1. 每 100ms 都执行一次 setData({ isScrolling: false }),会反复触发脏检查和渲染,开销偏大;
  2. isScrolling 其实只是个内部标记,没必要塞进 data,挂在 this 上就好。

下面给一版等价的、更干净的实现,推荐直接用这版。

3.3 推荐实现(更干净的等价写法)

Page({
  data: {
    subscribeRemindVisible: true,
  },

  onPageScroll() {
    // 滚动中:先清掉旧定时器,把区域藏起来
    if (this._tipTimer) clearTimeout(this._tipTimer);

    this.setData({ subscribeRemindVisible: false });

    // 等用户停手 200ms 后再把区域展开
    // 注意:句柄挂在 this 上即可,不要写进 data,避免多余渲染
    this._tipTimer = setTimeout(() => {
      this.setData({ subscribeRemindVisible: true });
      this._tipTimer = null;
    }, 200);
  },
});

思路完全一样——"滚动 → 清定时器 + 隐藏 → 重新起一个定时器,停 N ms 后再显示"——但好处很明显:

  • 只用了一个 setTimeout,没有 isScrolling 中间态,可读性更高;
  • 定时器句柄挂在 this._tipTimer,不进 data,不会触发额外渲染
  • 状态变更只有"显示 / 隐藏"两种,对外接口更稳定。

四、几个容易踩的坑

  1. onPageScroll 触发频率非常高。实测滚动一次屏幕会触发几十到上百次,千万不要在里面做重活(请求、复杂计算)。本文的方案里只做 clearTimeout + setData 一个布尔值,是安全的。
  2. 别用 setInterval,用 setTimeoutsetInterval 一旦开启就会一直执行,需要手动清除;setTimeout 触发一次就结束,跟"停手再显示"的语义更吻合。
  3. 停顿时间 100~200ms 比较合适。太短会把"滑动中短暂的停顿"误判为停止;太长会让用户觉得"卡"。
  4. data 里别存太多中间态isScrolling 只是个标记,没必要存进 data(不进 data 就不会触发脏检查,渲染更轻)。上面"推荐实现"里就直接挂在 this 上。

五、小结

整个需求拆开来看就三步:

  • onPageScroll 触发即隐藏 —— 监听滚动;
  • 滚动时清掉旧的 setTimeout,起一个新的 —— 防抖;
  • 新定时器到点了再显示 —— 识别停手。

代码总共不到 20 行,关键是想清楚"什么时刻才算停手"——它就是"定时器能跑完"的那一刻。所有没有提供 scrollEnd 事件的小程序/Web 容器,都可以用这个套路解决。


如果本文对你有帮助,欢迎点赞 / 收藏 / 评论三连支持一下~有更好的实现方案也欢迎留言交流 🙌

posted @ 2026-07-08 11:08  拒绝敲代码的程序猿  阅读(6)  评论(0)    收藏  举报