效果
先看一下官网效果:ys.mihoyo.com/main/map
我写的真实效果:ysFullPage (chenyajun.fun)
我的另一篇: 原神角色切换效果在这里:我使用 Vue 实现了原神官网的角色切换效果-系列下 - 掘金 (juejin.cn)
概念
每次当滚动鼠标滚轮的时候,页面会进行一整页的滚动,这就是全屏滚动。
要求
当窗口大小变化时,全屏滚动效果不会发生变化,即需要做到自适应,不受到高度宽度的影响。
当点击指示器也可以进行页面切换。
原理
最外层容器:就是我们的窗口,视角能看到的。
内层容器: 用来存放我们需要滚动内容的容器。
滚动元素: 就是我们看到的一张张照片。
首先对最外侧容器的overflow:hidden;这样保证不会溢出。
其次对内层容器进行滚动,每次滚动的高度就是窗口的高度,但是需要动态计算窗口的高度。
最后只需要判断上一页还是下一页来计算index,通过index ✖ 页面高度,来进行滚动即可。
代码实现
HTML代码
大家可以结合html结构,去理解 js 代码
<!-- 最外层容器 -->
<div class="outer-box" ref="fullPage">
<!-- 内层容器 -->
<div
ref="element"
:class="{ activeTranstion: isCloseTranstion }"
class="inner-box"
@mousewheel="mousewheel"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
@touchmove="handleTouchMove"
>
<!-- 滚动显示的元素 -->
<div
v-for="item in ysImage"
:style="{ backgroundImage: `url(${item.backgroundImage})`, height: windowHeight + 'px' }"
class="scroll-element"
></div>
</div>
<!-- 指示器 -->
<ul class="aside">
<li v-for="(item, index) in asideData" @click="changeBac(index)">
<span :class="{ active: index === $index }"></span>
<div v-show="index === $index" class="show-dec">{{ item.title }}</div>
</li>
</ul>
</div>
JS代码
给内层容器添加鼠标滚动事件
当滚动事件触发时判断是向上还是向下滚动,以此来控制滚动方向。
另外由于鼠标滚动事件触发过于频繁,我们需要增加节流,即保证在单位时间内只执行一次即可。
设置canRun为节流标志,每500毫秒执行一次。
function mousewheel(e) {
isTranstion.value = false
if (canRun.value) {
canRun.value = false
goScroll(e)
setTimeout(() => {
canRun.value = true
}, 1100)
}
}
开始滚动元素
通过上一页下一页增加减少$index,通过索引 ✖ 页面高度,来决定页面滚动的距离,也就是当前要显示的页面。
function goScroll(e) {
//e.wheelDelta 用来判断上一个下一个 <0 下一个 >0上一个
if (e.wheelDelta < 0) {
next()
} else {
last()
}
}
//$INDEX
const $index = ref(0) //索引控制第几个显示
// 下一个
function next() {
if ($index.value < ysImage.value.length - 1) {
$index.value++
}
}
// 上一个
function last() {
if ($index.value > 1 || $index.value === 1) {
$index.value--
}
}
动态计算页面高度
由于页面高度发生变化时,我们需要动态响应高度的变化。
使用 VueUse 库提供的 useWindowSize() 函数计算vueuse.org/core/useWin…
当页面高度变化时height会相应变化
const { height } = useWindowSize()
值得注意的是,当页面高度变化时,我们需要关闭动画效果。
const windowHeight = computed(() => {
isCloseTranstion.value = true
return height.value
})
通过索引计算滚动高度
const transformScroll = computed(() => {
return `-${$index.value * windowHeight.value}px`
})
将滚动高度应用到滚动容器上面
watchEffect: 会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。官方:cn.vuejs.org/guide/essen…
// ELEMENT
const element = ref('element')
watchEffect(() => {
if (element.value.style) {
element.value.style.top = transformScroll.value
}
})
点击指示器
点击指示器的时候,跳转到相应页面,只需设置相应的index即可。
// 点击切换
function changeBac(index) {
// 点击切换时需要开启动画
isCloseTranstion.value = false
$index.value = index
}
移动端
//#region 移动端
const startY = ref(0) //记录开始位置
const endY = ref(0) //记录结束位置
const moveDistance = ref(0) //滑动距离
// 触摸开始
function handleTouchStart(e) {
startY.value = e.touches[0].pageY || e.changedTouches[0].pageY
}
// 触摸移动
function handleTouchMove(e) {
e.preventDefault()//wx上拉默认事件
isCloseTranstion.value = true // 开始移动 关闭动画
moveDistance.value = (e.changedTouches[0].pageY || e.touches[0].pageY) - startY.value // 计算移动距离
//判断临界点
const isCriticalPoint =
($index.value === ysImage.value.length - 1 && moveDistance.value < 0) || ($index.value === 0 && moveDistance.value > 0)
// 如果是临界点就直接返回
if (isCriticalPoint) {
return
}
// 否则直接对内层容器应用 随之移动
element.value.style.top = `-${$index.value * windowHeight.value + moveDistance.value * -1}px`
// 触摸抬起
function handleTouchEnd(e) {
// 抬起时开启动画
isCloseTranstion.value = false
// 计算结束距离
endY.value = e.changedTouches[0].pageY || e.touches[0].pageY
// 计算移动距离,判断应该上一页还是下一页,直接更改index即可在原先基础上整页移动
moveDistance.value = endY.value - startY.value
// 这里我做了一个最小值 大于50才翻页
if (Math.abs(moveDistance.value) >= 60) {
if ($index.value < ysImage.value.length - 1 && moveDistance.value < 0) {
$index.value++
}
if ($index.value > 0 && moveDistance.value > 0) {
$index.value--
}
} else {
// 当临界值小于60意味着不需要翻页 就恢复原来的位置即可
element.value.style.top = `-${$index.value * windowHeight.value}px`
}
}
}
总结
全屏滚动的核心还是判断上一页还是下一页,动态计算页面高度,对内层容器进行滚动,从而显示我们需要展示的元素即可。不过在一些细节处理上还是需要注意,尤其是动画的开关。移动端的触摸同步也需要动画的配合,看似简单的一个应用,其实里面值得学习的内容还是比较多的。
写作不易,你的赞就是我最大的动力,觉得写的不错的,可以给点个赞呢~
全部源码
文章代码可能更新不及时,所有代码会同步更新GitHub
GitHub地址:github.com/chenyajun-c…
所有源码已经同步提交GitHub,觉得对你有帮助的话,记得帮作者点下star⭐~非常感谢呢
<script setup>
import { useWindowSize } from '@vueuse/core'
import { ref, computed, watchEffect } from 'vue'
// IMAGE DATA
const ysImage = ref([
{
backgroundImage: 'https://ys.mihoyo.com/main/_nuxt/img/5c125a1.png',
},
{
backgroundImage: 'https://uploadstatic.mihoyo.com/contentweb/20200319/2020031921550320292.jpg',
},
{
backgroundImage: 'https://uploadstatic.mihoyo.com/contentweb/20200319/2020031921552395638.jpg',
},
{
backgroundImage: 'https://uploadstatic.mihoyo.com/contentweb/20210719/2021071918001232800.jpg',
},
{
backgroundImage:
'https://webstatic.mihoyo.com/upload/contentweb/2022/08/15/8969f683b92839ac427c875d0d742be2_4825576482548821743.jpg',
},
{
backgroundImage:
'https://act-webstatic.mihoyo.com/upload/contentweb/hk4e/721a74c43614d7aeb25b046cabfb57be_2012964858524199390.jpg',
},
])
const asideData = ref([
{
title: '首页',
},
{
title: '蒙德',
},
{
title: '璃月',
},
{
title: '稻妻',
},
{
title: '须弥',
},
{
title: '枫丹',
},
])
// ELEMENT
const element = ref('element')
watchEffect(() => {
if (element.value.style) {
// element.value.style.transform = transformScroll.value
element.value.style.top = transformScroll.value
}
})
//HEIGHT
const { height } = useWindowSize()
const windowHeight = computed(() => {
// 高度变化时需要关闭动画
isCloseTranstion.value = true
return height.value
})
const transformScroll = computed(() => {
// return `translateY(-${$index.value * windowHeight.value}px)`
return `-${$index.value * windowHeight.value}px`
})
const isCloseTranstion = ref(false) //控制是否显示动画效果
const canRun = ref(true) //节流控制器
function mousewheel(e) {
isCloseTranstion.value = false
if (canRun.value) {
canRun.value = false
goScroll(e)
setTimeout(() => {
canRun.value = true
}, 1100)
}
}
//#region 移动端
const startY = ref(0) //记录开始位置
const endY = ref(0) //记录结束位置
const moveDistance = ref(0) //滑动距离
// 触摸开始
function handleTouchStart(e) {
startY.value = e.touches[0].pageY || e.changedTouches[0].pageY
}
// 触摸抬起
function handleTouchEnd(e) {
e.preventDefault()
// 抬起时开启动画
isCloseTranstion.value = false
// 计算结束距离
endY.value = e.changedTouches[0].pageY || e.touches[0].pageY
// 计算移动距离,判断应该上一页还是下一页,直接更改index即可在原先基础上整页移动
moveDistance.value = endY.value - startY.value
// 这里我做了一个最小值 大于50才翻页
if (Math.abs(moveDistance.value) >= 60) {
if ($index.value < ysImage.value.length - 1 && moveDistance.value < 0) {
$index.value++
}
if ($index.value > 0 && moveDistance.value > 0) {
$index.value--
}
} else {
// 当临界值小于60意味着不需要翻页 就恢复原来的位置即可
// element.value.style.transform = `translateY(-${$index.value * windowHeight.value}px)`
element.value.style.top = `-${$index.value * windowHeight.value}px`
}
}
// 触摸移动
function handleTouchMove(e) {
isCloseTranstion.value = true // 开始移动 关闭动画
// e.stopPropagation()
e.preventDefault()
// if (isIOS()) {
// return
// }
moveDistance.value = (e.changedTouches[0].pageY || e.touches[0].pageY) - startY.value // 计算移动距离\
//判断临界点
const isCriticalPoint =
($index.value === ysImage.value.length - 1 && moveDistance.value < 0) ||
($index.value === 0 && moveDistance.value > 0)
// 如果是临界点就直接返回
if (isCriticalPoint) {
return
}
// 否则直接对内层容器应用 随之移动
// element.value.style.transform = `translateY(-${$index.value * windowHeight.value + moveDistance.value * -1}px)`
element.value.style.top = `-${$index.value * windowHeight.value + moveDistance.value * -1}px`
}
//#endregion
//ANOTHER writting about full-page
// const { y } = useScroll(document)
// watchThrottled(
// y,
// (newValue, oldValue) => {
// if (newValue > oldValue && newValue > 120) {
// next()
// } else {
// last()
// }
// },
// { throttle: 300 },
// )
function goScroll(e) {
//e.wheelDelta 用来判断上一个下一个 <0 下一个 >0上一个
if (e.wheelDelta < 0) {
next()
} else {
last()
}
}
//$INDEX
const $index = ref(0) //索引控制第几个显示
// 下一个
function next() {
if ($index.value < ysImage.value.length - 1) {
$index.value++
}
}
// 上一个
function last() {
if ($index.value > 1 || $index.value === 1) {
$index.value--
}
}
// 点击切换
function changeBac(index) {
// 点击切换时需要开启动画
isCloseTranstion.value = false
$index.value = index
}
</script>
<template>
<!-- 最外层容器 -->
<div class="outer-box" ref="fullPage">
<!-- 内层容器 -->
<div
ref="element"
:class="{ activeTranstion: isCloseTranstion }"
class="inner-box"
@mousewheel="mousewheel"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
@touchmove="handleTouchMove"
>
<!-- 滚动显示的元素 -->
<div
v-for="item in ysImage"
:style="{ backgroundImage: `url(${item.backgroundImage})`, height: windowHeight + 'px' }"
class="scroll-element"
></div>
</div>
<!-- 指示器 -->
<ul class="aside">
<li v-for="(item, index) in asideData" @click="changeBac(index)">
<span :class="{ active: index === $index }"></span>
<div v-show="index === $index" class="show-dec">{{ item.title }}</div>
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.activeTranstion {
transition: all 0ms ease 0s !important;
}
.active {
display: inline-block;
width: 12px !important;
height: 12px !important;
}
.outer-box {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
.inner-box {
position: absolute;
width: 100%;
transition: all ease-in-out 0.5s;
.scroll-element {
// height: 100%;
background-size: cover !important;
background-position: center;
background-repeat: no-repeat;
}
}
.aside {
list-style: none;
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
li {
height: 14px;
width: 14px;
margin: 7px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.show-dec {
text-align: right;
position: absolute;
width: 70px;
right: 20px;
padding: 1px;
// opacity: 0;
color: #000;
transition: all linear 0.1s;
font-size: 12px;
background-color: #fff;
}
span {
border-radius: 100%;
border: #fff solid 1px;
width: 4px;
height: 4px;
display: inline-block;
background-color: #fff;
transition: all ease-in-out 0.2s;
}
&:hover span {
width: 10px;
height: 10px;
background-color: #fff;
cursor: pointer;
}
}
}
}
</style>