仿照旧版支付宝生活服务模块-滚动定位 + 点击定位(上)

 

想要实现的效果:

  1. 顶部tab栏标题与下面列表标题对应,滚动超过tab栏高度,吸顶定位
  2. 向下滚动时,列表出现标题与顶部tab栏高亮标题对应
  3. 点击顶部tab栏标题,下面列表滚动到对应标题,且tab标题栏相应左右滚动到合适位置

 

具体效果如图:

仿照旧版支付宝生活服务模块-滚动定位 + 点击定位(上)

 

 

又到了说实现原理的时刻,让我想想哈,有了如下:

1.吸顶:

  给页面添加一个滚动监听事件,在里面动态获取tab栏到顶部的距离, 以及滚动距离,二者比较确定是否添加吸顶类名

2.滚动定位:

   由于顶部tab栏与下面列表栏都是用for循环生成的li标签,就可以通过遍历tab栏标题数组,当滚动距离+ 110(tab栏自身高度 + 到顶部距离) >= 列表中每个模块到顶部距离时,tab栏对应标题高亮,且左右滚动一段距离

3.点击定位:

  获取点击标题,与内容列表中标题做判断,相同时,对应列表中模块进行滚动操作,滚动距离为 = 列表模块到顶部距离 - 110(tab栏自身高度 + 到顶部距离)

4.设置列表高度:

  • 要想再点击最后一个标题,页面也滚动到最后列表模块的话,那就需要给列表设置足够的高度,有留白,好让他往上滚动
  • 计算公式 = 屏幕高度 + 列表中最后模块到顶部距离offsetTop - (110 + 列表中最后模块高度 + padding) 
  • 通过行内属性,动态添加高度,为了让页面一打开就设置好高度,需要在 mounted 钩子中添加以上计算

 tab栏横向滑动实现代码:

<!-- 标题栏 -->
    <header class="title_father">
      <ul :class="['title_list_box',{'titleFixed':Fixed}]" ref="titleRef">
        <li
          v-for="(item,index) in titleList"
          :key="index"
          class="title_list_item"
          :class="{active: tabChose == index}"
          @click="handleTab(index)"
        >
          <div :class="{active: tabChose == index}">{{item.title}}</div>
        </li>
      </ul>
    </header>

/************ 样式 ****************/
.title_father {
    height: 1rem;
    background-color: #fff;
    width: 100%;
    overflow: hidden;
    ::-webkit-scrollbar {
      display: none;
    }
    .title_list_box {
      padding-bottom: 0.2rem;
      height: 1rem;
      background-color: #fff;
      z-index: 5;
      display: -webkit-box;
      overflow-x: scroll;
      overflow-y: hidden;
      -webkit-overflow-scrolling: touch;
      border-bottom: 1px solid #f5f5f9;
      &.titleFixed {
        position: fixed;
        top: 1rem;
      }
      .title_list_item {
        height: 1rem;
        display: flex;
        align-items: center;
        margin: 0 0.25rem;
        font-size: 0.28rem;
        font-weight: 700;
        color: #333333;
        position: relative;
        &.active {
          color: #fd1a84;
          // border-bottom: 0.04rem solid #1d8ff3;
          &::after {
            content: "";
            position: absolute;
            width: 1.12rem;
            height: 0.04rem;
            background-color: #fd1a84;
            left: 0;
            bottom: 0.02rem;
          }
        }
        &:last-child {
          width: 1.7rem;
          border-bottom: none;
          position: relative;
          &.active {
            &::after {
              content: "";
              position: absolute;
              width: 1.12rem;
              height: 0.04rem;
              background-color: #fd1a84;
              bottom: 0.02rem;
            }
          }
        }
      }
    }
  }

 

核心代码主要是上面标红部分:

  • html结构是: 一个父div 包含 ul标签
  • 父div,width:100%(一般是保持等于屏幕宽度),设置超出隐藏: overflow: hidden;
  • 子元素ul 设置自适应布局:display:-webkit-box;(li元素全部排成一行),设置x轴滚动,y轴隐藏,以及滚动顺滑

 

    overflow-x: scroll;     overflow-y: hidden;     -webkit-overflow-scrolling: touch;

 

