当页面比较长内容较多的时候,会使用导航栏,给用户提供方便跳转到页面某一模块的功能。由于导航栏需要监听页面的滚动事件,在小程序中,很容易出现性能问题,需要时刻注意滚动监听中 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 才能执行
导航栏组件实现问题及解决思路
导航栏组件的实现,大致需要如下准备工作:
-
获取锚点的信息,组成导航栏按钮文案;
-
获取锚点的位置信息,以便点击导航滚动到对应位置;
此外,还需要两个特性:
-
点击导航栏,让页面滚动到对应位置;
-
当页面滚动时,导航栏对应锚点的按钮需要改变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 状态。
组件中,主要是模块动态加载完成这个时机比较难捕捉到,这里利用加载完的事件触发导航更新,这种方式的优化方案还待思考讨论。如果大家有好的建议也欢迎留言讨论~
参考资料
今天的文章小程序滚动条操作及导航组件实现分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/19798.html