经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » 编程经验 » 查看文章
构建动态交互式H5导航栏:滑动高亮、吸顶和锚点导航技巧详解
来源:cnblogs  作者:一颗冰淇淋  时间:2024/4/15 9:48:59  对本文有异议

功能描述

产品要求在h5页面实现集锚点、吸顶及滑动高亮为一体的功能,如下图展示的一样。当页面滑动时,内容区域对应的选项卡高亮。当点击选项卡时,内容区域自动滑动到选项卡正下方。

布局设计

css 布局

为了更清晰的描述各功能实现的方式,将页面布局进行了如下的拆分。

★ 最外层的元素定义为 contentWrap,是使用 Intersection 定义的观察根元素
★ 所有可纵向滑动的元素包裹在 vertScrollWrap 中,也是粘性定位需要找到的父元素。
★ 横向可滑动的导航栏是 horiScrollWrap ,实现吸顶功能需要设置粘性定位。
★ observerWrap 用来包裹可观察的元素,observerItem 用来形容每一个可观察的子元素。

数据结构

导航栏的数据结构为数组,里面包括了选项卡需要显示的文案,对应的值,以及唯一值 key 。

  1. const list = {
  2. label: "选项卡一",
  3. value: "1",
  4. key: "1",
  5. height: 150, // 模拟使用,真实场景并不需要,数据会自动将盒子撑开
  6. }]

在我们真实的业务场景中,导航栏的标题来源于后端接口,内容区域也需要根据标题类型结合数据展示不同的内容,在获取接口数据后,我会为每一条数据增加一个随机的 key(非索引值,不会重复的8位哈希值) ,在选项卡内容区域增加自定义属性,如 data-tab-item-id,这样可以精准的获取到所需要的 dom 元素。

选项卡吸顶

按照这个场景,首先把选项卡横向滚动吸顶的功能实现。这里代码语法很简单,通过 position: sticky 就能实现,但需要注意的是,这里的 dom 元素布局很重要,父元素需要包裹滑动时无需展示的中间区域,以及选项卡、及里面的内容区域。

具体代码如下,这样就能实现向上滑动时,选项卡一整行固定在头部区域和内容区域之间。

  1. // 父元素
  2. .vertScrollWrap {
  3. position: relative;
  4. overflow: scroll;
  5. height: calc(100vh - 100px);
  6. }
  7. // 子元素
  8. .horiScrollWrap {
  9. position: sticky
  10. top: 0
  11. }

滑动导航高亮

当手指触摸页面滑动时,我们需要知道当前出现在可视区域的内容区域是哪些,传统方案可以通过绑定 scroll 方法,这里我使用的是 IntersectionObserver,通过观察元素与父元素的交叉状态,注意?? 这个api有一定的浏览器版本要求。

map 保存 dom 结构信息

在页面滑动时,需要知道每个内容区域距离父元素顶部的距离,找出距离顶部最近的元素,才能高亮对应的选项卡。当选项卡点击时,我们希望知道每个内容区域的高度,高度计算后,滚动整体到指定的高度,让选项卡对应的内容元素放在选项卡的最下方。

根据以上逻辑,需要每个内容模块的属性,这里我使用map来保存这些数据,key 为 dom 元素,value 值为对象,其中包含是否与父元素相交、距离顶部元素、元素高度等属性。

  1. // 初始化map
  2. domMap = new Map();
  3. // 设置map属性
  4. setDomMap = (dom, obj) => {
  5. const element = this.domMap.get(dom);
  6. const value = {
  7. key: element?.key,
  8. top: element?.top,
  9. height: element?.height,
  10. index: element?.index,
  11. isIntersecting: element?.isIntersecting,
  12. ...obj,
  13. };
  14. this.domMap.set(dom, value);
  15. };

IntersectionObserver 观察相交状态

使用 new IntersectionObserver(callback[, options]) 来定义观察逻辑。

初始化 domMap

