自定义滚动监听

251 阅读6分钟

自定义滚动条组件

场景:在微信小程序开发中,默认的滚动条已经非常完美了,不管是scroll-view还是默认的。但是还是会有些情况会使用到一些定制的滚动条,所以我就稍稍学习整理了一下,关于我对自定义滚动条的理解思路。

实例:

京东.gif

就像上面的那种,当我们滚动的时候,需要有一个标识块,用于表示当前滚动的位置。

废话不多说,上代码---

页面

思路 - 布局,样式什么的我就不说了,主要讲一下思路

通过获取容器宽度,总宽度,滚动的距离

利用计算 滚动距离 / (总宽度 - 容器宽度)可以获取滚动的比例 (0 ~ 1)

因为不管怎么说,总宽度 - 容器宽度 总是等于最大滚动距离的

也就是说,当我不滚动的时候,滚动距离比例是0,当我滚动最大的时候,滚动距离比例是1

所以我们就可以根据这个比例进行操作了,只不过比例计算我是放到组件里去了。

demo

<!--demo.wxss - 这里我使用的是微信小程序的scroll-view组件 -->
<view class="scroll_wrap">
        <scroll-view scroll-x style="overflow: auto;" bindscroll="onscroll" id="scroll">
          <view style="width: calc({{bagWidth}} * {{assets.length}});white-space: nowrap;" id="scroll_wrap">
            <view wx:for="{{assets}}" wx:key="id" class="item" style="width: {{bagWidth}}px;">
              <view class="price flex flex-justify-center flex-items-center margin-auto">{{item.number}}</view>
              <view class="title">
                <view>{{item.title}}</view>
                <view>{{item.remark}}</view>
              </view>
            </view>
          </view>
        </scroll-view>
      </view>
// 上面用到一些flex,flex-justify-center。。。不用在意,相信也都明白,这是全局样式设置的,类似于bootstrap,这里就不多介绍了。

demo.js

Page({
  data: {
    bagWidth:0,
    // 数据
    assets:[
      {id:1,number:"1",title:"优惠券",remark:""},
      {id:2,number:"4.79万",title:"白条分分卡",remark:"额度待领取"},
      {id:3,number:"0",title:"京豆",remark:""},
      {id:4,number:"0",title:"红包",remark:""},
      {id:5,number:"20.00万",title:"金条借款",remark:"白用30天"},
    ],
    // 传入组件参数  
    scrollObj:{
      scrollWidth:String,
      totalWidth:String,
      scrollLeft:String,
    }
  },
  // 滚动距离
  onscroll(e){
    const that = this
    wx.createSelectorQuery().select('#scroll').boundingClientRect(function(res){
      // 容器宽度
      let scrollWidth = res.width
      // 总宽度 = 块宽度 * 数量
      let cardWidth = that.data.bagWidth
      let number = that.data.assets.length
      let totalWidth = cardWidth * number
      // 滚动距离
      let scrollLeft = e.detail.scrollLeft
      let scrollObj = {
        scrollWidth,
        totalWidth,
        scrollLeft
      }
      that.setData({scrollObj})
    }).exec()
  },
  /**
   * 生命周期函数--监听页面加载
   */
  /* 这一步等会介绍 */
  onLoad(options) {
    const that = this
    // 获取bag宽度
    wx.createSelectorQuery().select('#bag').boundingClientRect(function(res){
      let bagWidth = res.width
      that.setData({bagWidth})
    }).exec()
  },
})
// 思路:
// 1.利用wx.createSelectorQuery()方法获取容器宽度
// 2.通过计算出总宽度
// 3.利用scroll-view组件中自带的bindscroll事件获取滚动的左边距

demo.wxss

