小说阅读切换滑动组件

352 阅读3分钟

前言

前文——项目搭建
书接上回项目搭建了基础功能。开始分析需求开发组件。本项目是模仿开发阅读的H5网站,参考的页面和业务逻辑来源于七猫小说App,掌阅小说App

分析App首页

image.png

image.png 两个App的首页都是有一个左右滑动页面轮播页面的功能。我们怎么实现这个功能呢? 设想:

  1. 使用scroll-X滚动实现
  2. 使用transtrom
  3. postion属性之后left/right 根据上面的设想去实践完成,组件页面布局

image.png

开始设计组件
1.红色区域为视图显示区域
2.橙色区域为内容区域
3.其他颜色为不同页面内容显示区域
创建组件 红色为全屏视图组件 蓝色为单独页面 image.png 在Vue3中无法通过children获取子组件是不是swipeInfo,children获取子组件是不是`swipeInfo`,`parent 属性获取父组件了,学习饿了么Plus后发现可以使用provide,inject`,通过相互约定放入(可能有更好的方案,希望大佬们勿喷

// 父组件 swipe-view
provide("setSwiper", children.value);
// 子组件
const setSwipe: any = inject("setSwiper");
const _this = getCurrentInstance();
setSwipe.push(_this);

开始写功能

组件内部使用transtrom作为滑动主要功能 获取需要滚动的dom元素

// 获取dom 绑定元素
const titleListDom: Ref<HTMLDataElement[]> = ref([]);

由于需要动态改Style,

// tab内容滚动的距离
const transFormX = computed(() => {
  const {swiperSet} = toRefs(swipeProxy)
  console.log('swiperSet', swiperSet.value.positionX)
  return {
    transform: `translateX(${swiperSet.value.positionX}%) translateY(0)`
  }
});
// tabTitle滑块样式
const sliderStyle = computed(() => {
  const {direction, sliderSet} = toRefs(swipeProxy);
  const {positionX, startX} = toRefs(sliderSet.value)
  console.log(positionX, startX)
  return {
    width: startX.value == 0 ? undefined : `${startX.value}px`,
    left: `${direction.value === "left" ? positionX.value + 'px' : 'unset'}`,
    right: `${direction.value === "right" ? positionX.value + 'px' : 'unset'}`,
  };
});

记录滑动组件内部数据

// 设置的元素值
const swipeProxy = reactive({
  swiperIndex: 0, // 标识获取当前的位置
  swiperSet: {    // view 滑动设置
    startX: 0,
    startY: 0,
    positionX: 0,
  },
  sliderSet: {    // title 滑动记录
    startX: 0,
    positionX: 0,
    defaultWidth: 0
  },
  startTime: new Date(),
  direction: "left",   //滑动方向
  distanceRatio: 0,    // 滑动占比
});

绑定事件

onMounted(() => {
  let touchFlag = false;  // 标识是否是长按
  // 设置初始化参数
  const touchDom = touchMain.value;
  const touchWidth = touchDom.offsetWidth;
  const sliderWidth = getBoundingClientRect(sliderMain.value).width;
  const startTitleDom = titleListDom.value[swipeProxy.swiperIndex]
  swipeProxy.sliderSet.startX = sliderWidth
  swipeProxy.sliderSet.defaultWidth = sliderWidth
  swipeProxy.sliderSet.positionX = getSliderPosition(startTitleDom).left;

  function touchStart(event: TouchEvent) {
    touchFlag = true; //设置长按不动
    const touch = event.touches[0]; //获取第一个触点
    const startX = Number(touch.pageX); //页面触点X坐标
    const startY = Number(touch.pageY); //页面触点Y坐标
    const startTime = new Date();
    swipeProxy.swiperSet = Object.assign(swipeProxy.swiperSet, {
      startX,
      startY,
    });
    swipeProxy.distanceRatio = 0;
    swipeProxy.startTime = startTime;
  }

  function touchMove(event: TouchEvent) {
    if (!touchFlag) return;
    const touch = event.touches[0]; //获取第一个触点
    const moveX = Number(touch.pageX); //页面触点X坐标
    const distance = moveX - swipeProxy.swiperSet.startX;
    const direction = distance > 0 ? "right" : "left";
    const lastIndex = childrenLength.value - 1
    // 在第一屏幕左滑动无效
    if (!swipeProxy.swiperIndex && direction === "right") return;
    // 在最后一屏幕右滑动无效
    if (swipeProxy.swiperIndex === lastIndex && direction === "left") return;
    //记录滑动方向
    swipeProxy.direction = direction;
    requestAnimationFrame(() => {
      // 换算出占比
      const Ratio = distance / touchWidth;
      // 计算出滑动距离
      let transformX = (swipeProxy.swiperIndex + Ratio) * 100;
      const lastPositionX = -lastIndex * 100
      if (transformX >= 0) transformX = 0;
      else if (transformX <= lastPositionX) transformX = lastPositionX;
      swipeProxy.swiperSet.positionX = transformX;
      swipeProxy.distanceRatio = Math.abs(Ratio);
      // 设置顶部title滚动位置
      const titleIndex = Math.abs(swipeProxy.swiperIndex);
      // 获取到要滚动到下一个的位置
      const nextIndex = direction === "right" ? titleIndex - 1 : titleIndex + 1;
      // 计算title之间的距离
      const currentTitleDom = getSliderPosition(titleListDom.value[titleIndex]);
      const nexTitleDom = getSliderPosition(titleListDom.value[nextIndex]);
      // (currentTitleDom.width + nexTitleDom.width) / 2  两个中心点的距离
      // sliderWidth title 滑块的宽度
      // 参考margin做垂直水平居中
      const allNum = (currentTitleDom.width + nexTitleDom.width) / 2 + sliderWidth
      const sliderDistance = swipeProxy.distanceRatio * allNum;
      // 获取元素的宽度
      const startX = sliderWidth + sliderDistance;
      swipeProxy.sliderSet.startX = startX > allNum ? allNum : startX
      const position = getSliderPosition(titleListDom.value[titleIndex]);
      swipeProxy.sliderSet.positionX = position[direction]
    });
  }

  function touchEnd() {
    if (swipeProxy.distanceRatio >= 0.3) {
      swipeProxy.direction === "right" ? swipeProxy.swiperIndex++ : swipeProxy.swiperIndex--;
      swipeProxy.direction = swipeProxy.direction === "right" ? 'left' : 'right'
    }
    requestAnimationFrame(() => {
      console.log(swipeProxy.swiperIndex * 100)
      swipeProxy.swiperSet.positionX = swipeProxy.swiperIndex * 100;
      const position:any = getSliderPosition(titleListDom.value[Math.abs(swipeProxy.swiperIndex)]);
      swipeProxy.sliderSet.positionX = position[swipeProxy.direction]
      requestAnimationFrame(() => {
        swipeProxy.sliderSet.startX = sliderWidth;
      })
      touchFlag = false;
    });
  }

  touchDom.addEventListener("touchstart", touchStart);
  touchDom.addEventListener("touchmove", touchMove);
  touchDom.addEventListener("touchend", touchEnd);
});

左右滑动组件基本样子已经完成,后续写的样式我也会提交到这个仓库里
gitee.com/tongbinzuo/…

总结

  1. 这个确实让我学习了不少Vue3的知识
  2. 对transform 也有了新的认识
  3. 代码中肯定有不合理的地方,大神勿喷。
  4. 后面我会继续更新的,有什么问题大家多多评论