在组件挂载时,初始化map数据,遍历所有的内容区域元素。

  1. const prefix = "nav";
  2. const blockId = `${prefix}-block-id`;
  3. // 每一个 observerItem 绑定 nav-block-id 的属性, 为了保存其 key 值
  4. const observerNodes = [
  5. ...contentWrap.querySelectorAll(`[${blockId}^="${prefix}-"]`),
  6. ];
  7. observerNodes.forEach((el, index) => {
  8. this.observer.observe(el);
  9. const attr = el.getAttribute(blockId);
  10. const key = attr?.split("-")?.[1];
  11. this.setDomMap(el, {
  12. isIntersecting: false,
  13. key,
  14. index,
  15. top: -1,
  16. height: -1,
  17. });
  18. });
callback 定义相交规则
  1. this.observer = new IntersectionObserver((entries) => {
  2. entries.forEach((entry) => {
  3. // 更新 isIntersecting 属性,是否相交
  4. this.setDomMap(entry.target, { isIntersecting: entry.isIntersecting });
  5. });
  6. // 遍历所有属性,更新距离顶部高度
  7. Array.from(this.domMap.keys()).forEach((dom) => {
  8. const rect = dom.getBoundingClientRect();
  9. this.setDomMap(dom, { top: rect.top, height: rect.height });
  10. });
  11. let min = 1000;
  12. let key = null;
  13. // 遍历domMap,根据每个dom元素存储的top值,找到距离父元素最近的一个dom元素
  14. for (const [, value] of this.domMap) {
  15. if (value.isIntersecting) {
  16. if (value.top < min) {
  17. min = value.top;
  18. key = value.key;
  19. }
  20. }
  21. }
  22. // 找到这个key值后,设置选项卡高亮,saveInfo.clickFlag 这里是判断当前操作是滑动还是手动点击了选项卡,如果手动点击选项卡后执行的滚动逻辑,则不再这里重复复制
  23. if (key && !saveInfo.clickFlag) {
  24. this.setActiveKey(key);
  25. }
  26. saveInfo.clickFlag = false;
  27. }, options);
options 中定义文档视口的属性
  1. const options = {
  2. root: contentWrap, // 监听元素的祖先DOM元素
  3. rootMargin: `-${marginTop}px 0px 0px 0px`, // 计算交叉值时添加至根的边界盒中的一组偏移量,marginTop 是头部区域+选项卡的高度
  4. threshold: 0, // 规定了一个监听目标与边界盒交叉区域的比例值
  5. };

设置选项卡高亮

设置选项卡高亮只需要通过 state 来绑定一个变量,这里需要注意两个逻辑??。

  1. 当需要高亮的选项卡不在当前可视区域内,需要将整个选项卡整体向左边滑动,露出高亮的选项卡。
  2. 当页面已经滑到底时,高亮的选项卡仍然可视区域内最靠近选项卡的那一个,比如下图的选项卡六。

判断选项卡是否在可视区域

首先是判断需高亮的选项卡是否在可视区域内,如果在可视区域内也就不需要再左滑了。

  1. isInViewport = (element) => {
  2. const rect = element.getBoundingClientRect();
  3. return (
  4. rect.top >= 0 &&
  5. rect.left >= 0 &&
  6. rect.bottom <=
  7. (window.innerHeight || document.documentElement.clientHeight) &&
  8. rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  9. );
  10. };
计算左滑的距离

可以通过即将高亮的选项卡dom元素来计算,如果每滑动一次都要进行dom计算会比较的耗费性能,更建议一开始就将每一个选项卡元素距离左边的x轴距离保存起来,在组件初始化的时候使用一个对象保存起来。

  1. calcTabsLeft() {
  2. this.tabsObj = {};
  3. // 为所有选项卡元素都绑定一个属性,格式为 data-tab-item-id={`${prefix}-${item.key}`}
  4. const tabs = document.querySelectorAll(`[data-tab-item-id]`);
  5. tabs.forEach((tab) => {
  6. const rect = tab.getBoundingClientRect();
  7. // 拆分出每个元素绑定在 dom 上的 key 值
  8. const key = tab.getAttribute("data-tab-item-id");
  9. this.tabsObj[key] = rect.x;
  10. });
  11. }
