草庐IT

微信小程序scroll-view锚点定位+界面滑动改变tab(防京东详情页)

Allie1995 2023-04-12 原文

这几天在开发这个锚点定位的需求本来感觉很简单,结果做起来发现很多坑,有的根本就注意不到,真的是很让人头大:下面是几个大坑

1、scroll-view的坑

  1.  scroll-view要增加高度,有的时候计算高度赋值给scroll-view时,会不起效,这个时候直接给它赋值100VH 就可以了
  2. scroll-view在使用的时候,如果需要使用禁止滑动,需要直接 scroll-y直接删掉或者定义成非布尔值类型

2、wx.createSelectorQuery()获取值不正确的问题

因为做的类似与京东的商品详情页,那么可定会出现图片以及富文本解析显示的。如果这个时候我们使用小程序自带的生命周期(onLoad、onShow、onReady),你会发现我们获取的节点信息要么不正确要么为null,这个全靠脸。

之后我就想着看能不能做一个延迟在onReady中去获取,这个时候还是会出现获取值不正确的问题,因为图片的一般我们都是自适应的,没有给他固定高度,由于不确定性这个方法也被淘汰了。

最后,请教了我们公司的同事,我们发现在小程序onReady之后,富文本解析的内容以及商品图片未必渲染完成,所以就出现上面那种情况。所以这个时候就想这个节点获取要放到图片渲染完毕以及富文本解析渲染之后。然后我们就找到了mp-html组件:

组件地址:mp-html: 小程序富文本组件,支持渲染和编辑 html,支持在微信、QQ、百度、支付宝、头条和 uni-app 平台使用 (gitee.com)

这个里面具备了渲染解析后的监听,具体的大家可以看一下。

3、锚点位置位于scroll-view 最下方,也就是锚点位置过低不能到达顶部的情况

  界面上肯定会出现这种情况,就是最后一个或者最后几个锚点的位置过低,不能根据点击使锚点到达顶部的情况。这个如果单独使用没什么问题,但是如果你配合scroll-view的滑动去改变tab的状态时就会出现问题,它会回弹,这就很让人头大,最后只能计算,再配合scroll-view 的binddragend使用,具体的处理方法一会在代码里我会说明

4、锚点获取紧挨这tab

  这个是界面上的问题,我发现如果我给这个view增加了margin-top,而它之上的盒子增加了一个margin-bottom时,锚点点击定位的点就是这个margin-bottom 的点,所以会出现这种紧挨着的问题。这个时候只需要把这个锚点盒子的margin改成padding就可以了

好了接下来就是代码了:(需要锚点的界面index.wxml)

<!-- 这是tab的组件 -->
<view class="navigateBox flexCenter" wx:if="{{isHidden}}">
  <nav-tabs list="{{title}}" active="{{activeIndex}}" customStyle="width:560rpx;margin:auto;" bind:change="jumpTo"></nav-tabs>
</view>


<scroll-view style="width:100%;height:100vh" scroll-into-view="{{toView}}"   scroll-y="{{isScroll}}" scroll-with-animation="true" enhanced="true" bindscroll="toScroll"
        binddragend="endScroll"  >
  <view id="product">
      <!-- 内容按需求 -->
  </view>
  <view id="store">
      <!-- 内容按需求 -->
  </view>
  <view id="info">
      <!--mp-html为富文本解析器,可以按照上面的路径去下载使用 -->
     <mp-html content="{{couponDetail.content}}"  bindready="mpHtmlReady"/>
  </view>

</scroll-view >

这里是需要锚点的index.js文件

data:{
    toView: '',
    activeIndex: 0,
   title: [{ name: "商品", opt: 'product', type: 0 }, { name: "门店", opt: 'store', type: 1 }, { name: "商品详情", opt: 'info', type: 2 }],
    isHidden: false,
    scrollLock:false,//滑动枷锁
    isScroll:'',
},

...

//富文本解析渲染完成  
mpHtmlReady(e){
    this.slideAnchor();
},

