功能描述

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

布局设计

css 布局

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

★ 最外层的元素定义为 contentWrap,是使用 Intersection 定义的观察根元素

★ 所有可纵向滑动的元素包裹在 vertScrollWrap 中,也是粘性定位需要找到的父元素。

★ 横向可滑动的导航栏是 horiScrollWrap ,实现吸顶功能需要设置粘性定位。

★ observerWrap 用来包裹可观察的元素,observerItem 用来形容每一个可观察的子元素。

数据结构

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

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

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

选项卡吸顶

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

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

// 父元素
.vertScrollWrap {
position: relative;
overflow: scroll;
height: calc(100vh - 100px);
} // 子元素
.horiScrollWrap {
position: sticky
top: 0
}

滑动导航高亮

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

map 保存 dom 结构信息

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

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

// 初始化map
domMap = new Map(); // 设置map属性
setDomMap = (dom, obj) => {
const element = this.domMap.get(dom);
const value = {
key: element?.key,
top: element?.top,
height: element?.height,
index: element?.index,
isIntersecting: element?.isIntersecting,
...obj,
};
this.domMap.set(dom, value);
};

IntersectionObserver 观察相交状态

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

初始化 domMap

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

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

设置选项卡高亮

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

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

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

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

isInViewport = (element) => {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
计算左滑的距离

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

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

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

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

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

setActiveKey = (key) => {
// 如果已经滑动到底部,则不继续设置高亮选项卡
if (!this.canElementScrollDown()) return;
this.setState(
{
activeKey: key,
},
() => {
// 判断选项卡是否在可视区域内,如果不是,则滑动到可视区域内
this.navScroll();
}
);
};

锚点跳转

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

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

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

getTop = (key) => {
let scrollTop = 0;
Array.from(this.domMap.keys()).forEach((dom) => {
const domValue = this.domMap.get(dom);
if (domValue.key === key) {
scrollTop = dom.offsetTop;
}
});
return scrollTop;
};

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

 onClickTabItem = (key) => {
const vertScrollWrap = document.querySelector(".vertScrollWrap");
// 导航栏高度 + 距离父元素高度
const tabs = document.querySelector(".horiScrollWrap");
const tabsHeight = tabs.getBoundingClientRect().height;
const top = this.getTop(key) - tabsHeight; const observerItem = vertScrollWrap.querySelector(
`[${blockId}="${prefix}-${key}"]`
);
if (observerItem) {
// 将 clickFlag 定义为 true 时,不会在 intersectionObserver 处因为滑动导致不相交时而再次更新选项卡高亮的值
saveInfo.clickFlag = true;
const options = {
left: 0,
top,
};
vertScrollWrap.scroll(options);
} this.setState({
activeKey: key,
});
};

完整代码

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

构建动态交互式H5导航栏:滑动高亮、吸顶和锚点导航技巧详解的更多相关文章

  1. jquery 实现导航栏滑动效果

    精简的代码实现导航栏滑动效果,实现详解: 1.滑块位置:通过父节点position=fixed,子节点position=absolute方式,实现子节点浮动: 2.导航栏居中:通过left=0px,r ...

  2. 原生JS实现全屏切换以及导航栏滑动隐藏及显示——重构前

    思路分析: 向后滚动鼠标滚轮,页面向下全屏切换:向前滚动滚轮,页面向上全屏切换.切换过程为动画效果. 第一屏时,导航栏固定在页面顶部,切换到第二屏时,导航条向左滑动隐藏.切换回第一屏时,导航栏向右滑动 ...

  3. 微信小程序导航栏,下面内容滑动,上册导航栏跟着滑动,内容随着导航栏滑动

    16.类似微信导航栏滑动.png 今日头条导航栏,下面滑动上面跟着滑动 index.wxml <swiper class="content" style="heig ...

  4. vue滑动吸顶以及锚点定位

    Vue项目中需要实现滑动吸顶以及锚点定位功能.template代码如下: <template> <div class="main"> <div id= ...

  5. 炫酷:一句代码实现标题栏、导航栏滑动隐藏。ByeBurger库的使用和实现

    本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发. 其实上周五的时候已经发过一篇文章.基本实现了底部导航栏隐藏的效果.但是使用起来可能不是很实用.因为之前我实现的方式是继承了系统的 ...

  6. vue-router+elelment-ui,实现导航栏激活高亮

    <el-menu :default-active="$route.path" class="el-menu-vertical-demo" backgrou ...

  7. iOS 滑动隐藏导航栏-三种方式

    /** 1隐藏导航栏-简单- */    self.navigationController.hidesBarsOnSwipe = YES; /** 2隐藏导航栏-不随tableView滑动消失效果 ...

  8. Android 动态隐藏显示导航栏,状态栏

    Talk is cheap, show me the code. --Linus Torvalds Okay, here: 一.导航栏: [java] view plain copy private  ...

  9. iOS开发笔记13:顶部标签式导航栏及下拉分类菜单

    当内容及分类较多时,往往采用顶部标签式导航栏,例如网易新闻客户端的顶部分类导航,最近刚好有这样的应用场景,参考网络上一些demo,实现了这种导航效果,记录一些要点. 效果图(由于视频转GIF掉帧,滑动 ...

  10. 记一次Vue跨导航栏问题解决方案

    简述 这篇文章是我项目中,遇到的一个issue,我将解决过程和方法记录下来. 本篇文章基于Vue.js进行的前端页面构建,由于仅涉及前端,将不做数据来源及其他部分的叙述.使用的CSS框架是 Boots ...

随机推荐

  1. Kotlin扩展函数与属性原理解析

    一.扩展函数 扩展函数可以方便地给现有类增加属性和方法而不改动类地代码. 二.原理 fun String.addTo(s: String): String{ return this + s } 反编译 ...

  2. 【项目实战】SpringBoot+vue+iview打造一个极简个人博客系统

    基于SpringBoot+vue+iview个人极简博客 项目介绍 个人极简博客 [个人极简博客]是一个适用于初学者学习的博客系统,其中包含文章分类.写文章.标签管理.用户管理等基础功能,代码简洁注释 ...

  3. 16. Class字节码结构

    1. 相关概念 1.1字节码文件的跨平台性 Java 语言是跨平台的(write once, run anywhere) 当 Java 源代码成功编译成字节码后,如果想在不同的平台上面运行, 则无须再 ...

  4. C#拾遗补漏之goto跳转语句

    前言 在我们日常工作中常用的C#跳转语句有break.continue.return,但是还有一个C#跳转语句很多同学可能都比较的陌生就是goto,今天大姚带大家一起来认识一下goto语句及其它的优缺 ...

  5. Toyota Programming Contest 2024#2(AtCoder Beginner Contest 341)D - Only one of two(数论、二分)

    目录 链接 题面 题意 题解 代码 总结 链接 D - Only one of two 题面 题意 求第\(k\)个只能被\(N\)或\(M\)整除的数 题解 \([1,x]\)中的能被\(n\)整除 ...

  6. TypeScript实践总结

    下文将TypeScript简称ts 一.为什么要学 1.1 减少bug,提高质量 强语言,语法等方面异常,编译阶段"提前"报错 支持面向对象,软件设计与工程化更为成熟,更容易做单元 ...

  7. DOSBox0.74使用Debug时p命令报错

    环境 操作系统:Windows 10 DOSBox 0.74 DEBUG.EXE 从 Windows XP 或其他复制到的DOSBox下 问题 在学习到 王爽的<汇编语言>时,第 4章,第 ...

  8. Java中使用JSON传递字符串的注意事项

    一.问题由来 项目开发中,由于实际需要将某一个功能模块抽取成了一个单独的服务,其他地方需要调用的时候,通过Spring提供的RestTemplate类发送请求进行调用. 经过测试这种方法完全可行,我和 ...

  9. $event - vue中默认参数的显示 - @on-change="func($event, code)" - 基础知识

    @on-change="checkAllOnChangeHandle($event,scItem.code)"

  10. 后端基础PHP-PHP简介及基本函数

    后端基础PHP-PHP简介及基本函数 1.PHP简单介绍 2.PHP基本语法 一.PHP简单介绍 PHP(超文本预处理器),是一种通用的开源脚本语言,标准的后端语言 比较常见的后端语言,ASP|ASP ...