/* 模块3开始 */
.personal_module3{
  background-color: #F6F6F6;
  /* height: 200px; */
}
/* 全部资产容器 */
.personal_module3 .assets{
  background-color: #fff;
  display: flex;
  justify-content: space-between;
}
.personal_module3 .scroll_wrap{
  width: 80%;
}
​
.personal_module3 .item{
  display: inline-block;
  text-align: center;
  padding-bottom: 30rpx;
}
.personal_module3 .item .price{
  width: 26px;
  height: 26px;
  font-size: 16px;
  font-weight: 700;
}
.personal_module3 .item .title{
  position: relative;
}
.personal_module3 .item .title>view:nth-of-type(1){
  font-size: 11px;
}
.personal_module3 .item .title>view:nth-of-type(2){
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  bottom: -26rpx;
  font-size: 10px;
  color: #999;
}
.personal_module3 .money_bag{
  width: 20%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.personal_module3 .money_bag>view:nth-of-type(1){
  width: 26px;
  height: 26px;
  
}
.scroll_bar{
  background-color: #999;
  height: 10px;
  width: 20%;
  margin: auto;
}
.scroll_position{
  background-color: #f00;
  width: 30%;
  height: 10px;
}
/* 模块3结束 */

demo.json - 使用组件

{
  "usingComponents": {
    "progress-bar":"/components/progress-bar/progress-bar"
  }
}

组件 - progress-bar

组件思路很简单

设定好两个块,一个块是背景,另一个块表示进度位置

根据传入的参数,计算出当前滚动的比例

然后根据比例移动内部块的位置

js

// components/progress-bar/progress-bar.js
Component({
  properties: {
    message:{
      type:Object,
      value:{}
    }
  },
  // 监听参数变化
  observers:{
    'message':function(newVal,oldVal){
      let {scrollLeft , scrollWidth , totalWidth} = newVal
      // 滚动比例 = 滚动距离 / (总宽度 - 容器宽度)
      let result = scrollLeft / (totalWidth - scrollWidth)
      if(!isNaN(result)){
        this.setData({result})
      }
    }
  },
  data: {
    result:0
  },
})

json

{
  "component": true,
  "usingComponents": {}
}

wxml

<!--components/progress-bar/progress-bar.wxml-->
<view class="wrap">
  <view class="opsition" style="margin-left: calc(35rpx * {{result}});"></view>
</view>

wxss

/* components/progress-bar/progress-bar.wxss */
.wrap{
  width: 60rpx;
  height:10rpx;
  background-color: #ccc;
  border-radius: 20px;
  overflow: hidden;
}
.opsition{
  width: 25rpx;
  height: 10rpx;
  background-color: #f00;
  border-radius: 20px;
}

成品:

demo.gif

讲解全部代码

页面

 <!-- 模块3 全部资产 -->
  <view class="personal_module3 px pt pb">
    // 外围容器 没有给固定宽高 但是设置了flex布局
    <view class="assets border-r-m py flex flex-items-center">
      <view class="scroll_wrap">
        <scroll-view scroll-x style="overflow: auto;" bindscroll="onscroll" id="scroll">
          // bagWidth就是“全部资产”的宽度
          <view style="width: calc({{bagWidth}} * {{assets.length}});white-space: nowrap;" id="scroll_wrap">
            <view wx:for="{{assets}}" wx:key="id" class="item" style="width: {{bagWidth}}px;">
              <view class="price flex flex-justify-center flex-items-center margin-auto">{{item.number}}</view>
              <view class="title">
                <view>{{item.title}}</view>
                <view>{{item.remark}}</view>
              </view>
            </view>
          </view>
        </scroll-view>
      </view>
      // 设置宽度为容器宽度的20%,确保显示部分保证为五个块
      <view class="money_bag" id="bag">
        <view><image style="height: 100%;width: 100%;" src="/static/personal/images/钱包.png" mode=""/></view>
        <view class="mt-1">全部资产</view>
      </view>
    </view>
    // 使用组件
    <view>
      <progress-bar message="{{scrollObj}}"></progress-bar>
    </view>
  </view>

wxss

/* 模块3开始 */
.personal_module3{
  background-color: #F6F6F6;
  /* height: 200px; */
}
/* 全部资产容器 */
.personal_module3 .assets{
  background-color: #fff;
  display: flex;
  justify-content: space-between;
}
.personal_module3 .scroll_wrap{
  width: 80%;
}
​
.personal_module3 .item{
  display: inline-block;
  text-align: center;
  padding-bottom: 30rpx;
}
.personal_module3 .item .price{
  width: 26px;
  height: 26px;
  font-size: 16px;
  font-weight: 700;
}
.personal_module3 .item .title{
  position: relative;
}
.personal_module3 .item .title>view:nth-of-type(1){
  font-size: 11px;
}
.personal_module3 .item .title>view:nth-of-type(2){
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1;
  bottom: -26rpx;
  font-size: 10px;
  color: #999;
}
.personal_module3 .money_bag{
  width: 20%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.personal_module3 .money_bag>view:nth-of-type(1){
  width: 26px;
  height: 26px;
  
}
.scroll_bar{
  background-color: #999;
  height: 10px;
  width: 20%;
  margin: auto;
}
.scroll_position{
  background-color: #f00;
  width: 30%;
  height: 10px;
}
/* 模块3结束 */

js

Page({
  data: {
    bagWidth:0,
    // 数据
    assets:[
      {id:1,number:"1",title:"优惠券",remark:""},
      {id:2,number:"4.79万",title:"白条分分卡",remark:"额度待领取"},
      {id:3,number:"0",title:"京豆",remark:""},
      {id:4,number:"0",title:"红包",remark:""},
      {id:5,number:"20.00万",title:"金条借款",remark:"白用30天"},
    ],
    // 传入组件参数  
    scrollObj:{
      scrollWidth:String,
      totalWidth:String,
      scrollLeft:String,
    }
  },
  // 滚动距离
  onscroll(e){
    const that = this
    wx.createSelectorQuery().select('#scroll').boundingClientRect(function(res){
      // 容器宽度
      let scrollWidth = res.width
      // 总宽度 = 块宽度 * 数量
      let cardWidth = that.data.bagWidth // 块宽度
      let number = that.data.assets.length // scroll里面的块数量
      let totalWidth = cardWidth * number  // scroll 整体宽度
      // 滚动距离
      let scrollLeft = e.detail.scrollLeft
      // 保存并当作参数传递给组件
      let scrollObj = {
        scrollWidth,
        totalWidth,
        scrollLeft
      }
      that.setData({scrollObj})
    }).exec()
  },
  /**
   * 生命周期函数--监听页面加载
   */
  /* 页面加载,获取“全部资产”宽度,适用于所有设备 */
  onLoad(options) {
    const that = this
    // 获取“全部资产”宽度,保存到bagWidth中
    wx.createSelectorQuery().select('#bag').boundingClientRect(function(res){
      let bagWidth = res.width
      that.setData({bagWidth})
    }).exec()
  },
})

组件

组件代码其实没什么可说的,如果硬要说的话,其实就一点

很多人可能不明白35rpx怎么回事

<view class="wrap">
  <view class="opsition" style="margin-left: calc(35rpx * {{result}});"></view>
</view>

这里我要说一下,35rpx不是随便来的

35rpx = 60rpx - 25rpx - 是个人都知道

但是:60rpx是wrap(外部容器)的宽度,而25rpx则是opsition(内部容器)宽度

这样一说,答案显而易见35rpx是空白部分的宽度

有人可能会问,我们要移动内部容器的位置,为什么要计算空白部分的宽度

这里就要说了,因为我们计算的滚动比例是从0-1的,而空白部分就恰恰符合我们的要求,当移动比例是0的时候,移动位置 = 35rpx * 0,位置不动,当滚动比例发生变化,最大到1的时候,我们的空白部分就是35rpx * 1 = 35rpx , 则我们的块刚好是25rpx,加一起刚好又是60rpx,多么巧合(当然不是啦)!是我们通过计算得来的啦。

PS:如果有什么疑问,欢迎到评论区提出!