//获取锚点的节点信息
slideAnchor(e){
   new Promise(resolve => {
      let query = wx.createSelectorQuery().in(this);
      query.select('#product').boundingClientRect();
      query.select('#store').boundingClientRect();
      query.select('#info').boundingClientRect();
      query.exec(function (res) {
        resolve(res);
      });
    }).then(res => {
      const windowHeight = wx.getSystemInfoSync().windowHeight;
      let heightArray= [],topArray= [];
      res.forEach(rect=> {
         heightArray.push(Math.floor(rect.top)); 
         topArray.push(rect.height)});
      this.setData({
        scrollHeight:windowHeight ,
        heightArray,
        topArray,
        scrollLock:true, 
        isScroll:true, //在富文本未解析之前,先禁止界面滑动
      });
    });
},

//点击锚点跳转
  jumpTo: function (e) {
    let target = e.detail.item.opt;
    let activeIndex = e.detail.item.type;
    let  {topArray,scrollHeight,isHidden} = this.data;
    let numHeight =0;
    //计算当前锚点是否能根据tab的点击至顶部
    for(var i =activeIndex; i<topArray.length;i++){
      numHeight +=  topArray[i]
    }
    isHidden =  target=='product'?false:isHidden;
    this.setData({
      toView: target,
      activeIndex,
      isHidden,
      scrollLock:numHeight>=scrollHeight?true:false //如果界面出现锚点位置过低的情况防止tab的active回弹
    })
  },

  //scroll-view滚动监听事件
  toScroll: function (e) {
    const { heightArray,scrollLock,toView} = this.data;
    let scrollTop = e.detail.scrollTop;
    let isHidden = scrollTop > 20 ? true : false; //控制tab显示与隐藏
    if (this.data.isHidden != isHidden) {this.setData({ isHidden, toView: '', activeIndex:0, })}
    //锚点高度足够时,姐买你滑动到相应的位置,tab的active发生相应的改变
    if(scrollLock){
        let len = heightArray.length;
        let lastIndex = len - 1;
        let activeIndex = 0;
        for (let i = 0; i < len; i++) {
          if (scrollTop >= 0 && scrollTop < heightArray[0]) {
            activeIndex = 0;
          } else if (scrollTop >= heightArray[i] && scrollTop < heightArray[i + 1]) {
            activeIndex = i;
          }else if(scrollTop>=heightArray[lastIndex]){
            activeIndex=lastIndex;
          } 
        }
        this.setData({ activeIndex })
    }
  },

//停止滚动,防止锚点位置过低,界面滚动时无效的情况
  endScroll(e){
    this.setData({
      scrollLock:true
    });
  },

这里是需要锚点的index.wxss文件

.navigateBox{
  width:100%;
  position: sticky;
  height:92rpx;
  background:#fff;
  z-index:8;
  top:0rpx;
  left:0rpx;
  border-top:2rpx solid #F2F2F2;

}
.navigateBox .menus {
  position: relative;
  display: inline-block;
  height: 92rpx;
  line-height: 92rpx;
  width: auto;
  padding: 0 24rpx;
  font-size: 28rpx;
  color: #7F7F7F;
}

这个是nav-tabs组件的.wxml

<!--
	customStyle 自定义css样式 
	scrollable 是否横向滚动
	lineOffsetLeft 当前选择的线条偏移量
	lineWidth 线条的宽度
  scrollable 是否滚动
-->

<view class="cl-tabs" style="{{customStyle}}">
     <scroll-view class="scroll-view scrollHeight" scroll-x="{{ scrollable }}" scroll-with-animation  scroll-left="{{ scrollLeft }}">
          <view class="cl-tabs-nav {{scrollable ? 'cl-tabs-scroll' : 'cl-tabs-noscroll'}}">
               <view class="cl-tabs__line"
                    style="width:{{lineWidth}}px;transform:translateX({{lineOffsetLeft}}px);-webkit-transform:translateX({{lineOffsetLeft}}px);transition-duration:0.3s;-webkit-transition-duration:0.3s">
               </view>
               <view wx:for="{{list}}" data-index="{{index}}" class="cl-tab {{currentIndex==index ? 'cl-tab-active' : ''}}" data-item="{{item}}" data-opt="{{item.opt}}" catchtap="swich">
                    <text>{{item.name}}</text>
               </view>
          </view>
     </scroll-view>
</view>

这个是nav-tabs组件的.js


