UniApp 自定义滚动条实现

364 阅读5分钟

前言

在移动端开发中,系统默认的滚动条往往无法满足UI设计的需求。本文将详细介绍如何在UniApp中实现一个完全自定义的滚动条,该滚动条不仅外观可控,还能根据内容比例动态调整宽度,提供更好的用户体验。

需求分析

设计要求

  • 轨道样式:宽度40rpx,高度8rpx,圆角4rpx,背景色#EAEAEA
  • 滑块样式:高度8rpx,圆角4rpx,背景色#0084CA
  • 动态宽度:滑块宽度根据可视区域与总内容的比例动态计算
  • 位置同步:滑块位置与内容滚动完全同步
  • 间距控制:与内容区域和底部保持合适的间距

技术挑战

  1. 隐藏系统默认滚动条
  2. 实现滚动位置的实时同步
  3. 动态计算滑块宽度
  4. 处理边界情况和异常状态

技术方案

整体架构

scroll-view (水平滚动容器)
├── 滚动内容项
└── 自定义滚动条容器
    └── 滚动条轨道
        └── 滚动条滑块

核心思路

  1. 使用scroll-view组件承载可滚动内容
  2. 监听@scroll事件获取滚动信息
  3. 根据滚动数据计算滑块位置和宽度
  4. 通过CSS样式控制滑块的transform属性实现位置更新

具体实现

1. HTML结构设计

<template>
  <view class="scroll">
    <!-- 水平滚动内容区域 -->
    <scroll-view 
      class="scroll-view" 
      scroll-x 
      @scroll="scrollMenu" 
      :show-scrollbar="false"
    >
      <view class="scroll-view-item_H" v-for="(item,index) in zoneList" :key="index">
        <view class="card" @click.stop="clickObj(index)">
          <view class="flex" style="justify-content: center;align-items: center;">
            <image class="pic" :src="item.iconUrl" mode="aspectFill"></image>
          </view>
          <view class="subtitle">{{ item.remark }}</view>
        </view>
      </view>
    </scroll-view>
    
    <!-- 自定义滚动条 -->
    <view class="custom-scrollbar-container">
      <view class="custom-scrollbar-track">
        <view 
          class="custom-scrollbar-thumb" 
          :style="{ 
            transform: `translateX(${scrollLeft})`, 
            width: scrollThumbWidth 
          }"
        ></view>
      </view>
    </view>
  </view>
</template>

2. 数据初始化

data() {
  return {
    scrollLeft: '0rpx',           // 滑块位置
    scrollThumbWidth: '20rpx',    // 滑块宽度,动态计算
    zoneList: [
      { iconUrl: '../../static/images/icon_field.png', remark: '外勤' },
      { iconUrl: '../../static/images/scroll_fk.png', remark: '房勘相机' },
      { iconUrl: '../../static/images/scroll_sy.png', remark: '水印相机' },
      { iconUrl: '../../static/images/scroll_dk.png', remark: '视频带看' },
      { iconUrl: '../../static/images/scroll_jsq.png', remark: '计算器' },
      { iconUrl: '../../static/images/scroll_yx.png', remark: '营销素材' },
      { iconUrl: '../../static/images/scroll_card.png', remark: '电子名片' }
    ]
  }
}

3. 核心滚动监听方法

scrollMenu(e) {
  // 获取滚动视图的相关尺寸
  const scrollLeft = e.detail.scrollLeft || 0;
  const scrollWidth = e.detail.scrollWidth || 0;
  const clientWidth = e.detail.clientWidth || 343;
  
  // 安全检查
  if (scrollWidth <= 0 || clientWidth <= 0) {
    return;
  }
  
  // 滚动条轨道宽度
  const trackWidth = 40; // rpx
  
  // 动态计算滚动条宽度:基于可视区域与总内容的比例
  const contentRatio = Math.min(1, clientWidth / scrollWidth);
  const thumbWidth = Math.max(8, Math.min(trackWidth, trackWidth * contentRatio));
  const maxThumbMove = trackWidth - thumbWidth; // 可移动范围
  
  // 计算滚动比例
  const maxScrollLeft = scrollWidth - clientWidth;
  const scrollRatio = maxScrollLeft > 0 ? Math.min(1, scrollLeft / maxScrollLeft) : 0;
  
  // 计算滚动条位置
  const thumbPosition = maxThumbMove > 0 ? scrollRatio * maxThumbMove : 0;
  
  // 更新滚动条状态
  this.scrollLeft = Math.max(0, thumbPosition) + 'rpx';
  this.scrollThumbWidth = thumbWidth + 'rpx';
}

4. 初始化滚动条宽度

// 初始化滚动条宽度
initScrollbar() {
  this.$nextTick(() => {
    setTimeout(() => {
      // 获取scroll-view的尺寸信息
      uni.createSelectorQuery().in(this).select('.scroll-view').boundingClientRect((rect) => {
        if (rect && rect.width > 0) {
          // 获取scroll-view的scrollWidth
          uni.createSelectorQuery().in(this).select('.scroll-view').scrollOffset((scroll) => {
            const clientWidth = rect.width;
            const scrollWidth = scroll.scrollWidth || clientWidth;
            
            // 安全检查
            if (clientWidth > 0 && scrollWidth > 0) {
              // 计算初始滚动条宽度
              const trackWidth = 40;
              const contentRatio = Math.min(1, clientWidth / scrollWidth);
              const thumbWidth = Math.max(8, Math.min(trackWidth, trackWidth * contentRatio));
              
              this.scrollThumbWidth = thumbWidth + 'rpx';
            }
          }).exec();
        }
      }).exec();
    }, 100); // 增加延迟确保DOM完全渲染
  });
}

5. 生命周期调用

