在现代网页设计中,流畅的滚动交互和精美的视觉效果是提升用户体验的关键。本文将深入解析 Vivo 手机官网 Demo 中的一个核心交互效果 ------ 基于滚轮滚动的内容展示系统。这个系统允许用户通过滚动鼠标滚轮来浏览不同的手机镜头配置信息,同时伴随平滑的过渡动画和视觉反馈。
交互效果概述
这个交互效果主要由三部分组成:
- 垂直滚动指示器:显示当前滚动位置
- 左侧文本导航:根据滚动位置高亮不同的配置选项
- 右侧信息面板:展示详细的镜头配置信息,随滚动平滑切换
效果预览:



当用户滚动鼠标滚轮时,页面不会发生传统的滚动,而是触发自定义的交互逻辑:红色指示器会在垂直刻度线上移动,左侧文本导航会高亮对应选项,右侧信息面板会平滑切换显示内容。
核心技术实现
1. HTML 结构设计
页面主要包含三个部分:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Vivo手机官网Demo</title>
<script src="js/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="./css/new_file.css" />
</head>
<body>
<div class="bigContent">
<img class="imgs" src="./img/9a83bd1d-09e6-4fa0-bee7-b34a1c0370eb.png" alt="" />
<div class="verticalLine">
<div class="red-line" id="redLine"></div>
</div>
</div>
<div class="textContent">
<div class="block">影像配置</div>
<div class="block">观演神器</div>
<div class="block">街拍神器</div>
<div class="block">视频</div>
<div class="block">人像</div>
</div>
<!-- 文字1 -->
<div class="textCenter" id="textCenter1">
<div class="text1">
85mm 蔡司 APO 超级长焦镜头 II
</div>
<div class="text2">
蓝图 × 三星 HP9 | 2 亿像素 | 1/1.4" |<br />
f/2.27 | 85mm 等效焦距 | CIPA 5.0 专业级防抖1 |<br />
OIS 光学防抖
</div>
<div class="click1">
➕ 点击探索 14mm 蔡司超广角镜头
</div>
</div>
<!-- 文字2 -->
<div class="textCenter" id="textCenter2">
<div class="text1">
14mm 蔡司超广角镜头
</div>
<div class="text2">
蓝图 × 索尼 LYT-818 | 5000 万像素 | 1/1.28" | f/2.0 |<br />
14mm 等效焦距 | CIPA 5.0 专业级防抖1 | OIS 光学防抖
</div>
<div class="click1">
➕点击探索 14mm 蔡司超广角镜头
</div>
</div>
<!-- 文字3 -->
<div class="textCenter" id="textCenter3">
<div class="text1">
35mm 蔡司人文纪实镜头
</div>
<div class="text2">
蓝图 x 索尼 LYT-818 | 5000 万像素 | 1/1.28" | f/1.69 |<br />
35mm 等效焦距 | CIPA 5.0 专业级防抖1 | OIS 光学防抖
</div>
<div class="click1">
➕ 点击探索 35mm 蔡司人文纪实镜头
</div>
</div>
<script src="js/new_file.js"></script>
</body>
</html>
- 主内容区(包含背景图片和垂直滚动指示器)
- 左侧文本导航
- 右侧信息面板(三个面板,分别对应不同镜头配置)
每个信息面板包含标题、详细描述和一个交互按钮,按钮在悬停时有平滑的颜色和位置变化效果。
2. CSS 样式优化
为了实现流畅的视觉效果,CSS 中添加了多种过渡动画:
css
* {
padding: 0;
margin: 0;
}
body {
background-color: black;
color: white;
font-family: Arial, sans-serif;
}
.bigContent {
width: 1300px;
padding-top: 450px;
position: relative;
}
.imgs {
width: 100%;
position: absolute;
left: -100px;
}
/* 横线样式 */
.axis-line {
height: 1px;
background: dimgray;
margin-top: 5px;
}
/* 红色刻度线样式 */
.red-line {
position: absolute;
width: 100%;
height: 2px;
background: red;
top: 0;
left: 0;
transition: top 0.2s ease;
z-index: 2;
}
.verticalLine {
width: 10px;
overflow: hidden;
margin-left: 50px;
position: absolute;
top: 370px;
height: 210px;
/* (6px * 35 lines) */
cursor: ns-resize;
}
.block {
color: gray;
margin-left: 80px;
z-index: 9999;
height: 50px;
line-height: 50px;
transition: all 0.3s ease;
}
.block.highlight {
color: white;
font-size: 1.2em;
transform: translateX(5px);
}
.textContent {
position: absolute;
top: 365px;
font-size: 12px;
}
#textCenter1 {
/* width: 300px; */
height: 275px;
border-left: 1px solid white;
position: absolute;
left: 916px;
top: 218px;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.text1 {
margin-left: 30px;
margin-bottom: 10px;
font-size: 35px;
font-weight: bold;
}
.text2 {
margin-left: 30px;
font-size: 19px;
line-height: 1.5;
font-weight: 600;
color: gray;
}
#textCenter2 {
height: 347px;
border-left: 1px solid white;
position: absolute;
left: 767px;
top: 209px;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
#textCenter3 {
height: 290px;
border-left: 1px solid white;
position: absolute;
left: 747px;
top: 200px;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.click1 {
margin-top: 50px;
margin-left: 30px;
padding: 5px 10px;
width: 300px;
border-radius: 50px;
display: flex;
align-items: center;
justify-content: center;
background-color: #272727;
color: #C8CCCC;
font-weight: 600;
cursor: pointer;
transition: all 0.5s ease;
/* 添加过渡效果 */
}
.click1:hover {
background-color: white;
/* 悬浮时背景变为白色 */
color: #272727;
/* 悬浮时文字变为深色 */
transform: translateY(-2px);
/* 轻微上浮效果 */
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2);
/* 添加阴影 */
}
- 信息面板的淡入淡出和垂直位移效果
- 文本导航的高亮状态变化
- 按钮的悬停效果(颜色变化、上浮和阴影)
特别值得注意的是,信息面板的显示和隐藏不是简单的切换,而是通过透明度和位置的连续变化实现平滑过渡,增强了页面的层次感和动态效果。
3. JavaScript 交互逻辑
整个交互的核心在于 JavaScript 代码,主要实现了以下功能:
3.1 滚动控制
通过监听鼠标滚轮事件,阻止默认滚动行为,实现自定义的滚动逻辑
javascript
$(document).on('wheel', { passive: false }, function(e) {
e.preventDefault();
e.stopPropagation();
// 根据滚轮方向更新位置
if (e.originalEvent.deltaY > 0) {
scrollPosition = Math.min(scrollPosition + scrollSpeed, maxPosition);
} else {
scrollPosition = Math.max(scrollPosition - scrollSpeed, 0);
}
// 更新UI显示
updatePosition();
highlightBlock();
updateText();
});
3.2 垂直指示器
根据当前滚动位置计算并更新红色指示器的位置
javascript
function updatePosition() {
redLine.css('top', scrollPosition * lineHeight + 'px');
}
3.3 文本导航高亮
将滚动位置映射到对应的文本块索引,实现高亮效果
javascript
function highlightBlock() {
blocks.removeClass('highlight');
const blockIndex = Math.floor(scrollPosition / (maxPosition / (blockCount - 1)));
const safeIndex = Math.min(blockIndex, blockCount - 1);
blocks.eq(safeIndex).addClass('highlight');
}
3.4 信息面板显示控制
这是最复杂的部分,将整个滚动范围划分为三个区间,每个区间对应一个信息面板。每个面板的显示分为两个阶段:
javascript
function updateText() {
textCenters.each(function(index) {
const textCenter = $(this);
const interval = textIntervals[index];
// 计算当前位置在区间中的相对比例
let ratio = 0;
if (scrollPosition < interval.start) {
ratio = 0;
} else if (scrollPosition > interval.end) {
ratio = 0;
} else {
// 计算相对位置并调整比例以实现淡入淡出效果
const range = interval.end - interval.start;
ratio = (scrollPosition - interval.start) / range;
if (ratio < 0.5) {
// 淡入阶段 (0-0.5)
ratio = ratio * 2;
} else {
// 淡出阶段 (0.5-1)
ratio = 1 - ((ratio - 0.5) * 2);
}
}
// 应用透明度和位置变换
textCenter.css({
opacity: ratio,
transform: `translateY(${20 * (1 - ratio)}px)`
});
});
}
js代码总览:
javascript
$(function() {
// 页面加载完成后执行的代码
let lineNum = 35; // 定义垂直滚动条上的刻度线总数
const verticalLine = $('.verticalLine'); // 获取垂直滚动条容器元素
const redLine = $('#redLine'); // 获取红色指示线元素
const blocks = $('.block'); // 获取所有文本块元素(影像配置、观演神器等)
const textCenters = $('.textCenter'); // 获取所有右侧信息面板元素
let scrollPosition = 0; // 当前滚动位置,初始为0
const maxPosition = lineNum - 1; // 最大滚动位置(从0开始计数)
const blockCount = blocks.length; // 文本块的数量(5个)
const lineHeight = 6; // 每个刻度线的高度(像素)
const scrollSpeed = 0.5; // 控制每次滚动的距离
// 定义滚动区间
const textIntervals = [{
start: 0,
end: 3
}, // 第一个面板显示区间
{
start: 3,
end: 6
}, // 第二个面板显示区间
{
start: 6,
end: 9
} // 第三个面板显示区间
];
// 创建刻度线 - 在垂直滚动条容器中添加35条横线
for (let i = 0; i < lineNum; i++) {
$('<div>').addClass('axis-line').appendTo(verticalLine);
}
// 设置红色刻度线初始位置
updatePosition();
// 将滚轮事件绑定到document,处理鼠标滚动
$(document).on('wheel', function(e) {
e.preventDefault(); // 阻止默认滚动行为
e.stopPropagation(); // 阻止事件冒泡
// 根据滚轮方向更新位置
if (e.originalEvent.deltaY > 0) {
// 向下滚动,增加当前位置,但不超过最大位置(位置+距离)
scrollPosition = Math.min(scrollPosition + scrollSpeed, maxPosition);
} else {
// 向上滚动,减少当前位置,但不小于0(位置-距离)
scrollPosition = Math.max(scrollPosition - scrollSpeed, 0);
}
// 更新UI显示
updatePosition(); // 更新红色指示线位置
highlightBlock(); // 高亮对应的文本块
updateText(); // 更新信息面板的显示状态
});
// 更新红色刻度线位置的函数
function updatePosition() {
// 根据当前位置计算红色指示线的top值
redLine.css('top', scrollPosition * lineHeight + 'px');
}
// 高亮对应文本块的函数
function highlightBlock() {
// 清除所有文本块的高亮状态
blocks.removeClass('highlight');
// 计算当前应该高亮的文本块索引
// (计算当前滚动位置所在的范围(计算每个文本块对应的滚动范围()))
const blockIndex = Math.floor(scrollPosition / (maxPosition / (blockCount - 1)));
// 确保索引不超过文本块数量
const safeIndex = Math.min(blockIndex, blockCount - 1);
// 高亮对应的文本块
blocks.eq(safeIndex).addClass('highlight');
}
// 更新所有信息面板显示状态的函数
function updateText() {
textCenters.each(function(index) {
const textCenter = $(this); // 当前处理的面板
const interval = textIntervals[index]; // 当前面板的显示区间(如 [0,4]、[4,8] 等)
// 情况1:滚动位置在区间之前,完全隐藏面板
if (scrollPosition < interval.start) {
textCenter.css({
opacity: 0,
transform: 'translateY(20px)' // 下移20px并完全透明
});
}
// 情况2:滚动位置在区间之后,完全隐藏面板
else if (scrollPosition > interval.end) {
textCenter.css({
opacity: 0,
transform: 'translateY(20px)' // 同样下移20px并完全透明
});
}
// 情况3:滚动位置在区间的前半部分(显示阶段)
else if (scrollPosition < interval.start + (interval.end - interval.start) * 0.5) {
// 计算当前位置在前半段的进度比例(0~1)
const ratio = (scrollPosition - interval.start) / ((interval.end - interval.start) *
0.5);
textCenter.css({
opacity: ratio, // 透明度从0渐变为1
transform: `translateY(${20 * (1 - ratio)}px)` // 位置从下移20px渐变为0
});
}
// 情况4:滚动位置在区间的后半部分(隐藏阶段)
else {
// 计算当前位置在后半段的进度比例(0~1),并反转(1~0)
const ratio = 1 - ((scrollPosition - (interval.start + (interval.end - interval
.start) * 0.5)) /
((interval.end - interval.start) * 0.5));
textCenter.css({
opacity: ratio, // 透明度从1渐变为0
transform: `translateY(${20 * (1 - ratio)}px)` // 位置从0渐变为下移20px
});
}
});
}
// 初始高亮第一个块并更新文本显示状态
highlightBlock();
updateText();
});