const tools = require('../../utils/tools');
Component({
     properties: {
          list: {
               type: Array,
               value: []
          },
          customStyle: {
               type: String,
               value: ''
          },
     
          active:{
               type:Number,
               value:0,
               observer(val) {
                 this.change(val);
               }
          },
          scrollable: {
               type: Boolean,
               value: true
          }
     },
     data: {
          scrollLeft: 0,
          currentIndex: 0,
          lineOffsetLeft: 0,
          lineWidth: 14
     },
     lifetimes: {
     //在组件在视图层布局完成后执行
          ready: function () {
               let that = this;
               this.setData({
                    currentIndex:this.data.active,
                    scrollable: this.data.list.length <= 5 ? false : true
               }, function () {
                    const currentIndex = that.data.currentIndex;
                    Promise.all([
                      tools.getAllRect(this, '.cl-tab'),
                      tools.getRect(this, '.cl-tabs__line'),
                    ]).then(([rects = [], lineRect]) => {
                         const rect = rects[currentIndex];
                         if (rect == null) {
                              return;
                         }
                         let lineOffsetLeft = rects.slice(0, currentIndex).reduce((prev, curr) => prev + curr.width, 0);
                         lineOffsetLeft += (rect.width - lineRect.width) / 2;
                         that.setData({
                              lineOffsetLeft
                         });
                    });
               });
          }
     },
     methods: {
     //切换组件
          swich: function (e) {
               const { data } = this;
               const currentIndex = e.currentTarget.dataset.index;
               const item = e.currentTarget.dataset.item;
               if (currentIndex === data.currentIndex) {
                    return;
               }
               const shouldEmitChange = data.currentIndex !== null;
               this.setData({ currentIndex });
               tools.nextTick(() => {
                    this.resize(false);
                    this.scrollIntoView();
                    if (shouldEmitChange) {
                    //绑定事件到change
                         this.triggerEvent('change', {
                              index: currentIndex,
                              item:item
                         })
                    }
               });
          },
          change:function(index){
               this.setData({
                    currentIndex:index
               },()=>{
                    this.resize(false);
                    this.scrollIntoView();   
               })
          },
          resize() {
               const { currentIndex } = this.data;
               Promise.all([
                tools.getAllRect(this, '.cl-tab'),
                tools.getRect(this, '.cl-tabs__line'),
               ]).then(([rects = [], lineRect]) => {
                    const rect = rects[currentIndex];
                    if (rect == null) {
                         return;
                    }
                    let lineOffsetLeft = rects.slice(0, currentIndex).reduce((prev, curr) => prev + curr.width, 0);
                    lineOffsetLeft += (rect.width - lineRect.width) / 2;
                    this.setData({
                         lineOffsetLeft
                    });
               });
          },
          scrollIntoView() {
               const { currentIndex, scrollable } = this.data;
               if (!scrollable) {
                    return;
               }
               Promise.all([
                tools.getAllRect(this, '.cl-tab'),
                tools.getRect(this, '.cl-tabs-nav'),
               ]).then(([tabRects, navRect]) => {

                    const tabRect = tabRects[currentIndex];
                    const offsetLeft = tabRects
                         .slice(0, currentIndex)
                         .reduce((prev, curr) => prev + curr.width, 0);
                    this.setData({
                         scrollLeft: offsetLeft - (navRect.width - tabRect.width) / 2,
                    });
               });
          },
     }
})

<!-- 其中tools的内容 -->
<!--
  const nextTick=cb=> {
    if (wx.canIUse('nextTick')) {
        wx.nextTick(cb);
    }
    else {
        setTimeout(() => {
            cb();
        }, 1000 / 30);
    }
  }
  const getRect=(context, selector) =>{
   return new Promise((resolve) => {
       wx.createSelectorQuery()
           .in(context)
           .select(selector)
           .boundingClientRect()
           .exec((rect = []) => resolve(rect[0]));
   });
  }
  
  const getAllRect=(context, selector) =>{
   return new Promise((resolve) => {
       wx.createSelectorQuery()
           .in(context)
           .selectAll(selector)
           .boundingClientRect()
           .exec((rect = []) => resolve(rect[0]));
   });
  }

module.exports = {nextTick,getRect,getAllRect}
-->

这个是nav-tabs组件的.wxss


.cl-tabs{
  background-color: #ffffff;
}

.cl-tabs-scroll {
  position: relative;
  user-select: none;
  white-space: nowrap;
}
.cl-tabs-noscroll{
  display: -webkit-flex;
  display: flex;
  overflow: hidden;
  
}

