You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android ScrollView如何在视图两侧显示垂直滚动条轨道

实现视图两侧显示垂直滚动条的方案

我之前也碰到过类似的需求,嵌套滚动条的方式确实容易出现同步不及时或者样式不符合预期的问题,其实咱们可以换个思路——不用嵌套原生滚动条,而是自定义两个同步的垂直滚动条,绑定到同一个内容容器上,这样就能完美实现两侧都显示滚动轨道的效果了。

核心思路

  1. 用一个主容器包裹内容区,隐藏原生滚动条
  2. 在主容器的左右两侧各添加一个自定义的滚动条组件(包含轨道和滑块)
  3. 监听主容器的滚动事件,同步两个滑块的位置
  4. 点击或拖动滑块时,同步主容器的滚动位置
  5. 处理窗口 resize 事件,确保滑块高度能自适应内容长度

具体代码实现

HTML 结构

<div class="scroll-container">
  <!-- 左侧滚动条 -->
  <div class="custom-scrollbar left-scrollbar">
    <div class="scrollbar-thumb"></div>
  </div>
  
  <!-- 内容区域 -->
  <div class="content-wrapper">
    <!-- 这里放你的内容 -->
    <div class="content">
      <!-- 示例内容,替换成你自己的 -->
      <p>测试内容行 1</p>
      <p>测试内容行 2</p>
      <p>测试内容行 3</p>
      <p>测试内容行 4</p>
      <p>测试内容行 5</p>
      <!-- 重复足够多的内容让滚动条出现 -->
      <p>测试内容行 6</p>
      <p>测试内容行 7</p>
      <p>测试内容行 8</p>
      <p>测试内容行 9</p>
      <p>测试内容行 10</p>
      <p>测试内容行 11</p>
      <p>测试内容行 12</p>
      <p>测试内容行 13</p>
      <p>测试内容行 14</p>
      <p>测试内容行 15</p>
    </div>
  </div>
  
  <!-- 右侧滚动条 -->
  <div class="custom-scrollbar right-scrollbar">
    <div class="scrollbar-thumb"></div>
  </div>
</div>

CSS 样式

/* 主容器:相对定位,隐藏溢出 */
.scroll-container {
  position: relative;
  width: 100%;
  height: 500px; /* 自定义容器高度 */
  overflow: hidden;
  border: 1px solid #eee;
}

/* 内容包裹器:开启原生滚动,但隐藏原生滚动条 */
.content-wrapper {
  width: calc(100% - 40px); /* 给左右滚动条留出空间(每个滚动条宽20px) */
  height: 100%;
  overflow-y: auto;
  margin: 0 20px;
  padding: 0 15px;
  box-sizing: border-box;
}

/* 隐藏原生滚动条(兼容不同浏览器) */
.content-wrapper::-webkit-scrollbar {
  display: none;
}
.content-wrapper {
  -ms-overflow-style: none;
  scrollbar-width: none;
}

/* 自定义滚动条轨道样式 */
.custom-scrollbar {
  position: absolute;
  top: 0;
  width: 20px;
  height: 100%;
  background-color: #f5f5f5;
}

/* 左右滚动条定位 */
.left-scrollbar {
  left: 0;
}
.right-scrollbar {
  right: 0;
}

/* 滚动条滑块样式 */
.scrollbar-thumb {
  width: 100%;
  background-color: #ccc;
  border-radius: 10px;
  cursor: pointer;
  position: relative;
  top: 0;
  transition: background-color 0.2s;
}

.scrollbar-thumb:hover {
  background-color: #999;
}

JavaScript 逻辑

const scrollContainer = document.querySelector('.scroll-container');
const contentWrapper = document.querySelector('.content-wrapper');
const leftThumb = document.querySelector('.left-scrollbar .scrollbar-thumb');
const rightThumb = document.querySelector('.right-scrollbar .scrollbar-thumb');

// 初始化滑块高度
function initScrollbarThumb() {
  const containerHeight = scrollContainer.clientHeight;
  const contentHeight = contentWrapper.scrollHeight;
  
  // 计算滑块高度:容器高度 / 内容高度 * 容器高度,最小高度设为50px避免过短
  const thumbHeight = Math.max(50, (containerHeight / contentHeight) * containerHeight);
  leftThumb.style.height = `${thumbHeight}px`;
  rightThumb.style.height = `${thumbHeight}px`;
}

// 同步滑块位置和内容滚动
function syncScrollbar() {
  const containerHeight = scrollContainer.clientHeight;
  const contentHeight = contentWrapper.scrollHeight;
  const scrollTop = contentWrapper.scrollTop;
  
  // 内容不足以滚动时隐藏滑块
  if (contentHeight <= containerHeight) {
    leftThumb.style.display = 'none';
    rightThumb.style.display = 'none';
    return;
  } else {
    leftThumb.style.display = 'block';
    rightThumb.style.display = 'block';
  }
  
  // 计算滑块的top值:滚动距离 / (内容高度 - 容器高度) * (容器高度 - 滑块高度)
  const thumbTop = (scrollTop / (contentHeight - containerHeight)) * (containerHeight - leftThumb.clientHeight);
  leftThumb.style.top = `${thumbTop}px`;
  rightThumb.style.top = `${thumbTop}px`;
}

// 监听内容滚动事件
contentWrapper.addEventListener('scroll', syncScrollbar);

// 监听窗口 resize,重新计算滑块高度
window.addEventListener('resize', () => {
  initScrollbarThumb();
  syncScrollbar();
});

// 处理滑块拖动逻辑
function setupDragScroll(thumb) {
  let isDragging = false;
  let startY = 0;
  let startScrollTop = 0;

  thumb.addEventListener('mousedown', (e) => {
    isDragging = true;
    startY = e.clientY;
    startScrollTop = contentWrapper.scrollTop;
    document.body.style.userSelect = 'none'; // 拖动时禁止选中文本
  });

  document.addEventListener('mousemove', (e) => {
    if (!isDragging) return;
    
    const containerHeight = scrollContainer.clientHeight;
    const contentHeight = contentWrapper.scrollHeight;
    const thumbHeight = thumb.clientHeight;
    
    // 计算拖动的距离比例,同步到内容滚动
    const deltaY = e.clientY - startY;
    const scrollRatio = deltaY / (containerHeight - thumbHeight);
    contentWrapper.scrollTop = startScrollTop + scrollRatio * (contentHeight - containerHeight);
  });

  document.addEventListener('mouseup', () => {
    isDragging = false;
    document.body.style.userSelect = '';
  });
}

// 给左右滑块绑定拖动事件
setupDragScroll(leftThumb);
setupDragScroll(rightThumb);

// 初始化
initScrollbarThumb();
syncScrollbar();

关键细节说明

  • 隐藏原生滚动条是为了避免和自定义滚动条冲突,同时保证内容能正常滚动
  • 滑块高度根据内容高度和容器高度的比例计算,确保滚动比例和原生滚动一致
  • 拖动滑块时通过比例计算同步内容滚动,保证滚动的流畅性
  • 内容不足以滚动时自动隐藏滑块,提升用户体验
  • 窗口 resize 时重新计算滑块高度,避免尺寸变化导致滚动条样式错乱

这个方案完全不需要嵌套滚动条,能完美实现两侧显示滚动轨道的效果,而且样式可以根据你的需求随意调整——比如修改滚动条的宽度、颜色、圆角,甚至给轨道添加阴影效果都可以。

内容的提问来源于stack exchange,提问作者Mahmoud Hadad

火山引擎 最新活动