scrollWidth / clientWidth / scrollLeft 判断溢出,平滑滚动模拟翻页
横向导航栏菜单过多的时候,会造成横向的导航栏隐藏。现想做:当导航栏过多隐藏的时候,加一个【下一页】;当左边有隐藏的时候,加一个【上一页】。效果如下:
dom 元素中有 scrollWidth、clientWidth、scrollLeft 属性。在 DOM 中,这三个是与元素滚动和尺寸相关的属性,特别是在处理滚动容器或可滚动元素时非常有用:
返回元素的完整宽度,包括由于溢出(overflow)而被隐藏的部分。通常等于元素可见宽度加上任何由于内容过多而溢出到视口之外的部分,用于确定元素的实际内容宽度。
返回元素的内部宽度(以像素为单位),包括元素的填充(padding),但不包括边框(border)、外边距(margin)和水平滚动条。通常是元素在视口中可见的宽度。
设置或返回元素被滚动到左边的距离(以像素为单位)。这是一个可写属性,可以设置它来滚动元素到特定位置,也可读取来确定当前滚动位置。常用于滚动动画、滚动到特定位置或跟踪滚动位置。
| 属性 | 含义 |
|---|---|
| clientWidth | 可视窗口宽度 |
| scrollWidth | 全部内容的宽度 |
| scrollLeft | 元素距离左右的滚动距离 |
判断逻辑:
代码结构如下,借助 vue3 来实现 demo,源码如下:
<template>
<div class="header">
<div class="prev btn" @click="changeLeft" v-if="prevable">前</div>
<div class="menu-wrap" ref="wrapRef">
<!-- 导航 -->
<div class="menu-item" v-for="(item, index) in nemuList" :key="index">{{ item }}</div>
</div>
<div class="next btn" @click="changeRight" v-if="nextable">后</div>
<div class="logo">
头像等占位符
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, nextTick } from "vue"
// 导航
const nemuList = ref(Array.from({ length: 20 }, (_, index) => `测试导航${index + 1}`));
// 上一页是否可点击
const prevable = ref(false);
// 下一页是否可点击
const nextable = ref(false);
// dom
const wrapRef = ref();
const changeLeft = async () => {
if (!prevable.value) return;
// 每次翻页一个
const dom = wrapRef.value;
smoothScrollTo(dom, dom.scrollLeft - 120, 300)
setTimeout(async () => {
await nextTick();
init()
}, 300);
}
const changeRight = async () => {
if (!nextable.value) return;
// 每次翻页一个
const dom = wrapRef.value;
smoothScrollTo(dom, dom.scrollLeft + 120, 300)
setTimeout(async () => {
await nextTick();
init()
}, 300);
}
const init = () => {
const dom = wrapRef.value;
if (!dom) return;
if (dom.scrollWidth > dom.clientWidth + dom.scrollLeft + 80) {
nextable.value = true;
} else {
nextable.value = false;
}
// 判断是否有上一页面
if (dom.scrollLeft) {
prevable.value = true;
} else {
prevable.value = false;
}
}
onMounted(() => {
init()
})
// 定义一个函数来实现平滑滚动
const smoothScrollTo = (element: any, targetScrollLeft: any, duration: any) => {
// 初始滚动位置
let start = element.scrollLeft;
let startTime: number | null = null;
// 动画效果的定时器
function scroll() {
// 确保startTime被设置
if (!startTime) startTime = performance.now();
// 计算经过的时间
const currentTime = performance.now();
const timeElapsed = currentTime - startTime;
// 计算当前应该滚动到的位置
let progress = Math.min(timeElapsed / duration, 1);
// 使用ease-out立方贝塞尔曲线公式
element.scrollLeft = start + ((targetScrollLeft - start) * (1 - Math.pow(1 - progress, 3)));
// 如果动画没有结束,则继续动画
if (timeElapsed < duration) {
requestAnimationFrame(scroll);
}
}
// 开始滚动动画
requestAnimationFrame(scroll);
}
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.header {
height: 50px;
width: 100%;
border: 1px solid gray;
display: flex;
.btn {
width: 50px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid salmon;
border-radius: 50%;
cursor: pointer;
&:hover {
color: white;
background-color: blue;
}
}
.menu-wrap {
flex: 1;
overflow: hidden;
display: flex;
transition: all 0.4s ease-in-out;
.menu-item {
transition: all 1s ease-in-out;
width: 120px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid greenyellow;
flex-shrink: 0;
}
}
.logo {
width: 200px;
background: orange;
display: flex;
justify-content: center;
align-items: center;
color: white;
}
}
</style>