滚动定位 + 点击定位 实现代码:

html:

    <!-- 标题栏 -->
    <header class="title_father">
      <ul :class="['title_list_box',{'titleFixed':Fixed}]" ref="titleRef">
        <li
          v-for="(item,index) in titleList"
          :key="index"
          class="title_list_item"
          :class="{active: tabChose == index}"
          @click="handleTab(index)"
        >
          <div :class="{active: tabChose == index}">{{item.title}}</div>
        </li>
      </ul>
    </header>

    <!-- 内容列表 -->
    <ul
      :class="['content_list_box',{'contentFixed':Fixed}]"
      :style="{height:(screenHeight + lastTop - 260) + 'px'}"
    >
      <li
        v-for="(item,index) in contentBox"
        :key="index"
        class="content_list_item"
        @scroll="contentScroll(index)"
        ref="contentRef"
        @click="goPage(item)"
      >
        <div class="content_title" v-if="index" ref="contentBox">{{item.title}}</div>
        <div class="content_box">
          <div
            v-for="(subItem,subIndex) in item.contentList"
            :key="subIndex"
            class="content_item"
            @click="goLifePage(subItem,subIndex)"
          >
            <div class="item_inner" :class="{'selectStatus':isEdit}">
              <i
                :style="{background:`url(${require(`@/assets/images/life/index/${subItem.icon_name}.png`)}) no-repeat center/100%`}"
                class="icon"
              ></i>
              <p class="content_name">{{subItem.content_name}}</p>
              <i class="jia_icon" v-if="isEdit" :class="{'jianIcon':subItem.changeIcon}"></i>
            </div>
          </div>
        </div>
        <div class="line" v-if="index<contentBox.length-1"></div>
      </li>
    </ul>

 

