用户
 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,登录网站

小程序社区 首页 教程 实战教程 查看内容

小程序滚动条操作及导航组件实现

Rolan 2019-8-9 00:27

当页面比较长内容较多的时候,会使用导航栏,给用户提供方便跳转到页面某一模块的功能。由于导航栏需要监听页面的滚动事件,在小程序中,很容易出现性能问题,需要时刻注意滚动监听中 setData 的次数。本文将介绍页 ...

当页面比较长内容较多的时候,会使用导航栏,给用户提供方便跳转到页面某一模块的功能。由于导航栏需要监听页面的滚动事件,在小程序中,很容易出现性能问题,需要时刻注意滚动监听中 setData 的次数。

本文将介绍页面滚动条操作相关的微信 API,并利用这些 API 实现一个通用的导航栏组件。

导航组件效果如下图:


滚动到页面目标位置的相关API

实现滚动到页面目标位置的功能,需要“滚动操作”和”目标位置“。


将页面滚动到目标位置(wx.pageScrollTo)

将页面滚动到页面中的目标位置,可以使用 wx.pageScrollTo 这个微信提供的 API。该方法可以接收一个对象作为参数,对象中可以指定:

  • 滚动的目标位置 scrollTop,单位为 px;

  • 滚动动画的时长 duration,单位为 ms;

  • 选择器 selector,但支持的基础库版本是从2.7.3;

其实,直接使用选择器可以方便地完成我们想要的效果,但遗憾的是,我们的小程序大约只有60%用户的基础库是2.7.3以上。如果只有这些用户享受到新功能,用户量稍微少了些,不过我们可以使用 scrollTop 直接指定目标位置。


获取元素在页面中的位置(SelectorQuery)

首先,需要创建节点查询对象 selectorQuery,创建方法如下:

wx.createSelectorQuery()		// 返回selectorQuery对象
复制代码

selectorQuery 对象可以利用选择器选择匹配的节点,使用 selectAll 方法:

wx.createSelectorQuery().selectAll('.nav-target') // 返回 NodesRef复制代码

NodesRef 可以使用 fields 方法获取到节点的信息,比如大小、dataset等,使用 boundingClientRect 可以获取节点的位置信息,如上边界坐标等,最后调用 exec 方法才能执行:

wx.createSelectorQuery().selectAll('.nav-target').fields({
    dataset: true,	    // 指定返回节点 dataset 的信息
    size: true,		    // 指定返回节点大小信息
}, rects => {
    rects.forEach(rect => {
        rect.dataset;
        rect.width;
        rect.height;
    })
}).boundingClientRect(rects => {
    rects.forEach(rect => {
	rect.dataset;
        rect.top;
  })
}).exec()		// 最后要加 exec 才能执行
复制代码


导航栏组件实现问题及解决思路

导航栏组件的实现,大致需要如下准备工作:

  1. 获取锚点的信息,组成导航栏按钮文案;

  2. 获取锚点的位置信息,以便点击导航滚动到对应位置;

此外,还需要两个特性:

  1. 点击导航栏,让页面滚动到对应位置;

  2. 当页面滚动时,导航栏对应锚点的按钮需要改变active状态;


准备工作1:获取锚点信息

我们可以约定,所有锚点都需要加上:

  • class: nav-target;

  • data-label:导航栏中显示的文本;

  • data-key:作为锚点标识;

所以,一个锚点元素可能会编写成如下形式:

<view class="nav-target" data-key="overview" data-label="概览">...</view>复制代码

有了class,我们就可以利用前文提到的selectorQuery取到这些锚点,进而利用boundingClientRect方法取到锚点上的dataset,关键代码如下:

wx.createSelectorQuery().selectAll('.nav-target').boundingClientRect(res => {
	this.setData({
  	navList: res.map(item => item.dataset).filter(Boolean)
  })
})
复制代码

取到了锚点信息后,存入navList,其中的label作为导航栏的按钮文案,而key则用于接下来存储锚点位置。


准备工作2:获取锚点的位置信息

锚点的位置信息,也可以通过boundingClientRect获取,取到位置信息后,存入一个Map中,我们命名为positionMap,结合上面获取锚点信息,_getAllAnchorInfoAndScroll方法代码如下:

_getAllAnchorInfoAndScroll(selectorIdToScroll) {
  wx.createSelectorQuery().selectAll('.nav-target').boundingClientRect(res => {
    if (!res || res.length === 0) return

    this.setData({
      navList: res.map(item => item.dataset).filter(Boolean)
    })
    
    // 为了减少setData传输数据量,我们将视图层不需要用到的position信息存在Page实例上
    res.forEach(item => {
      const { top, dataset: { key} } = item
      if (top >= 0) {
        this.positionMap[key] = Math.max(top - 55, 0)	// 向上留55px的空间给导航栏
      }
    })

    // 如果需要做滚动的操作,则在这里执行
    if (selectorIdToScroll) {
      wx.pageScrollTo({ scrollTop: this.positionMap[selectorIdToScroll] })
    }
  }).exec()
}
复制代码


模块动态加载

由于需要加导航的页面长度都比较长,我们通常会对非首屏的模块使用动态加载技术。而页面模块的动态加载意味着,导航组件获取锚点位置的时机不能简单地设置在组件的 ready 事件。

很显然,获取锚点位置的时机应该设置在所有模块都加载完成的时候。我们可以在模块(组件)加载完成后,通知导航组件进行锚点信息的更新。

关键代码大致如下:

页面 page.wxml

<!-- 导航组件 -->
<nav id="nav" />

