需求
之前在工作中遇到的需求。色块里面放了具体内容,有文字或文字加图片。要求并排的3个色块在鼠标往下滚动的过程中慢慢从下往上浮现,当滚动到一定距离后,色块内容更改,新的并排色块重新从下往上浮现,脱离动效后如果屏幕向上滚动,色块以相反效果展示。
具体效果如下GIF所示,黑色部分是页面外的调试界面。
问题分析
上面的需求已经做了一层分析,将UI的想法抽象出来,用程序员的语言描述一遍,但这层分析还远远还不够。
DOM结构设计
难点在于整个动效DOM的结构设计。
首先我们需要理解,页面在向下滚动的过程中,高度会不断增大,我们肉眼看到动效没有下移,但实际上动效在父结点中一直在往下掉。我们不仅要考虑并排色块,同时要考虑上面的紫色和粉色色块,因为在向下滚动过程中,它们是静止不动的,并没有按照正常的逻辑往下滚,所以它们的静止本身也是一种动效。所以当鼠标往下滚动时,我们需要考虑两种动效,一种是并排色块逐渐浮现消失的动效,另一种是紫色色块和粉色色块(下面合二为一改称静止色块)的静止动效。
在动效外,需要再包一层div作为动效的父元素,动效在这个父元素里面滚动。举个例子,就像一个滑梯,有个小女孩在上面往下滑,我们的视角(屏幕窗口)只定格在孩子以及周围一小块环境身上,我们能感受到孩子在往下滑,但是看不到完整的滑梯。
说到动效,我们自然而然会想到定位属性position,通过上面的分析,一开始我尝试的是绝对定位和固定定位。鼠标往下滚动时,监控屏幕高度,当滚动到一定的高度时,用固定定位将静止色块固定在屏幕的一个位置,计算高度,不断修改并排色块的css参数,其中包含绝对定位(通过不断刷新这些参数达到浮现效果,在下面会详细说明)。可惜这个方案不够完美,具体表现在,当我们滚动时,在将静止色块的定位属性值从默认变成fixed的那一刻,整个元素会卡一下,而并排色块由于不断刷新绝对定位且屏幕同时往下滚动,会出现明显的抖动。总之,这种方案的表现非常不流畅,影响观感,不信的朋友大可尝试一番。
尝试使用粘性定位
既然使用粘性定位,那么以什么内容作为对象呢?在上面列举的小女孩滑滑梯的例子中,我们可以想到,对象应该就是小女孩
。小女孩即是两种动效的结合,因为我们的视角跟随着小女孩,所以在我们的视角中,有些元素即是自然而然静止的。我们可以将例子继续生动一些,假如小女孩在下滑的过程中,不断向我们招手,其他身体部位则保持不动。那么手臂即可看做并排色块,其他部位看做静止色块。此刻思路逐渐清晰了,dom结构设计大概如下:
<div class="滑梯">
<div class="小女孩">
<div class="粉色色块"></div>
<div class="紫色色块"></div>
<div class="并排色块">
...
</div>
</div>
</div>
我们的视角在小女孩身上,所以应该对小女孩使用粘性布局:
.小女孩 {
height: 100vh; // 整个元素高度占满一个屏幕
position: sticky; // 粘性定位
top: 0; // 整屏黏在顶部,不会展示小女孩以外的内容,直到滚出滑梯
...
}
滑梯要足够长:
.滑梯 {
height: 300vh; // 设计长度为3屏高
}
至此,从原理分析了整个动效DOM的设计,我认为这一层相对重要也更难一些。
并排色块动效详细设计
我们继续引用上述小女孩滑滑梯的例子。
具体思路:监听屏幕滚动高度,当高度到达滑梯顶部时,动效开始(接下来说的动效仅指并排色块动效),计算小女孩与滑梯顶部的距离,分别更改并排色块与父元素的顶内边距(padding-top)以及色块的透明度(opacity)。另外,因为色块在中途会更换,所以其实是有6个不同的色块,分成2组,均匀变化。需要再添加一个控制参数,控制每组色块的显隐,当高度达到某一点时,一组色块立即隐藏,换另一组色块逐渐浮现。
所以并排色块内部DOM结构应该如下:
<div class="并排色块">
<div class="第1组">
<div class="色块1"></div>
<div class="色块2"></div>
<div class="色块3"></div>
</div>
<div class="第二组">
<div class="色块4"></div>
<div class="色块5"></div>
<div class="色块6"></div>
</div>
</div>
通过以上分析,我们需要监控高度差,实时更新2个参数:opacity、padding-top以及整组元素的显隐状态,这里需要使用JS函数实现,具体实现看代码,值得一提的是,并排色块向上提升的快慢,透明度变化的速率,都可以自己调节。比如增加一个额外的参数作为变化速率,实时更改透明度时加上这个速率。
我的设计是滑梯占3屏高,原始状态占一屏高,第一组色块动效变化占一屏高,第二组色块动效占一屏高。
具体实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一种滚动动效</title>
<style>
.head {
width: 100%;
height: 400px;
background-color: aqua;
}
.footer {
width: 100%;
height: 1000px;
background-color: brown;
}
.body {
width: 100%;
height: 300vh;
}
.body-full-screen {
height: 100vh;
padding-top: 100px;
position: sticky;
top: 0;
text-align: center;
}
.body-content {
text-align: center;
}
.body-title {
width: 800px;
height: 100px;
background-color: pink;
margin: 0 auto;
margin-bottom: 60px;
}
.body-des {
width: 1200px;
height: 200px;
background-color: purple;
margin: 0 auto;
margin-bottom: 80px;
}
.body-num-first,
.body-num-second {
display: flex;
justify-content: space-between;
width: 1500px;
margin: 0 auto;
padding-top: 120px;
}
.body-num-item {
width: 200px;
height: 200px;
}
.body-num-1 {
background-color: red;
}
.body-num-2 {
background-color: green;
}
.body-num-3 {
background-color: blue;
}
.body-num-4 {
background-color: salmon;
}
.body-num-5 {
background-color: khaki;
}
.body-num-6 {
background-color: peru;
}
.hide {
display: none;
}
.opacity-zero {
opacity: 0;
}
</style>
</head>
<body>
<div>
<div class="head"></div>
<div class="body">
<div class="body-full-screen">
<div class="body-title"></div>
<div class="body-des"></div>
<div class="body-num-first opacity-zero">
<div class="body-num-1 body-num-item"></div>
<div class="body-num-2 body-num-item"></div>
<div class="body-num-3 body-num-item"></div>
</div>
<div class="body-num-second opacity-zero hide">
<div class="body-num-4 body-num-item"></div>
<div class="body-num-5 body-num-item"></div>
<div class="body-num-6 body-num-item"></div>
</div>
</div>
</div>
<div class="footer"></div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script>
let screenHeight = $(window).height()
let bodyTop = $('.body').offset().top
$(window).scroll(function () {
let currentTop = $(window).scrollTop()
if (currentTop > bodyTop && currentTop < (bodyTop + screenHeight)) {
$('.body-num-first').css('display', 'flex')
$('.body-num-second').css('display', 'none')
let firstOpacityValue = (currentTop - bodyTop) / screenHeight
let firstPaddingTopValue = (1 - firstOpacityValue) * 120 + 'px'
$('.body-num-first').css({
'opacity': firstOpacityValue,
'padding-top': firstPaddingTopValue
})
} else if (currentTop >= (bodyTop + screenHeight) && currentTop < (bodyTop + 2 * screenHeight)) {
$('.body-num-first').css('display', 'none')
$('.body-num-second').css('display', 'flex')
let secondOpacityValue = (currentTop - bodyTop - screenHeight) / screenHeight
let secondPaddingTopValue = (1 - secondOpacityValue) * 120 + 'px'
$('.body-num-second').css({
'opacity': secondOpacityValue,
'padding-top': secondPaddingTopValue
})
}
})
</script>
</body>
</html>