js:

 data() {
    return {
      titleList: [
        {
          title: "视频娱乐",
          id: 0
        },
        {
          title: "卡密相关",
          id: 1
        },
        {
          title: "充值中心",
          id: 2
        },
        {
          title: "生活缴费",
          id: 3
        },
        {
          title: "车务交罚",
          id: 4
        },
        {
          title: "生活服务",
          id: 5
        }
      ], // tab标题数据
      tabChose: 0, // 高亮索引值
      contentBox: [
        {
          type: "videoEnt",
          title: "视频娱乐",
          contentList: [
            {
              type: "iqiyiVideo",
              icon_name: "aiqiyi@2x",
              content_name: "爱奇艺",
              changeIcon: false
            },
            {
              type: "tencentVideo",
              icon_name: "tengxunshipin@2x",
              content_name: "腾讯视频",
              changeIcon: false
            },
            {
              type: "youkuVideo",
              icon_name: "youkushipin@2x",
              content_name: "优酷视频",
              changeIcon: false
            }
          ]
        },
        {
          title: "卡密相关",
          contentList: [
            {
              type: "jingdongEcard",
              icon_name: "jingdongeka@2x",
              content_name: "京东E卡卡密",
              changeIcon: false
            },
            {
              type: "wangyiEcard",
              icon_name: "wangyi@2x",
              content_name: "网易严选",
              changeIcon: false
            }
            //   {
            //     icon_name: "weipinhui@2x",
            //     content_name: "唯品会卡密"
            //   },
            //   {
            //     icon_name: "xingbake@2x",
            //     content_name: "星巴克卡密"
            //   },
            //   {
            //     icon_name: "baiguoyuan@2x",
            //     content_name: "百果园卡密"
            //   },
            //   {
            //     icon_name: "shenzhouzhuanche@2x",
            //     content_name: "神州专车卡密"
            //   }
          ]
        },
        {
          title: "充值中心",
          contentList: [
            {
              type: "FlowRecharge",
              icon_name: "liuliang@2x",
              content_name: "流量充值",
              changeIcon: false
            },
            {
              type: "PhoneRecharge",
              icon_name: "huafei@2x",
              content_name: "话费充值",
              changeIcon: false
            }
          ]
        },
        {
          title: "生活缴费",
          contentList: [
            {
              type: "lifePayment",
              icon_name: "shuifei@2x",
              content_name: "水费查缴",
              changeIcon: false
            },
            {
              type: "lifePayment",
              icon_name: "dianfei@2x",
              content_name: "电费查缴",
              changeIcon: false
            },
            {
              type: "lifePayment",
              icon_name: "ranqi@2x",
              content_name: "燃气查缴",
              changeIcon: false
            }
          ]
        },
        {
          title: "车务交罚",
          contentList: [
            {
              type: "violationSearch",
              icon_name: "chaxun@2x",
              content_name: "违章查询",
              changeIcon: false
            },
            {
              type: "violationPay",
              icon_name: "jiaofei@2x",
              content_name: "违章缴费",
              changeIcon: false
            }
          ]
        },
        {
          type: "",
          title: "生活服务",
          contentList: [
            {
              type: "weather",
              icon_name: "zhiliang@2x",
              content_name: "天气预报",
              changeIcon: false
            },
            {
              type: "aqi",
              icon_name: "yubao@2x",
              content_name: "空气质量",
              changeIcon: false
            }
          ]
        }
      ], // 列表数据
      Fixed: false, // 是否吸顶
      scroll: 0, // y轴滚动距离
      screenHeight: document.body.clientHeight, //屏幕高度
      titleArr: "", // tab栏数组
      contentArr: "", // 列表栏数组
      titleBox: "", // tab栏父元素
      titleTop: 0, // tab栏到顶部的距离
      titleBoxHeight: 0, //标题栏的高度
      lastTop: 0, //最后一个内容距离顶部的高度
    };
  },
  mounted() {
    this.titleArr = document.querySelectorAll(".title_list_item");
    this.contentArr = document.querySelectorAll(".content_list_item");
    this.titleBox = document.querySelector(".title_list_box");
    window.addEventListener("scroll", this.handleScroll);
    // 获取屏幕的高度
    const that = this;
    window.onresize = () => {
      return (() => {
        window.screenHeight = document.body.clientHeight;
        that.screenHeight = window.screenHeight;
      })();
    };
    this.setContentHeight();
  },
   methods: {
    // tab栏切换
    handleTab(index) {
      this.tabChose = index;
      if (this.titleList[index].title === this.contentBox[index].title) {
        this.contentScroll(index);
      }
    },
    // 点击内容发生相应变化
    contentScroll(index) {
      // 获取每一个内容offsetTop高度
      let contentTop = this.contentArr[index].offsetTop;
      // 获取tab栏的高度
      this.titleBoxHeight =
        this.titleBox.offsetHeight || this.titleBox.clientHeight;
      window.scrollTo(0, contentTop - 110); // 55
    },
    //滚动监听
    handleScroll() {
      // 获取滚动距离
      this.scroll =
        document.documentElement.scrollTop || document.body.scrollTop;
      // 获取tab栏到顶部的距离
      if (document.querySelector(".title_list_box")) {
        this.titleTop = document.querySelector(".title_list_box ").offsetTop;
      }
      // 判断距离进行吸顶操作
      if (this.scroll > this.titleTop) {
        this.Fixed = true;
      } else {
        this.Fixed = false;
        this.tabChose = 0;
      }
      this.titleTop = 40;
      // 循环判断 滚动的距离 + 标题盒子的高度 >= 某条内容的高度时,设置该标题高亮,标题栏发生相应的滚动
      for (var i = 0; i < this.titleArr.length; i++) {
        if (this.scroll + this.titleTop * 2 >= this.contentArr[i].offsetTop) {
          // 1.5
          this.tabChose = i + 1;
          // scrollLeft代替scrollTo,解决安卓手机在微信浏览器中scrollTo事件失效
          document.querySelector(".title_list_box").scrollLeft = i * 30; // 25
        }
      }
    },
    // 动态设置内容总体提高
    setContentHeight() {
      this.lastTop = this.contentArr[5].offsetTop;
      console.log(this.contentArr[5].offsetHeight);
    },
   }

 

还有一部分样式代码,我就不在上传了,核心实现代码皆以上传

 

上一篇:数值分析--第三章--共轭梯度法


下一篇:leetcode_31【数学】---- x 的平方根