如何实现一个自定义日历滚动条

492 阅读3分钟

本文主要通过下面的实例来总结滚动条的相关知识,效果如下:

需要实现的功能有:

  1. 基本的滚轮滑动功能
  2. 左右两侧有箭头,点击一次滑动一屏
  3. 默认锚定值为第一项
  4. 滑动到边界时,对应侧的箭头消失

一、组件结构

<CalendarScrollBar> 组件的结构如下:

<template>
  <div class="scroll-bar">
    <arrow-left />
    <div class="scroll-bar__view"
        <ul class="scroll-bar__items">
          <li>
            <scroll-bar-item />
          </li>
        </ul>
      </div>
    <arrow-right />
  </div>
</template>
复制代码

二、组件功能

2.1 滚轮滑动

我们知道,滚动条的可见内容长度是小于实际的内容长度的。所以,当内容溢出边框时,需使用 overflow: hidden 对内容进行修剪、隐藏溢出。

.scroll-bar {
  overflow: hidden;
  .&__view {
    overflow: hidden;
  }
}
复制代码

效果如下:

可视元素 <div class="scroll-bar__view">

截屏2022-10-16 下午7.23.08.png

内容元素 <div class="scroll-bar__items">

截屏2022-10-16 下午7.23.27.png

2.2 点击箭头一次,切换一屏 tab

我们希望这个滚动条能实现:当点击左箭头时,滚动条会向左滑动,滑动的距离是一整个可视区的长度,如果此时到左边界的距离不满足一屏,那么滚动条会滑到左边界;同理,点击右箭头时,滚动条会向右滑动一整个可视区的长度,如果此时距离右边界不满足一屏,那么将滑到右边界。

思路:

这其实是一个平移计算问题。首先,我们使用 refoffsetWidth 来获取到可见区域 view 的长度内容区域 items 的长度,如下所示:

<div
  class="scroll-bar"
  ref="viewEl"
>
  ...
  <ul =
    class="scroll-bar__items"
    ref="itemsEl"
  >
  ...
  </ul>
</div>
复制代码
const viewEl = ref<HTMLElement | null>()
const itemsEl = ref<HTMLElement | null>()

const viewWidth = computed(() => unref(viewEl)?.offsetWidth ?? 0)
const itemsWidth = computed(() => unref(itemsEl)?.offsetWidth ?? 0)
复制代码

然后,定义变量 distance 来表示滑动位移,并给两侧的箭头添加点击事件,通过点击事件来改变 distance

<arrow-left @click="handleClick('left')" />
<arrow-right @click="handleClick('right')" />
复制代码
const distance = ref(0) // 滑动位移,初始默认 0 值

// 滑动位移的边界情况
const rangeDistance = (dis: number, offset: number) => {
  const res = dis + offset // 滑动后的位移
  const max = unref(itemsWidth) - unref(viewWidth)
  
  // 边界处理
  if (res < 0) return 0 // 位移最小值
  if (res > max) return max // 位移最大值
  return res
}

// 点击箭头,切换一屏
const handleClick = (arrow: 'left' | 'right') => {
  arrow === 'left'
    ? distance.value = rangeDistance(unref(distance), -unref(viewWidth))
    : distance.value = rangeDistance(unref(distance), unref(viewWidth))
}
复制代码

另外,由于可视区的宽度会随着浏览器窗口大小变化而变化,所以可以通过 watch 来监听resize

watch(() => unref(viewEl), () => {
  handleResize() // 记得作防抖处理
}, { immediate: true })
复制代码

2.3 滑动到边界时,对应侧箭头消失

最后,我们对边界情况进行处理,当滑动到边界时,对应侧的箭头将会消失。

很明显,箭头是否展示取决于 distance 的值。当值等于 0 时,说明此时滑动到了左边界,左箭头应不展示;当值等于 unref(itemsWidth) - unref(viewWidth) 时,说明此时滑动到了右边界,右箭头应不展示。

<arrow-left v-if="showLeft" ... />
<arrow-right v-if="showRight" ... />
复制代码
const showLeft = computed(() => !!unref(distance))
const showRight = computed(() => unref(distance) === (unref(itemsWidth) - unref(viewWidth))
复制代码

三、其他实践

3.1 点击滚动项,该项滑动到中间

首先,我们需要在 <CalendarScrollBar> 组件中添加一个 activeIndex 的 props,表示点击的是第几个滚动项。

<canlendar-scroll-bar
  :active-index="activeIndex"
/>
复制代码

然后,借助 scrollIntoView 函数来实现点击后滑到中间的效果,该函数用法可参考 Element.scrollIntoView

watch(() => unref(activeIndex), async (v) => {
  await nextTick()
  
  const target = unref(itemsEl)?.[v]
  if (target) {
    target.scrollIntoView({ behavior: 'smooth', inline: 'center' })
  }
}, { immediate: true })