问题现象

这个问题是最近在优化小程序代码时发现的。

在ios环境下,微信小程序的scrollview组件包裹着一个position:fixed的view。

当在scrollview组件上滑动时,这个view会疯狂抖动。

直接上图吧:

下面是简化后的代码:

<view class="main">
  <scroll-view scroll-y="{{true}}" bindscroll="handleScroll" style="height:100%;" >
    <view>
        <view class="weui-navbar navbar-fixed">
          我是头部fixed元素
        </view>
        <view>
          这里是一大段test文字,用于占位
        </view>
    </view>
  </scroll-view>
</view>

猜测与验证

原生组件?

这个现象在没有简化代码前,我以为我是哪里用了什么原生组件。

因为原生组件在ios下的定位缓慢,导致了这个问题的出现。

但是当我的代码简化到上面这一步的时候,发现并没有应用原生组件。

ios下橡皮筋功能的影响?

问题在于我去掉了scroll-view后,滚动得不错,这个头部fixed的元素并没有抖动。

确定是scroll-view组件下fixed元素随着滑动就会抖动

到这一步我就确定了问题的原因,所以当然我是要先百度一下答案的。

于是我果然发现了一堆难兄难弟:ios下scroll-view中fixed元素无法固定

貌似他们遇到的问题比我还严重啊,都像一条咸鱼一样跟着滚了,而我的只是得了帕金森。

简单场景解决方案

上面的问题还没有官方人员回答。

不过最好的解决方案其实就是将fixed元素移出scroll-view,这个没什么好多说的。

元素都fixed了,没道理还要放在scroll-view中是吧?

复杂场景解决方案

既然说了上面是简单场景,那么就肯定有复杂场景嘛。

我元素都fixed了,确实是没道理要放在一个scroll-view元素中包裹着。

但是有的事就是这么没道理啊。

就比如我的微信小程序肯定没有示例这么简单,里面这个fixed元素不能移出去。

因为这个元素的fixed状态并不是固定的,最开始他需要跟随页面一起滚动,当和顶部贴紧后,它就变成fixed了。

废话少说,现在就说一下我的解决方案的思路:

既然要随着页面一起滚动,那么肯定是要保证这个元素在scroll-view中的。

而scroll-view中的fixed元素肯定会抖,所以这个元素又一定要放在scroll-view外。

看似鱼与熊掌不可兼得,实际上我们搞两个人一人取鱼,一人取熊掌就好了嘛。

我们可以在scroll-view外设置一个同样的元素,并将其设置为fixed,并且隐藏。

当scroll-view内部的元素贴紧顶部后,将内部的元素隐藏,再显示外部的元素即可。

以下是实现代码:

index.wxml:

<view class="main">
  <view class="navbar navbar-fixed" hidden="{{scrollTop<=initTop}}">
    我是头部fixed元素
  </view>
  <scroll-view scroll-y="{{true}}" bindscroll="handleScroll" style="height:100%;" >
    <view>
        <view>
          这里是一大段test文字,用于占位
        </view>
        <view id="navbar" class="weui-navbar navbar-fixed" hidden="{{scrollTop>initTop}}">
          我是头部fixed元素
        </view>
        <view>
          这里是一大段test文字,用于占位
        </view>
    </view>
  </scroll-view>
</view>

index.js:

Page({
  data: {
    initTop: 0,
    scrollTop: 0,
  },
  onLoad: function (options) {
    let query = wx.createSelectorQuery()
    query.select('#navbar').boundingClientRect()
    query.exec((res) => {
      this.setData({
        initTop: res[0].top
      })
    })
  },
  handleScroll: function (e) {
    this.setData({ scrollTop: e.detail.scrollTop })
  }
})

index.wxss:

.navbar-fixed {
  position:fixed;
  width:100%;
  top:0;
  left:0;
  z-index:100;
}
.navbar{
  height:80rpx;
  line-height: 80rpx;
  background:red;
  text-align: center;
  color: #fff;
}

隐藏BUG与修复

以上代码如果快速滑动是没有问题,但是当红色头部元素快要贴紧顶部时慢速滑动就会出现一个很诡异的现象:

红色头部元素往下弹动,始终不能贴紧顶部。

而实际上不是红色头部元素往下弹动,而是红色头部元素贴紧顶部后,此时内部头部元素隐藏,那么scrollTop立刻变小。

因为scrollTop变小,小于了initTop,那么内部头部元素再次出现,于是就这样不断循环。

我们这里需要明白hidden实际上是一个display:none的效果,所以这里我们对内部元素的隐藏不能用hidden,而是用visibility:hidden。

这样的话,这个内部元素就只是看不见了而已,并且页面上显示为背景色(这里我们假设是白色),但是还是占用了那么多的空间。

那么scrollTop就不会突然间变小,也就不会造成BUG。

同时,外部的元素会在内部元素变成白色矩形时直接出现,覆盖在内部元素上面,那么内部元素隐藏所造成的白色区域实际上就被外部元素遮挡住了。

当用户在使用时,完全不会感知到内部元素这个白色区域的存在。

好了,这里我们给出修改后的代码:

<view class="main">
  <view class="navbar navbar-fixed" hidden="{{scrollTop<=initTop}}">
    我是头部fixed元素
  </view>
  <scroll-view scroll-y="{{true}}" bindscroll="handleScroll" style="height:100%;" >
    <view>
        <view>
          这里是一大段test文字,用于占位
        </view>
        <view id="navbar" class="weui-navbar navbar-fixed" style="visibility:{{scrollTop>initTop?'hidden':'initial'}}">
          我是头部fixed元素
        </view>
        <view>
          这里是一大段test文字,用于占位
        </view>
    </view>
  </scroll-view>
</view>

总结

方法蠢是蠢了点,但是好用啊。

而且万一哪天微信小程序修复了这个问题,咱们的方案不会出问题,替换起来也很简单。

07-30 16:18