悬浮滚动特效
需求
最近项目经理提到一个这样的交互需求:长页面内容,中间有一块部分是分为左右布局,左侧是目录菜单,右侧是内容,右侧内容部分很长。
- 需求1:左侧菜单锚点定位,点击,右侧滑动到对应菜单的内容;
- 需求2:右侧内容滑动到指定位置,左侧菜单锚点定位到对应菜单;
- 需求3:当向下滑动时,左右侧同时滑动,当左侧菜单滑动到视口顶部,右侧内容还没有结束,左侧菜单要固定再顶部;当向上滑动时,左右侧同时滑动,当左侧菜单滑动到视口底部,右侧内容还没有结束,左侧菜单要固定到底部;直到右侧滑动结束,左右一起划出
原理
- 初始化记录固定元素位置,目标盒子的高度
- 监听页面滚动
- 动态获取元素位置,开启计算,如果目标盒子最顶部滑出视口或者小于设定值(距离视口顶部的值),开启菜单位置固定;如果目标盒子底部出现在视口底部,继续向上滑动,则左侧菜单要根据视口高度+初始固定值。
- 滑动时激活对应锚点
实现
以vue3为例
- html部分
<div ref="stickyContainerRef" >
<!-- 左侧菜单 -->
<div ref="sytckyMenuRef" class="menu">
<ul>
<li v-for="(item,index) in menuList" :class="[{active:activeTag===item.id}]" :key="index" @click="handleMenuClick(item)">{{item.name}}</li>
</ul>
</div>
<!-- 右侧内容 -->
<div class="content">
<div class="content-inner">
<div class="content-inner-inner">{{content}}</div>
</div>
</div>
</div>
- js部分
const stickyContainerRef=ref() // 外层盒子实例化
const stickyContainerRect=ref({
top:0,
left:0,
height:0,
width:0
})
const stickyMenuRef=ref() // 左侧菜单实例化
const stickyMenuRect=ref({
top:0,
left:0
})
const stickyMenuLeft=ref(0) // 左侧菜单距离左侧距离
const containerTop=ref(0) // 外层盒子top
const containerHeight=ref(0) // 外层盒子高度
//
const webHeaderHeight=ref(0) // 顶部导航高度(即距离顶部多少开始固定)
onMounted(()=>{
// 初始化记录值
containerHeight.value=getDomPosition(stickyContainerRef.value).height;
containerTop.value=getDomPosition(stickyContainerRef.value).top;
stickyMenuLeft.value=getDomPosition(stickyMenuRef.value).left;
// 1. 监听滚动
document.addEventListener('scroll',handleScroll)
})
// 滚动方法
const handleScroll=()=>{
stickyContainerRect.value=getDomPosition(stickyContainerRef.value)
scrollCompute()
handleMenuActive()
}
// 滚动计算
const scrollCompute=()=>{
// 1. 计算目标盒子外壳顶部距离视口最下方的高度差
let diffHeight=stickyContainerRect.value.height-Math.abs(stickyContainerRect.value.top)-webHeaderHeight.value;
if(stickyContainerRect.value.top<35){
stickyMenuRef.value.style.position='fixed';
stickyMenuRef.value.style.left=stickyMenuLeft.value+'px';
// 高度差<0:说明盒子整体底部已经离开视口底部 ;
// 高度差>=0:说明盒子整体底部在视口顶部下面,
if(diffHeight<0){
// 盒子整体底部已经离开视口底部 下次进来左侧菜单需要从底部开始展现,所以需要计算整理盒子距离底部位置来动态计算菜单盒子位置
stickyMenuRef.value.style.top=webHeaderHeight.value+diffHeight+'px';
}else{
sticklyMenuRef.value.style.top=webHeaderHeight.value+'px';
}
}else{
clearStyleSite(stickyMenuRef.value)
}
}
// 清楚动态位置
const clearStyleSite=(dom:HTMLElement)=>{
dom.style.position='static';
dom.style.top='0px';
dom.style.left='0px';
}
// 获取dom元素位置值
function getDomPosition(dom:HTMLElement){
return dom.getBoundingClientRect()
}
// 处理锚点激活
function handleMenuActive(marginTop:number=0){
const className='card-box'
for(let dom of document.getElementsByClassName(className)){
if(getDomPosition(dom).top>0&&getDomPosition(dom).top-marginTop<=0){
activeMenu()
}
}
}
// 锚点高亮激活
const activeMenu=debounce((dom)=>{
activeTag.value=dom.getAttribute('data-id')
,200})
// 点击锚点滚动
const handleMenuClick=(item)=>{
document.querySelector(`[data-id="${item.id}"]`).scrollIntoView({behavior:'smooth',block:'start'})
}