onReady() {
  // 页面渲染完成后初始化滚动条
  this.initScrollbar();
},

onShow() {
  // 每次显示页面时重新初始化滚动条
  this.initScrollbar();
}

CSS样式设计

1. 滚动容器样式

.scroll {
  margin-left: 20rpx;
  margin-right: 20rpx;
  background: white;
  border-radius: 20rpx;
  margin-top: 40rpx;
}

.scroll-view {
  padding-top: 15rpx;
  border-top-left-radius: 20rpx;
  border-top-right-radius: 20rpx;
  white-space: nowrap;
  height: 141rpx;
}

/* 隐藏scroll-view的默认滚动条 */
.scroll-view ::-webkit-scrollbar {
  display: none;
}

2. 自定义滚动条样式

/* 自定义滚动条样式 */
.custom-scrollbar-container {
  padding: 0rpx 40rpx 12rpx 40rpx;
  display: flex;
  justify-content: center;
}

.custom-scrollbar-track {
  width: 40rpx;
  height: 8rpx;
  background-color: #EAEAEA;
  border-radius: 4rpx;
  position: relative;
  overflow: hidden;
}

.custom-scrollbar-thumb {
  height: 8rpx;
  background-color: #0084CA;
  border-radius: 4rpx;
  transition: transform 0.15s ease-out, width 0.15s ease-out;
  position: absolute;
  top: 0;
  left: 0;
}

3. 滚动项样式

.scroll-view-item_H {
  display: inline-block;
  margin-right: 14rpx;
  margin-left: 20rpx;
}

.subtitle {
  height: 32rpx;
  font-size: 24rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
  color: #666666;
  margin: 20rpx 20rpx 10rpx 20rpx;
  white-space: pre-wrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

核心算法解析

1. 滑块宽度计算

滑块宽度的计算基于可视区域与总内容的比例关系

// 内容比例 = 可视区域宽度 ÷ 总内容宽度
const contentRatio = Math.min(1, clientWidth / scrollWidth);

// 滑块宽度 = 轨道宽度 × 内容比例
const thumbWidth = Math.max(8, Math.min(trackWidth, trackWidth * contentRatio));

设计思路:

  • 当内容较少时,滑块较宽,接近轨道宽度
  • 当内容较多时,滑块较窄,体现更多可滚动内容
  • 设置最小宽度8rpx,确保滑块始终可见
  • 设置最大宽度为轨道宽度,避免溢出

2. 滑块位置计算

滑块位置的计算基于当前滚动位置与最大滚动距离的比例

// 最大滚动距离 = 总内容宽度 - 可视区域宽度
const maxScrollLeft = scrollWidth - clientWidth;

// 滚动比例 = 当前滚动位置 ÷ 最大滚动距离
const scrollRatio = maxScrollLeft > 0 ? Math.min(1, scrollLeft / maxScrollLeft) : 0;

// 滑块最大移动距离 = 轨道宽度 - 滑块宽度
const maxThumbMove = trackWidth - thumbWidth;

// 滑块位置 = 滚动比例 × 滑块最大移动距离
const thumbPosition = maxThumbMove > 0 ? scrollRatio * maxThumbMove : 0;

3. 边界处理

为了确保滚动条的稳定性,需要处理各种边界情况:

// 1. 数据安全检查
if (scrollWidth <= 0 || clientWidth <= 0) {
  return;
}

// 2. 比例限制
const contentRatio = Math.min(1, clientWidth / scrollWidth);
const scrollRatio = maxScrollLeft > 0 ? Math.min(1, scrollLeft / maxScrollLeft) : 0;

// 3. 尺寸限制
const thumbWidth = Math.max(8, Math.min(trackWidth, trackWidth * contentRatio));

// 4. 位置限制
const thumbPosition = Math.max(0, thumbPosition);

性能优化

1. CSS过渡动画

为滑块添加平滑的过渡效果,提升用户体验:

.custom-scrollbar-thumb {
  transition: transform 0.15s ease-out, width 0.15s ease-out;
}

2. 查询优化

使用uni.createSelectorQuery()时添加.exec()确保查询执行:

uni.createSelectorQuery().in(this)
  .select('.scroll-view')
  .boundingClientRect((rect) => {
    // 处理结果
  })
  .exec(); // 确保查询执行

3. 异步处理

使用$nextTicksetTimeout确保DOM完全渲染后再进行计算:

this.$nextTick(() => {
  setTimeout(() => {
    // 执行滚动条初始化
  }, 100);
});

兼容性处理

1. 隐藏默认滚动条

不同平台的滚动条隐藏方式:

/* Webkit内核浏览器 */
.scroll-view ::-webkit-scrollbar {
  display: none;
}

/* 组件属性方式 */
<scroll-view :show-scrollbar="false">

2. 数值安全处理

处理可能的undefinednull值:

const scrollLeft = e.detail.scrollLeft || 0;
const scrollWidth = e.detail.scrollWidth || 0;
const clientWidth = e.detail.clientWidth || 343;

应用场景

这种自定义滚动条适用于以下场景:

  1. 导航菜单:水平滚动的功能菜单
  2. 图片轮播:自定义进度指示器
  3. 数据列表:水平滚动的数据表格
  4. 时间轴:可滚动的时间选择器

总结

本文详细介绍了UniApp中自定义滚动条的完整实现方案,包括:

  1. 结构设计:合理的HTML结构和组件层次
  2. 样式控制:精确的CSS样式定义
  3. 动态计算:基于比例的宽度和位置计算
  4. 事件处理:实时的滚动同步机制
  5. 性能优化:平滑的动画和高效的查询

通过这种方案,可以实现一个完全可控、响应灵敏的自定义滚动条,大大提升移动端应用的用户体验。