.cl-tab {
  position: relative;
  min-width: 0;
  height:86rpx;
  width:150rpx;
  /* padding: 0 16rpx; */
  text-align: center;
  color: #7F7F7F;
  font-size: 30rpx;
  line-height: 86rpx;
  background:#fff;
}
.cl-tabs-scroll .cl-tab{
  display: inline-block;
  /* padding:0 24rpx; */
}
.cl-tab text{
  display: block;
  width:100%;
  height:86rpx;
}
.cl-tabs-noscroll .cl-tab{
  flex: 1;
}
.cl-tab:active{
   background:#fff!important;
}
.cl-tab-active{
  font-weight: bold;
  color:#333333;
}
.cl-tabs__line {
  position: absolute;
  bottom:14rpx;
  left: 0;
  z-index: 1;
  height: 6rpx;
  background: linear-gradient(90deg, #FF9973 0%, #FF4F4F 100%);
  border-radius: 6rpx;
}
.scrollHeight{
  height:90rpx;
}

以上就是这个功能的全部过程了,真的是很心酸。目前是这个情况,但是感觉代码还有有点不够简练,也希望各位大佬看看能不能改进优化一下,在此谢过。

有关微信小程序scroll-view锚点定位+界面滑动改变tab(防京东详情页)的更多相关文章

  1. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  2. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  3. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  4. ruby-on-rails - 如何在 Rails View 上显示错误消息? - 2

    我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c

  5. ruby CSV : How can I read a tab-delimited file? - 2

    CSV.open(name,"r").eachdo|row|putsrowend我得到以下错误:CSV::MalformedCSVErrorUnquotedfieldsdonotallow\ror\n文件名是一个.txt制表符分隔文件。我是专门做的。我有一个.csv文件,我转到excel,并将文件保存为.txt制表符分隔的文件。所以它是制表符分隔的。CSV.open不应该能够读取制表符分隔的文件吗? 最佳答案 尝试像这样指定字段分隔符:CSV.open("name","r",{:col_sep=>"\t"}).eachdo|row|

  6. ruby-on-rails - 复数 for fields_for has_many 关联未显示在 View 中 - 2

    目前,Itembelongs_toCompany和has_manyItemVariants。我正在尝试使用嵌套的fields_for通过Item表单添加ItemVariant字段,但是使用:item_variants不显示该表单。只有当我使用单数时才会显示。我检查了我的关联,它们似乎是正确的,这可能与嵌套在公司下的项目有关,还是我遗漏了其他东西?提前致谢。注意:下面的代码片段中省略了不相关的代码。编辑:不知道这是否相关,但我正在使用CanCan进行身份验证。routes.rbresources:companiesdoresources:itemsenditem.rbclassItemi

  7. 「Python|Selenium|场景案例」如何定位iframe中的元素? - 2

    本文主要介绍在使用Selenium进行自动化测试或者任务时,对于使用了iframe的页面,如何定位iframe中的元素文章目录场景描述解决方案具体代码场景描述当我们在使用Selenium进行自动化测试的时候,可能会遇到一些界面或者窗体是使用HTML的iframe标签进行承载的。对于iframe中的标签,如果直接查找是无法找到的,会抛出没有找到元素的异常。比如近在咫尺的例子就是,CSDN的登录窗体就是使用的iframe,大家可以尝试通过F12开发者模式查看到的tag_name,class_name,id或者xpath来定位中的页面元素,会抛出NoSuchElementException异常。解决

  8. 微信小程序通过字典表匹配对应数据 - 2

    前言一般来说,前端根据后台返回code码展示对应内容只需要在前台判断code值展示对应的内容即可,但要是匹配的code码比较多或者多个页面用到时,为了便于后期维护,后台就会使用字典表让前端匹配,下面我将在微信小程序中通过wxs的方法实现这个操作。为什么要使用wxs?{{method(a,b)}}可以看到,上述代码是一个调用方法传值的操作,在vue中很常见,多用于数据之间的转换,但由于微信小程序诸多限制的原因,你并不能优雅的这样操作,可能有人会说,为什么不用if判断实现呢?但是if判断的局限性在于如果存在数据量过大时,大量重复性操作和if判断会让你的代码显得异常冗余。wxswxs相当于是一个独立

  9. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  10. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

随机推荐