判断当前展示内容是否已滑动到底部
  1. canElementScrollDown = () => {
  2. // vertScrollWrap 是上图所标记出来的,滑动元素的父级
  3. return vertScrollWrap.scrollTop < vertScrollWrap.scrollHeight - vertScrollWrap.clientHeight;
  4. };
导航栏横向滑动

为每一个 horiScrollItem 定义了 data-tab-item-id 属性,用于记录其 key 值。

  1. navScroll() {
  2. const { activeKey } = this.state;
  3. // 可横向滚动选项卡父级
  4. const scrollTab = document.querySelector('[data-tab="tab"]');
  5. // 需滑动的选项卡元素
  6. const horiScrollItem = scrollTab?.querySelector(
  7. `[data-tab-item-id=${prefix}-${activeKey}]`
  8. );
  9. // 如果选项卡元素存在并且不在可视区域内,才滑动
  10. if (horiScrollItem && !this.isInViewport(horiScrollItem)) {
  11. const navDataId = `${prefix}-${activeKey}`;
  12. const elementX = this.tabsObj[navDataId] - 12;
  13. scrollTab.scrollTo(elementX, 0);
  14. }
  15. }

接着就可以定义高亮选项卡的方法

  1. setActiveKey = (key) => {
  2. // 如果已经滑动到底部,则不继续设置高亮选项卡
  3. if (!this.canElementScrollDown()) return;
  4. this.setState(
  5. {
  6. activeKey: key,
  7. },
  8. () => {
  9. // 判断选项卡是否在可视区域内,如果不是,则滑动到可视区域内
  10. this.navScroll();
  11. }
  12. );
  13. };

锚点跳转

在点击选项卡的时候,通过选项卡自定义属性上的 key 值找到对应内容区域的 dom 元素,再计算出它和父元素的距离,将对应的 vertScrollItem 滑动到可视区域即可。

这里需要注意??的是,锚点元素已经完全出现在可视区域或者已经滑到底部时,内容区域不会再向上滑动。比如下图中,点击选项卡七选项卡八展示的页面形式是一样的,因为他们对应的内容区域已经完全展示出来了。如果设计为向上滑动,则会页面底部很大一片空白。

计算内容区域与父级的距离

  1. getTop = (key) => {
  2. let scrollTop = 0;
  3. Array.from(this.domMap.keys()).forEach((dom) => {
  4. const domValue = this.domMap.get(dom);
  5. if (domValue.key === key) {
  6. scrollTop = dom.offsetTop;
  7. }
  8. });
  9. return scrollTop;
  10. };

点击锚点后滑动到可视区域

  1. onClickTabItem = (key) => {
  2. const vertScrollWrap = document.querySelector(".vertScrollWrap");
  3. // 导航栏高度 + 距离父元素高度
  4. const tabs = document.querySelector(".horiScrollWrap");
  5. const tabsHeight = tabs.getBoundingClientRect().height;
  6. const top = this.getTop(key) - tabsHeight;
  7. const observerItem = vertScrollWrap.querySelector(
  8. `[${blockId}="${prefix}-${key}"]`
  9. );
  10. if (observerItem) {
  11. // 将 clickFlag 定义为 true 时,不会在 intersectionObserver 处因为滑动导致不相交时而再次更新选项卡高亮的值
  12. saveInfo.clickFlag = true;
  13. const options = {
  14. left: 0,
  15. top,
  16. };
  17. vertScrollWrap.scroll(options);
  18. }
  19. this.setState({
  20. activeKey: key,
  21. });
  22. };

完整代码

以上便是滑动高亮+吸顶+锚点跳转的H5导航栏功能的分布解析,完整代码我放在了 github 上,戳 H5导航栏 anchor-sticky-nav 可查看,欢迎大家点个 star~

原文链接:https://www.cnblogs.com/vigourice/p/18134646

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号