微信小程序全栈开发实践 第二章 微信小程序组件介绍及使用 -- 2.8 滑动选择器表单组件,如何基于WXS自定义一个竖向的 slider ?

一、微信开放社区的自定义竖向slider

https://developers.weixin.qq.com/community/develop/article/doc/0002c8ac9603d0600e09b003b56413

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Touch对象一共有三组属性,
identifier ,它是触控点的标识符
pageX、pageY是距离文档左上角的距离,是以文档以基准的,它会把滚动的距离计算进去

蓝色的框相当于文档,可以向上滚动的
红色的框相当于我们的可视窗口
灰色的框,相当于我们桌面的窗口,就是一个屏幕的大小

pageY =  clientY + scrollTop
pageX = clientX + scrollLeft

clientX 、clientY 是距离页面可显示区域左上角的距离
screenX、screenY 他们表示距离屏幕左上角的距离
offsetX、offsetY 单击点相对于单击对象左上角的偏移量

在这里插入图片描述

在这里插入图片描述

components/sliderVertical.wxml

<wxs module="eventHandle" src="./SliderVertical.wxs"></wxs>

<view class="slider" catchtouchmove="empty">
 <!-- 直接切换到最大值,tapTop -->
  <view class="slider-append" data-percent="1" bindtap="{{eventHandle.tapEndPoint}}"></view>


  <!-- 通过prop向wxs传递默认数据 -->
  <!-- 

    prop="{{ {max,min,step,value,totalTop,totalHeight,disabled} }}"
    外面的两个花括号是绑定变量用的,
    里面的花括号,指的是这是一个对象


    
    view上有个一个 change:prop属性  change:prop="{{eventHandle.propsChange}}"

    view这个组件上其实并不存在prop这个属性,设置这个属性纯粹是为了基于WxsPopObserver机制,向wxs模块里的eventHandle.propsChange函数传值

    在属性prop前面加一个 "change:",这就是为了使用这个WxsPopObserver机制,这个机制是为了在WXS模块里,监听对WXML属性的设置。
    eventHandle.propsChange函数会在视图第一次渲染的时候触发。

  -->
  <view class="slider-container" change:prop="{{eventHandle.propsChange}}" prop="{{ {max,min,step,value,totalTop,totalHeight,disabled} }}" >

    <view class="slider-upper" id="upper" catchtap="{{eventHandle.tap}}">
      <view class="slider-upper-line" style="background-color: {{backgroundColor}}"></view>
    </view>

    <view class="slider-middle">
      <view 
        class="slider-block" 
        style="background-color:{{blockColor}};box-shadow:{{blockColor=='#ffffff'?'0 0 2px 2px rgba(0,0,0,0.2)':'none'}};width:{{blockSize}}px;height:{{blockSize}}px" 
        catchtouchstart="{{eventHandle.start}}" 
        catchtouchmove="{{eventHandle.move}}"
        catchtouchend="{{eventHandle.end}}"
        ></view>
    </view>

    <!-- 

      slider-middle 
      .slider-middle {
        flex-shrink: 0; 
        width: 0;
        height: 0;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .slider-block {
        flex-shrink: 0;
        width: 20rpx;
        height: 20rpx;
        border-radius: 50%;
        position: relative;
        z-index: 1;
      }
      它本身中间这一块的容器不占用空间,里面的滑块.slider-block有大小的,
      滑块通过position样式一个相对定位,去挂在到中间的位置
      竖的滑块是通过上下灰色与绿色滑条,然后定中间的分界点在哪里,
      中间分界点有一个空容器,在空容器上挂载滑块

     -->

    <view class="slider-lower" id="lower" catchtap="{{eventHandle.tap}}">
      <view class="slider-lower-line" style="background-color: {{activeColor}}"></view>
    </view>

  </view>

  <!-- 
    这个竖向slider是 以底部为起点的,滑动到底部为min值,滑动到顶部为max值,
    整个slider分成上、中、下三部分,分别是灰色的竖条、白色的滑块、以及绿色的竖条
    灰色竖条是slider-upper
    圆形白色滑块是slider-middle
    绿色的竖条是slider-lower

    中间滑块slider-middle是不占用大小的,宽高都是0.它的子组件 .slider-block 它是有大小的,并且它的position样式是relative,它是以相对定位的方式挂在中间位置的,

    .slider-container {
      flex: 1;
      margin: 0 20px;
      width: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    最外层的slider-container使用的是flex布局


   -->

  <!-- 直接切换到最小值,tapEnd,data-percent=0, 是dataset数据属性 -->
  <view class="slider-append" data-percent="0" bindtap="{{eventHandle.tapEndPoint}}"></view>

   <!-- 
    <view class="slider-append" data-percent="1" bindtap="{{eventHandle.tapEndPoint}}"></view>

    <view class="slider-append" data-percent="0" bindtap="{{eventHandle.tapEndPoint}}"></view>

    上下都有一个slider-append,它俩的区别是它们这个扩展属性data-percent值设置的不一样
    这两个区域的存在是为了实现单击顶部、底部,自动设置选择值为最大值、最小值的功能

    这两个组件绑定的WXS事件函数是同一个,eventHandle.tapEndPoint,

    -->



  <view class="slider-value" wx:if="{{showValue}}">{{currentValue}}</view>
</view>

components/sliderVertical.wxs

var notInt = function (num) {
  return num !== parseInt(num)
}
/*
 * state:共享临时数据对象
 * state.max:最大值
 * state.min:最小值
 * state.offset:当前高度,即value-min的值(未按照step纠正的值)
 * state.step:步长
 * ins:页面或组件实例
 * 计算上、下 灰、绿色竖条的长度,并设置样式的角色功能,
 */
// 这个函数主要是为了计算,当value变化时,计算出上、下两部分灰、绿色各占百分多少
// 然后通过组件描述对象设置样式
// 这个方法会在第一次属性设置中、调用
var calculate = function (instance, state, changeCallback) {
  var max = state.max
  var min = state.min
  var offset = state.offset
  var step = state.step
  // 1、计算 offset 按照 step 算应该是几个。
  // Math.round函数,是大于等于0.5算一步,否则不算
  // Math.round(offset % step / step) 计算的是 offset 对 step 取模后剩下的长度四舍五入,就是多出来的部分是否该算一步
  // Math.floor(offset / step) 计算的是 offset 中包含多少个完整的 step
  var stepNum = Math.round(offset % step / step) + Math.floor(offset / step)
  // 2、纠正后的当前高度
  // 当前的步值数
  var current = stepNum * step

  // 3、当前高度所占比例,由于 offset 的大小已经在进方法前经过了修正,所以这里不需要再判断是否小于0或者大于100了
  // 算出百分比
  var percent = current * 100 / (max - min)

  // 设置上灰色是多少高度,百分比
  // value是从底部开始计算的,所以这里是100-percent
  instance.selectComponent("#upper").setStyle({
    height: (100 - percent) + "%"
  })
  // 设置选中的绿色部分百分比
  instance.selectComponent("#lower").setStyle({
    height: percent + "%"
  })
  // 如果值有变化,调用回调函数
  if (state.current !== current) {
    state.current = current
    changeCallback(current + min)
  }
}









module.exports = {


  /*
  propsChange: function(newValue, oldValue, ownerIns,ins) {
  在这个函数的参数列表里边,我们可以依次拿到四个参数,

  newValue, oldValue 属性对象的新旧值
  在这里其实只有newValue,因为是第一次设置,也是唯一的一次设置
  newValue就是我们在WXML里面通过prop属性传进去的里边的临时数据对象  prop="{{ {max,min,step,value,totalTop,totalHeight,disabled} }}"   ,这个里面包括min、max等等这些变量

  owerIns  是包含派发事件的组件的父组件描述对象,它是一个ComponentDescriptor对象

  最后面的ins是派发事件的组件对象,在这里我们使用ownerIns就足够了
  */




  /*
    我们需要重点关注一下ownerIns组件描述对象,它都有哪些方法可以使用,这对我们写WXS脚本很有帮助    
    
    组件描述对象的方法

    selectComponent  
    它返回组件的ComponentDescriptor实例,它用于查找WXML页面中的组件,参数与jQuery中组件查询是类似的

    selectAllComponents
    它返回ComponentDescriptor对象数组,与第一个类似,不同的是返回的是一个数组

    setStyle 
    用于设置内联样式,在这里它支持rpx单位,并且它的优先级比组件WXML里面定义的样式要高,但不能用它设置最高层页面样式

    addClass/removeClass/hasClass
    这三个方法是为了设置组件的class样式,作用于setStyle类似,这里使用的是类名称,并且设置class优先级比组件的WXML里面定义的class要高,同样也不能设置最高层页面的class样式

    getDataset
    返回当前组件对象或页面对象的dataset对象,我们可以在组件上以dataset-x这样的形式,定义组件的扩展样式,绑定逻辑层js中的数据,并将它们以这种方式传递到wxs脚本中。

    callMethod(functionName:string,args:object)
    调用当前组件对象或页面对象在逻辑层 AppService 里面定义的函数, 
    functionName 函数名称,args 函数的参数,参数是一个对象

    requestAnimationFrame
    用于实现动画,相当于一个与 页面渲染同频的定时器,就是每渲染一帧,它就执行一次。

    getState
    返回一个object对象,当有一个数据变量需要存储起来,在各个WXS方法之间共享使用的时候,
    用这个方法比较方便

    triggerEvent(eventName,detail)
    它和js中组件的triggerEvent一致,目的都是派发事件,但由于callMethod只能一次传递一个参数,
    并不能通过callMethod调用这个方法,这是一个替代方法。

   */

  // 这是由wxsPropObserver机制调用的
  propsChange: function (newValue, oldValue, ins) {
    var state = ins.getState()
    var step = newValue.step;
    var min = newValue.min;
    var max = newValue.max;
    // value是设置的当前值
    var value = newValue.value;
    if (notInt(step) || notInt(min) || notInt(max) || notInt(value)) {
      console.log("你不把 step min max value 设成正整数,我没法做啊")
      return
    }
    if (min > max) {
      min = oldValue.min
      max = oldValue.max
    }
    if (value > max) {
      console.log("value的值比max大,将value强制设为max")
      value = max
    } else if (value < min) {
      console.log("value的值比min小,将value强制设为min:" + min)
      value = min
    }
    if (step <= 0 || (max - min) % step != 0) {
      console.log("step只能是正整数且必须被(max-min)整除,否则将step强制设为1")
      step = 1
    }
    state.min = min
    state.max = max
    state.step = step
    state.offset = value - min
    state.disabled = newValue.disabled
    state.totalTop = newValue.totalTop
    state.totalHeight = newValue.totalHeight
    if (newValue.totalTop !== null && newValue.totalHeight !== null) {
      calculate(ins, state, function (currentValue) {
        ins.callMethod("setCurrent", {
          value: state.current + state.min
        })
      })
    }
  },

  // .slider-append  实现单击顶部、底部,自动设置选择值为最大值、最小值的功能
  // 这是由顶点、底点单击时调用的
  // 直接在最大、最小值之间切换
  // 因为在组件上设置了dataset属性,所以可以用一种函数
  // 
  tapEndPoint: function (e, ins) {
    // ins.getState() 取出wxs模块的数据共享对象,在各个js函数共享一些数据
    var state = ins.getState()
    if (state.disabled) return

    // e.currentTarget 当前单击的组件对象
    var percent = e.currentTarget.dataset.percent //取出扩展属性的值
    state.offset = (state.max - state.min) * percent
    calculate(ins, state, function (currentValue) {
      ins.triggerEvent("change", {
        value: currentValue
      })
      ins.callMethod("setCurrent", {
        value: currentValue
      })
    })
  },

  //.slider-upper .slider-lower  监听单击灰色竖条、绿色竖条的tap事件
  // 单击upper、lower两处或灰色、或绿色,都是调用这个函数
  tap: function (e, ins) {
    var state = ins.getState()
    if (state.disabled) return
    //(总高度+头部高度-点击点高度)/ 总高度 = 点击点在组件的位置
    // 点击事件只在线条上,所以percent是不可能小于0,也不可能超过100%,无需另加判断
     // 计算从滑块底部,到音击点之间的距离,再除为总高度,计算出百分比
    /* 
     e.changedTouches取得当前的触控点,是一个数组,在通过touch对象的pageY属性,
     拿到单击点相对于文档左上角左上角的y坐标,通过这个坐标计算出来当前应该设置的百分比是多少?

    */


    var percent = (state.totalHeight - e.changedTouches[0].pageY + state.totalTop) / state.totalHeight
     // 依据百分比,计算出偏移量数量,这个数量与上面计算出来的从滑块底部到音击点之间的距离,并不一定一致,因为单位是由调用者定义的
    state.offset = (state.max - state.min) * percent
     // 最后调用计算函数,完成后派发事件,设置逻辑层的当前值
    calculate(ins, state, function (currentValue) {
      ins.triggerEvent("change", {
        value: currentValue
      })
      ins.callMethod("setCurrent", {
        value: currentValue
      })
    })
  },
  /*
  滑动是怎么实现的
  这个功能利用了touchEvent事件,

  start函数
  在touchstart事件中,先记下起始坐标 startPoint
  以及当前滑动的相对距离currentPx
  这个时候就把起始坐标和起始距离绑定在一块了,即使我们按下的不是滑块中心,是滑块的任何地方都没有关系

  move函数  
  滑动的过程中触发的函数  
  在滑动的过程当中,再根据当前的触控点坐标endPoint,
  根据startPoint和endPoint计算出滑动的差值,再以差值计算出百分比
  */
  // 滑块开始滑动时,记录当前坐标,及当前的current值
  // 由于单击点,与当前滑块的高度,只能作一个绑定,并不能完全等同,因为每次单击的点并不太一样
  start: function (e, ins) {
    var state = ins.getState()
    if (state.disabled) return
    state.startPoint = e.changedTouches[0]
    // 本次滑动之前的高度px = 当前高度value / (最大值-最小值) * 最大高度
    var currentPx = state.current / (state.max - state.min) * state.totalHeight
    state.currentPx = currentPx
  },
  // 滑块开始移动了
// getState是获取模块变量对象,可以在模块内共享利用,因为没有this,模块内并不方便在各个方法之间共享变量
  move: function (e, ins) {
    var state = ins.getState()
    if (state.disabled) return
    var startPoint = state.startPoint
    // 当前活动过程当中,第一个触控点是多少,把这个值给拿到
    var endPoint = e.changedTouches[0]
    // 当前的高度px = 滑动之前的高度px + 起始点高度 - 当前点高度
    // 根据startPoint和endPoint计算出滑动的差值,再以差值计算出百分比
    var currentPx = state.currentPx + startPoint.pageY - endPoint.pageY
    var percent = currentPx / state.totalHeight
    // 由于可能滑出slider范围,所以要限制比例在 0-1之间
    // 有可能会移动范围之处,值可能会超过0~1的范围
    percent = percent > 1 ? 1 : percent
    percent = percent < 0 ? 0 : percent
    state.offset = (state.max - state.min) * percent
    calculate(ins, state, function (currentValue) {
      ins.triggerEvent("changing", {
        value: currentValue
      })
      ins.callMethod("setCurrent", {
        value: currentValue
      })
    })
  },
  // 滑块结束
  end: function (e, ins) {
    var state = ins.getState()
    if (state.disabled) return
    ins.triggerEvent("change", {
      value: state.current + state.min
    })
  }
}

components/sliderVertical.js

// 该竖向组件来源于:https://developers.weixin.qq.com/community/develop/article/doc/0002c8ac9603d0600e09b003b56413
// 仅有少量修改
Component({
  properties: {
    blockColor: {
      type: String,
      value: "#ffffff"
    },
    blockSize: {
      type: Number,
      value: 28
    },
    backgroundColor: {
      type: String,
      value: "#e9e9e9"
    },
    activeColor: {
      type: String,
      value: "#1aad19"
    },
    step: {
      type: Number,
      value: 1
    },
    min: {
      type: Number,
      value: 0
    },
    max: {
      type: Number,
      value: 100
    },
    value: {
      type: Number,
      value: 0
    },
    disabled: {
      type: Boolean,
      value: false
    },
    showValue: {
      type: Boolean,
      value: false
    },
  },
  observers: {
    'blockSize': function(blockSize) {
          // 这个地方是规范blockSize的最大、最小值
      if (blockSize > 28) {
        this.setData({
          blockSize: 28
        })
      } else if (blockSize < 12) {
        this.setData({
          blockSize: 12
        })
      }
    },
    'showValue': function(){
      this.queryHeight() // 由于显示数字后,滑动区域变化,需要重新查询可滑动高度
       // 如果被设置了,相应的observer是自动触发的
      // 因为这个组件在使用时,设置了showValue属性,所以queryHeight被调用了
      // 如果没有设置,这个函数是不会被调用的
      console.log('showValue----',this.properties.showValue);
    }
  },
  data: {
    totalTop: null,
    totalHeight: null,
    currentValue: 0,
  },
  ready(){
    // 适合在这里调用
    this.queryHeight()
  },
  methods: {
     // 设置当前值是多少,是加上最小值之后的当前值
    setCurrent: function(e){
      this.setData({
        currentValue: e.value
      })
    },
    queryHeight: function(){
       // 这个地方是为了取出滚动容器在页面中的起点,距离页面顶边的y距离是多少totalTop
      // totalTop相当于startY
      // 还有整个滑动容器的高度totalHeight,相当于sliderHeight
      // 因在wxs模块中组件或页面描述对象,没有createSelectorQuery方法,所以这个查询只能放在逻辑层js中进行,好在这个查询并不需要重复执行
      // 整个totalHeight,要被min,max的差,以step为粒度平分掉
      wx.createSelectorQuery().in(this).select('.slider-container').boundingClientRect((res) => {
        this.setData({
          totalTop: res.top,
          totalHeight: res.height
        })
      }).exec()
    },
    empty: function(){},
  }
})

components/sliderVertical.json

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

components/sliderVertical.wxss

.slider {
  height: 100%;
  padding: 30rpx 0;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-around;
}


.slider-upper-line {
  height: 100%;
  margin: 0 12px;
  width: 2px;
}
.slider-lower-line {
  height: 100%;
  margin: 0 12px;
  width: 2px;
}

/* 
 flex-shrink: 0; 
它是处理flex元素的缩放策略的,它的值大于等于0
当值等于1时,代表元素是刚性的,不支持被动压缩下的缩放

*/
/* 
在这个组件的wxml代码中,组件容器默认高度为200px,
如果我们将高度在外围修改为150px,只有slider-container被压缩
 flex-shrink 不是平分空间的权重,它是承担被压缩空间的权重

 总压缩量 200-150 = 50

 单个组件的压缩量 = 总压缩量 * ( 单个组件的flex-shrink值 / 总的 flex-shrink 值 )
 如果所有子组件都设置了 flex-shrink: 0; 都不想被压缩,都表示不接受被动压缩
 这种情况下,从测试结果看,所有的子组件都会被压缩
 和同时为1的情况是一样的

*/

/* 
flex-shrink这个样式是决定flex元素的收缩规则的
并且,它不是决定元素的所占宽度比,而是决定了不足的空间,各个元素各分摊多少的缩小额度
*/


.slider-container {
  flex: 1;
  margin: 0 20px;
  width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.slider-upper {
  flex-shrink: 0;
  height: 100%;
}
.slider-middle {
  flex-shrink: 0; 
  width: 0;
  height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.slider-lower {
  flex-shrink: 0;
  height: 0%;
}
.slider-append {
  flex-shrink: 0;
  height: 10px;
  padding: 0 20px;
}




.slider-block {
  flex-shrink: 0;
  width: 20rpx;
  height: 20rpx;
  border-radius: 50%;
  position: relative;
  z-index: 1;
}
.slider-value {
  flex-shrink: 0;
  pointer-events: none;
}


/index/index.wxml

<view style="padding:20px;">自定义竖向slider</view>
<view style="height: 400rpx;margin: 20px;display: flex;justify-content: space-around">
 
  <slider-vertical 
    block-color="#ffffff"
    block-size="28"
    backgroundColor="#e9e9e9" 
    activeColor="#1aad19" 
    bindchange="slider1change"
    bindchanging="slider1changing"
    step="1"
    min="0" 
    max="200"
    value="0"
    disabled="{{false}}"
    show-value="{{true}}"
    ></slider-vertical>
  <slider-vertical 
    block-color="#ffffff"
    block-size="28"
    backgroundColor="#e9e9e9" 
    activeColor="#1aad19" 
    bindchange="slider1change"
    bindchanging="slider1changing"
    step="5"
    min="50" 
    max="200"
    value="115"
    disabled="{{false}}"
    show-value="{{false}}"
    ></slider-vertical>
</view>

index/index.js

Page({
  slider1change: function (e) {
    console.log("change:",e)
  },
  slider1changing: function (e) {
    console.log("changing:",e)
  },
})

index/index.json

{
  "usingComponents": {
    "slider-vertical": "/components/sliderVertical/SliderVertical"
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值