scrollIntoView: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollIntoView
背景
笔者想要实现一个页面,该页面包括如下功能:
- 顶部Tab
-需要置顶;
-超出则左右可滑动;
-点击时将选中Tab高亮,且自动居中 - 内容滚动区域
-需和顶部Tab联动,即点击Tab,内容滚动至该Tab对应的锚点处;相应的,滚动内容,如到达该锚点时,对应的Tab也需要切换为高亮选中状态
模块、功能拆分:
- 页面容器内容,分为两个模块:
tab等置顶模块;
滚动区域模块 - tab模块实现切换高亮和选中时滚动居中功能
- 内容滚动区域,实现与顶部tab的联动
功能实现设计:
- 顶部Tab置顶,内容滚动区域选择
方案一: 严格区分置顶区域和滚动区域,使用overflow:hidden 固定页面容器,将滚动区域的overflow设置为可滚动(注意此时的滚动容器)
方案二: 直接使用position: fixed固定顶部Tab,滚动区域不做安排,直接使用window容器滚动 - 点击Tab时滚动内容导到指定锚点处:
方案一: 使用scrollIntoView
方案二: 使用基础的scrollTop赋值的方式 - 滚动内容时,如到达该埋点时,自动切换Tab
方案: 监听scroll(根据功能1的方案确定滚动容器)
注意: 其实滚动容器的选择就决定了功能2、功能3的方案选择
方案比较:
-
滚动容器选择方案1,即部分区域可滚动
缺点1: 需要配置好、区分好可滚动区域,且外部需要设置 overflow: hidden 阻止滚动(注意外部不可有滚动区域)
缺点2: 后续的scroll监听需要依赖于选择的滚动容器节点
缺点3: scrollIntoView的使用兼容性问题(smooth平滑动画无效果) -
滚动容器选用方案2,即使用window容器进行滚动
缺点1: scrollIntoView的效果,是将节点滚动到可滚动容器的顶端,而我们是有置顶内容的
所以:
最终选择: 1. 使用window作为滚动区域,不使用scrollIntoView,而是直接通过scrollTop赋值来实现滚动效果
方案一CSS代码: 可实现 顶部Tab 和 滚动区域的严格区分
.page-container {
position: relative;
overflow: hidden;
height: 100%;
display: flex;
flex-direction: column;
header {
// 顶部固定模块
width: 100%;
height: 1.8rem;
}
.scroll-container {
// 滚动容器
flex: 1;
overflow: auto;
.scroll-content {
// 滚动内容
.first {}
.second {}
.third {}
.fourth {}
.fifth {}
}
}
}
方案一JS代码: 可实现 点击Tab 滚动到指定锚点
methods: {
setActiveIndex(index) {
// 滚动tab到可视区域中间
// 使用 smooth 或者 center 有问题?
this.$refs[`topTab${index}`][0]?.scrollIntoView({ inline: 'center' });
if (this.activeIndex === index) {
return;
}
this.activeIndex = index;
// 滚动到指定位置
this.$refs[`scrollView${index}`]?.scrollIntoView();
},
}
scrollIntoView 平滑滚动
element.scrollIntoView({behavior: "smooth"})
最终实现方案:
切换Tab实现内容滚动到指定锚点:
methods: {
// 滚动到指定位置
scrollToElePosition(index) {
const { first, second, third, fourth, fifth } = this.$refs;
// 记录当前滚动状态
this.isSmoothScrolling = true;
// 动画时间
const SCROLL_DURATION = 500;
if (index === TAB_INDEX_ZERO) {
scrollToY(first.offsetTop, SCROLL_DURATION);
} else if (index === TAB_INDEX_ONE) {
scrollToY(second.offsetTop, SCROLL_DURATION);
} else if (index === TAB_INDEX_TWO) {
scrollToY(third.offsetTop, SCROLL_DURATION);
} else if (index === TAB_INDEX_THREE) {
scrollToY(fourth.offsetTop, SCROLL_DURATION);
} else if (index === TAB_INDEX_FOUR) {
scrollToY(fifth.offsetTop, SCROLL_DURATION);
}
},
}
使用scrollTop赋值实现滚动动画:
/*
* y: the y coordinate to scroll, 0 = top
* duration: scroll duration in milliseconds; default is 0 (no transition)
* element: the html element that should be scrolled ; default is the main scrolling element
*/
const scrollToY = function (y, duration = 0, element = document.scrollingElement) {
// cancel if already on target position
if (element.scrollTop === y) {
return;
}
const NUMBER_TWO = 2;
const cosParameter = (element.scrollTop - y) / NUMBER_TWO;
let scrollCount = 0;
let oldTimestamp = null;
const step = function (newTimestamp) {
if (oldTimestamp !== null) {
// if duration is 0 scrollCount will be Infinity
scrollCount += Math.PI * (newTimestamp - oldTimestamp) / duration;
if (scrollCount >= Math.PI) {
element.scrollTop = y;
return;
}
element.scrollTop = cosParameter + y + cosParameter * Math.cos(scrollCount);
}
oldTimestamp = newTimestamp;
window.requestAnimationFrame(step);
};
window.requestAnimationFrame(step);
};
监听内容滚动,切换Tab
实现方式: 主要是通过scroll监听来实现,通过获取当前的scrollTop 来和 每个锚点模块的offsetTop 进行比较
注意点:
- 注意throttle节流的使用
- 需要使用 isSmoothScrolling 来避免 和 切换Tab时滚动到指定锚点的功能相互影响
mounted() {
// 基准offset - first模块
const { offsetTop: firstOffsetTop } = first;
// 节流时间间隔
const THROTTLE_INTERVAL = 200;
window.addEventListener(
'scroll',
!this.isSmoothScrolling &&
throttle(() => {
// 正在平滑滚动,无需监听
if (this.isSmoothScrolling) return;
// 当前的滚动距离
const scrollTop =
window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
// 各个临界值,可能会变
const secondOffsetTop = second.offsetTop;
const thirdOffsetTop = third.offsetTop;
const fourthOffsetTop = fourth.offsetTop;
const fifthOffsetTop = fifth.offsetTop;
// scroll计算当前的activeTab
this.scrollCalcCurrentActiveTab({
scrollTop,
firstOffsetTop,
secondOffsetTop,
thirdOffsetTop,
fourthOffsetTop,
fifthOffsetTop,
});
}, THROTTLE_INTERVAL)
);
},
methods: {
// scroll计算当前的activeTab
scrollCalcCurrentActiveTab({
scrollTop,
firstOffsetTop,
secondOffsetTop,
thirdOffsetTop,
fourthOffsetTop,
fifthOffsetTop,
}) {
// 此处向上取整,因为offsetTop获取的是整数,这会导致滚动时tab位置错误
const currentOffsetTop = firstOffsetTop + Math.ceil(scrollTop);
if (currentOffsetTop < secondOffsetTop) {
this.activeIndex = 0;
} else if (currentOffsetTop >= secondOffsetTop && currentOffsetTop < thirdOffsetTop) {
this.activeIndex = 1;
} else if (currentOffsetTop >= thirdOffsetTop && currentOffsetTop < fourthOffsetTop) {
this.activeIndex = 2;
} else if (currentOffsetTop >= fourthOffsetTop && currentOffsetTop < fifthOffsetTop) {
this.activeIndex = 3;
} else if (currentOffsetTop >= fifthOffsetTop) {
this.activeIndex = 4;
}
},
}
最后:
- 需要注意 动画滚动和 window.addEventListener('scroll') 监听相互干扰的情况,需要使用变量 isSmoothScrolling 来规避
实现:
在切换tab,内容滚动至锚点时 this.isSmoothScrolling = true;
监听touchmove事件,将 this.isSmoothScrolling = false;
scroll监听是通过判断isSmoothScrolling,来确定是否执行scroll监听的回调方法
mounted() {
// 此处通过
window.addEventListener(
'touchmove',
throttle(() => {
if (this.isSmoothScrolling) {
this.isSmoothScrolling = false;
}
})
);
}
- 注意使用节流来节省性能
最后的最后:
无jquery 的 滚动动画效果:
https://*.com/questions/21474678/scrolltop-animation-without-jquery
https://github.com/Robbendebiene/Sliding-Scroll/blob/master/sliding-scroll.js