<!-- 页面模块组件 -->
<page-module bindupdate="updateNavList" />
复制代码

页面 page.js

Page({
  updateNavList() {
     this.getNavComponent().updateNavInfo()
  },
  getNavComponent() {
    // 避免多次调用 selectComponent,将其结果存入变量 _navComponent
    if (!this._navComponent) {
      this._navComponent = this.selectComponent('#nav')
    }
    return this._navComponent
  },
})
复制代码

模块组件 pageModule.js

// 模块组件中,加载完成时触发页面实例的 updateNavList 方法
this.triggerEvent('update')
复制代码

导航组件 nav.js

Component({
  methods: {
    ...,
    updateNavInfo() {
      this._getAllAnchorInfoAndScroll()
    }
  }
})
复制代码


如此一来,页面模块更新后,导航组件也会更新锚点信息和位置,保证导航组件的信息是最新的。最后需要注意如果有懒加载的图片,需要提前设定好高度,否则等图片加载完锚点信息就错乱了。当然,也可以在图片加载完成的方法中,调用更新导航信息的 updateNavList 方法,这部分与模块组件的加载触发思路一致本文就不赘述。


特性1:点击导航按钮,页面滚动到对应位置

有了前面两项准备工作,这个特性实现起来,就简单多了。导航栏的按钮有可能一行放不下,应该使用 scroll-view 标签支持滚动。wxml 代码如下:

导航组件 nav.wxml

<scroll-view scroll-x>
  <view class="scroll-inner" bindtap="bindClickNav">
    <view class="nav {{index === currentIndex ? 'nav--active' : ''}}"
          wx:for="{{navList}}" wx:key="{{index}}" data-key="{{item.key}}" data-index="{{index}}">{{item.label}}</view>
  </view>
</scroll-view>复制代码

其中,currentIndex 记录当前选中的导航项;bindClickNav 则处理点击导航项的更新 currentIndex 和页面滚动逻辑。

导航组件 nav.js

bindClickNav(e) {
  const { index, key } = e.target.dataset
  this.setData({ currentIndex: index })
  if (this.data.positionMap[selectorId] === undefined) {
    // 如果点击时,锚点位置还未取得,则需要先获取位置并传入key,在获取位置之后滚动
    this._getAllAnchorInfoAndScroll(key)
    return
  }
  wx.pageScrollTo({ scrollTop: this.positionMap[selectorId] })
},
复制代码


特性2:导航栏按钮的状态支持随着页面滚动而改变

页面滚动的监听函数是 onPageScroll,我们需要在其中判断页面滚动到哪个锚点。

判断滚动到哪个锚点的具体逻辑是在导航组件中的 watchScroll 实现,页面实例中的 onPageScroll 则传递页面滚动位置给导航组件 watchScroll 方法。

页面实例 page.js

Page({
  onPageScroll({ scrollTop }) {
    const navComponent = () => {
      if (!this._navComponent) {
        this._navComponent = this.selectComponent('#nav')
      }
      return this._navComponent
    }
    navComponent && navComponent.watchScroll(scrollTop)
  }
})
复制代码


在导航组件中,应该如何判断页面滚动的位置与锚点的关系呢?

以下图为例,页面滚动超过了”模块1“与”模块2“的锚点,但未超过”模块3“的锚点,此时导航栏显示的”模块2“应该是 active 态:

总结一下实现思路:按照从上到下的顺序遍历各个模块,并将各个模块的锚点位置与页面的 scrollTop 进行对比,找到最后一个小于 scrollTop 的锚点模块,该模块的状态即为 active。

由于“最后一个小于”比较难找,我们可以转换成找“第一个大于”的模块,该模块的上一个模块即为 active 态的模块。关键代码如下:

导航组件 nav.js

Component({	
  ...,
  methods: {
	  ...,
    watchScroll(pageScrollTop) {

      // 判断是否为空,即初始化尚未完成
      if (isEmpty(this.positionMap)) {
        return
      }

      // 当页面滚动时,停止更新navIndex
      if (_navIndexLock) {
        return
      }

      // 判断滚动的scrolltop,然后设置 currentIndex
      const lastIndex = this.data.navList.length - 1
      for (let idx = 0; idx <= lastIndex; idx++) {
        const navItem = this.data.navList[idx]
        const top = this.positionMap[navItem.key]
        const indexToSet = idx === 0 ? idx : idx - 1

        // 寻找“第一个大于scrollTop”的模块,其上一个模块即为 active 态的模块
        if (top > pageScrollTop) {
          this.data.currentIndex !== indexToSet && this.setData({ currentIndex: indexToSet })
          break
        }

        // 到最后一个tab还没有break,说明已经滚动到了最后tab
        if (idx === lastIndex) {
          this.data.currentIndex !== lastIndex && this.setData({ currentIndex: lastIndex })
        }
      }
    }
	}
})
复制代码


总结

本文介绍了微信小程序对页面滚动和元素操作的支持情况,利用这些特性实现了一个导航组件。这个导航组件支持动态加载的模块,并能够根据页面滚动的位置更新导航组件的 active 状态。

组件中,主要是模块动态加载完成这个时机比较难捕捉到,这里利用加载完的事件触发导航更新,这种方式的优化方案还待思考讨论。如果大家有好的建议也欢迎留言讨论~


参考资料

官方文档:developers.weixin.qq.com/miniprogram…

鲜花
鲜花
鸡蛋
鸡蛋
分享至 : QQ空间
收藏
原作者: w_西城 来自: 掘金