关于滚动条,有几个公式要先了解下:

scale 
= scrollBar.clientHeight / scrollBarThumb.clientHeight
= page.scrollHeight / page.clientHeight
// = (scrollBar.clientHeight - scrollBarThumb.clientHeight) / page.scrollTop

翻译下就是:

缩放比例 
= 滚动条高度(x) / 滑块高度(y)
= 页面总高度(a) / 页面可视区高度(b)
// = 滑块顶部距离(x-y) / 页面可视区顶部距离(a-b)

比如:

  • 比例是5
  • 滚动条高度是4,滑块高度是0.8
  • 页面总高度为25,页面可视区高度为5
  • 页面可视区顶部距离为25 - 5,滑块顶部距离为4 - 0.8
5 
= 25 / 5 
= 4 / 0.8 
!= (25 - 5) / (4 - 0.8) 

注意x/y等于a/b时,不一定等于x-y/a-b

另外,上述只是一个理想例子,比较好处理,如果你硬要滑块高度变小,也没问题。下面这个比例才是滑动时是真正要处理的:

页面可视区顶部最大距离(a - b, maxScrollTop) / 滑块顶部最大距离(x - y)
= 当前页面顶部距离scrollTop / 当前滑块顶部距离

还是以上面的数值为例,(25 - 5) / (4 - 0.8) 等于 6.25

  • 滑块顶部高度为1时,页面顶部距离应为 6.25
  • 滑块顶部高度为2时,页面顶部距离应为 6.25 * 2 = 12.5
  • 滑块顶部高度为3时,页面顶部距离应为 6.25 * 3 = 18.75
  • 滑块顶部高度为3.2时,页面顶部距离应为 6.25 * 3.2 = 20,此时页面在最底部,20 + 5 正好等于 25。

下面是基本的结构,一个scrollBar包住scrollBarThumb

<div
  v-show="scrollBarNum>2 && (showThis || isHoldDown)"
  ref="scrollBar"
  class="tip-match-scrollbar"
  :class="{'tip-match-scrollbar--active': isHoldDown}"
  :style="{height: `${scrollBarTotalHeight}px`}"
  @click.stop="onClickScrollBar"
>
  <div
    ref="scrollBarThumb"
    class="tip-match-scrollbar-thumb"
    :style="{transform:`translate3d(0,${scrollTranslateY}px,0)`, height: `${thumbHeight}px`}"
    @touchmove="onTouchMoveScrollBar"
    @touchstart="onTouchStartScrollBar"
    @touchend="onTouchEndScrollBar"
  >
    <div class="tip-match-team-member-tip">
      {{ +currentPageNum }}/{{ Math.ceil(scrollBarNum) - 1 }}页
    </div>
  </div>
</div>

获取scrollBarThumb拖动的距离:

// 触摸开始时:
startY = e.touches[0].clientY;
disY = startY - this.thumbTranslateY;
// 触摸滑动时:
const endY = e.touches[0].clientY;
let T = endY - disY; // 拖动距离

注意

  • clientY是鼠标相对于浏览器(这里说的是浏览器的有效区域)左上角y轴的坐标; 不随滚动条滚动而改变
  • offsetY是鼠标相对于事件源左上角Y轴的坐标

点击scrollBar时,thumb也应该跳到对应位置:

const { offsetY } = e;
this.thumbTranslateY = offsetY;

let scrollTop = offsetY * this.getPageBarScale();
pageScrollRef.scrollTo(pageScrollRef.scrollLeft, scrollTop);

下面是一些帮助方法:

// 页面顶部最大距离
getPageMaxTop() {
  const { pageScrollRef } = this;
  return pageScrollRef.scrollHeight - pageScrollRef.clientHeight;
},
// scrollBar顶部最大距离
getScrollBarMaxTop() {
  const { scrollBar, scrollBarThumb } = this.$refs;
  return scrollBar.clientHeight - scrollBarThumb.clientHeight;
},
// 修正滑块顶部高度
reviseThumbTop(T) {
  const maxTop = this.getScrollBarMaxTop();
  if (T < 0) {
    T = 0;
  } else if (T > maxTop) {
    T = maxTop;
  }
  return T;
},
// 页面顶部最大距离/scrollBar顶部最大距离
getPageBarScale() {
  const scale = this.getPageMaxTop() / this.getScrollBarMaxTop();
  return scale;
},
// 获取当前页数
getCurrentPageNum(scrollTop) {
  const { pageScrollRef } = this;
  this.currentPageNum = scrollTop === 0 ? 1 : Math.ceil(scrollTop / pageScrollRef.clientHeight) ;
},
// 让页面滚动
makePageScroll(scrollTop) {
  const { pageScrollRef } = this;
  if (!pageScrollRef) return;
  pageScrollRef.scrollTo(pageScrollRef.scrollLeft, scrollTop);
},

scrollBarThumb拖动时的几个事件:

let startY = 0;
let disY = 0;
let showThisTimer = null;
let holdDownTimer = null;

onTouchEndScrollBar() {
  clearTimeout(holdDownTimer);
  this.isHoldDown = false;
},
onTouchMoveScrollBar(e) {
  if (!this.isHoldDown) return;
  this.onShowThis();

  e.preventDefault();
  const endY = e.touches[0].clientY;

  let T = endY - disY;
  T = this.reviseThumbTop(T);

  this.scrollTranslateY = T;
  this.$forceUpdate();

  const scale = this.getPageBarScale();
  let scrollTop = T * scale;

  if (T === this.getScrollBarMaxTop()) {
    scrollTop = this.getPageMaxTop();
  }
  this.makePageScroll(scrollTop);

  this.getCurrentPageNum(scrollTop);
},
// 触发滚动条
onTouchStartScrollBar(e) {
  startY = e.touches[0].clientY;
  disY = startY - this.scrollTranslateY;

  clearTimeout(holdDownTimer);
  holdDownTimer = setTimeout(() => {
    this.isHoldDown = true;
  }, 200);
}

点击scrollBar的某一处时的事件:

onClickScrollBar(e) {
  this.clickScrollBar = true;
  this.onShowThis();

  const { offsetY } = e;

  this.scrollTranslateY = offsetY;

  let scrollTop = offsetY * this.getPageBarScale();
  const maxTop = this.getPageMaxTop();
  if (scrollTop > maxTop) {
    scrollTop = maxTop;
  }

  this.makePageScroll(scrollTop);

  setTimeout(() => {
    this.clickScrollBar = false;
  });
}

此外还有一个监听页面主体滚动的方法:

onScrollMain(e) {
  if (this.isHoldDown || this.clickScrollBar) return;
  this.onShowThis();

  const { scrollTop } = e.target;
  const scale = this.getPageBarScale();

  this.scrollTranslateY = scrollTop / scale;
  this.getCurrentPageNum(scrollTop);
}