视频链接:
2.12 滑动选择器:如何基于 wxs 自定义实现一个竖向的 slider?
片 1
上节课我们学习了滚动选择器组件,以及如何基于 wxs 脚本自定义实现一个省、市、区三级联动的滚动选择器组件,我们了解了如何在 wxs 模块里编写组件,这节课我们学习滑动选择器组件,继续学习如何使用 wxs 脚本,自定义实现一个竖向的 slider 组件。
我们先看一下 slider 这个组件,这个组件的属性非常简单,通过查看官方文档,很快就能明白怎么使用了。只有一个事件属性 changing 值得一提,这个事件是在滑动的变动过程当中触发的,我们通过这个事件可以实现一些联动效果,让页面上的一些其它功能,随着 slider 滑动而有所变化。当然啦,在使用的时候要注意一下,最好使用 wxs 脚本去实现,避免在逻辑层 js 里写许多的视图交互逻辑。
接下来我们看如何实现这个竖向的 slider 组件。
片 2
这是在开发者社区上有一个开发者实现的自定义组件(见上面网址),基本上实现了所有官方 slider 组件的属性,并且也是基于 wxs 脚本实现的,接下来我们看看这个组件它具体是怎么实现的。基本上通过对这个组件的剖析,我们就能够对如何写 wxs 组件了然于胸了。
<wxs module="eventHandle" src="./index.wxs">wxs>
<view class="slider-container" change:prop="{{eventHandle.propsChange}}" prop="{{ {max,min,step,value,totalTop,totalHeight,disabled} }}" >
propsChange: function(newValue, oldValue, ownerIns, ins) {
var state = ownerIns.getState()
var step = newValue.step;
var min = newValue.min;
var max = newValue.max;
...
state.totalTop = newValue.totalTop
state.totalHeight = newValue.totalHeight
if (newValue.totalTop !== null && newValue.totalHeight !== null) {
calculate(ownerIns, state, function(currentValue){
ownerIns.callMethod("setCurrent", state.current + state.min)
})
}
}
片 3
首先我们看一下,这里在 view 上有一个关于 change:prop 属性,及 prop 属性的设置(见上面 wxml 代码)。在 view 这个组件上并不存在 prop 这个属性,设置这个属性,纯粹是为了基于WxsPropObserver 机制,向wxs 模块里的 eventHandle.propsChange 函数传值。
在属性 prop 前面加一个 “change:”,这就是为了使用 WxsPropObserver 机制,这个机制是为了在 wxs 模块中监听对 wxml 组件属性的设置。这个机制在上节课我们已经接触过了。eventHandle.propsChange 这个函数会在视图第一次渲染的时候触发,在这个函数的参数列表里,我们可以依次拿到四个参数对象:newValue, oldValue, ownerIns, ins(见上方代码)。
其中 newValue、oldValue,是属性对象的新、旧值,在这里只有 newValue,因为是第一次设置,也是唯一的一次。newValue 它就是我们在 wxml 里面传递进去的,包括 min、max 等变量的临时数据对象。ownerIns 是包含派发事件的组件的父组件描述对象,是一ComponentDescriptor对象,后面的 ins 是派发事件的组件对象。在这里我们使用 ownerIns 就足够了。我们需要重点关注下这个 ownerIns 组件描述对象,它都有哪些可以使用的方法,这对我们写 wxs 脚本很有帮助。
selectComponent
selectAllComponents
setStyle
addClass/removeClass/ hasClass
getDataset
callMethod(funcName:string, args:object)
requestAnimationFrame
getState
triggerEvent(eventName, detail)
片 4
从上面这个列表中,我们可以看到一个 ComponentDescriptor 组件描述对象的所有方法。
第一个 selectComponent 方法,它返回组件的 ComponentDescriptor 实例,这和我们当前查看的对象类型是相同的。它用于查找 wxml 页面中的组件,参数与 JQuery 中组件查询是类似的,可以是以井号(#)开头的组件 id,也可以是以点号(.)开头的样式类名称。
第二个方法 selectAllComponents,返回组件的 ComponentDescriptor 对象的数组,它与第一个方法类似,不同的地方在于它返回的是一个数组。
第三个方法 setStyle,用于设置内联样式,支持 rpx 单位,并且优先级比组件 wxml 里面定义的样式要高,但不能用它设置最顶层页面的样式。
第四个方法组,addClass/removeClass/ hasClass,这三个方法是为了设置组件的 class样式,作用与 setStyle 类似,但这里使用的是类名称,并且设置的 class 优先级也比组件 wxml 里面定义的 class 要高,同样它不能设置最高顶层页面的 class样式。
第五个方法 getDataset,返回当前组件对象或页面对象的 dataset 对象,我们可以在组件上以 dataset-x 这样的形式,定义组件的扩展属性,绑定逻辑层 js 中的数据,并将它们以这种方式传递到 wxs 脚本中去。
第六个方法 callMethod,这是最经常使用的方法之一了,调用当前组件对象或页面对象在逻辑层(App Service)定义的函数,funcName 表示的是函数名称,args 表示函数的参数。在这里要注意,args 参数是一个对象,这意味着如果逻辑层 js 代码里面,有函数需要被 wxs 调用,参数只能声明一个。如果有多个参数,可以考虑把它们复合成一个参数对象。
第七个方法 requestAnimationFrame,是用于实现动画的,相当于是一个与页面渲染同频的定时器,就是每渲染一帧,它就执行一次。
第八个方法 getState,返回一个 object 对象,当有一些数据变量,需要存储起来,在各个 wxs 方法之间共享使用的时候,用这个方法比较方便。稍后我们在源码中可以看到它的使用示例。
第九个方法 triggerEvent,这也是最常使用的方法之一,它和 js 中组件的 triggerEvent 一致,目的都是派发事件。但由于 callMethod 只能传递一个参数,并不能通过 callMethod 调用这个方法,这是一个替代方法。
看完了组件描述对象的方法,接下来我们接着解读竖向 slider 组件的源码。
<view class="slider-container"...>
<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>
<view class="slider-lower" id="lower" catchtap="{{eventHandle.tap}}">
片 5
这个竖向 slider 是以底部为起点的,滑动到底部为 min 值,滑动到顶部为 max 值。整个 slider 分成上、中、下三部分,分别是灰色的竖条、白色的滑块,以及绿色的竖条。灰色竖条是 slider-upper,中间的 slider-middle 是圆形白色的滑块,最下面的 slider-lower 是绿色的竖条。中间滑块 slider-middle 是不占大小的,宽高都是 0,它的子组件 slider-block,它是有大小的,它的 postion 样式是 relative,它是以相对定位的方式挂在中间位置的。
.slider-container {
flex: 1;
margin: 0 20px;
width: 0;
display: flex;
flex-direction: column;
align-items: center;
}
片 6
最外层的 slider-container,使用的是 flex 布局,并且 flex-direction 是 column,也是竖向排列的,它的样式 align-items 等于 center,限定了子组件左右居中对齐。
var calculate = function(instance,state,changeCallback){
..
。
instance.selectComponent("#upper").setStyle({
height: (100 - percent) + "%"
})
....
}
片 7
我们接着往下看看代码,容器大小是固定的,上面的灰色与下面的绿色竖条的长度,是在 calculate 这个函数中,在经过计算后,通过 setStyle 设置的。在 wxs 代码里面,我们只设置了#upper 的百分比,因为下面的#lower 的百分比,会因为它的 flex:1 这样的样式而自动扩展。
这个 calculate 函数是一个通用的计算函数,在源码中担当了计算上、下灰、绿色竖条的长度,并设置样式的角色功能。
<view class="slider-append" data-percent="1" bindtap="{{eventHandle.tapEndPoint}}">view>
<view class="slider-append" data-percent="0" bindtap="{{eventHandle.tapEndPoint}}">view>
tapEndPoint: function(e, ins){
var state = ins.getState()
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", currentValue)
})
}
片 8
我们接着看源码实现,在 wxml 组件里,上下有两个高度为 10px 的 .slider-append 区域,这两个区域的存在,是为了实现单击顶部、底端自动设置选择值为最大值、最小值的功能。这两个组件绑定的 wxs 事件函数是同一个 eventHandle.tapEndPoint。
因为两个组件上都定义了一个 data-percent 扩展属性,一个为 1,一个为 0,所以在 wxs 事件函数里面,我们可以通过 e.currentTarget.dataset.percent 取出这个扩展属性的值,从而对单击情况作出区别对待。其中 e.currentTarget,代表当前单击的组件对象。
在 eventHandle.tapEndPoint 这个事件函数中,我们还通过 ins.getState 取出了 wxs 模块的数据共享对象。组件描述对象的 getState 这个方法在前面我们讲过了,它是用于在各个方法中间共享数据的。在这里我们用到的它里面的数据,是我们在别的函数中存储进去的。
tap: function(e, ins) {
var state = ins.getState()
var percent = (state.totalTop + state.totalHeight - e.changedTouches[0].pageY) / state.totalHeight
state.offset = (state.max - state.min) * percent
calculate(ins, state, function(currentValue){
ins.callMethod("setCurrent", currentValue)
ins.triggerEvent("change", {
value: currentValue
})
})
}
片 9
接着往下看源码,tap 这个函数,是监听单击在中间滑块上的 tap 事件的。处理手法与 tapEndPoint 类似,不同的地方在于,在这个函数里面,通过 e.changedTouches 取到了当前的触控点,然后又通过 Touch 对象 的 pageY 属性,拿到了单击点相对于文档左上角的 y 坐标,最后通过这个坐标计算出来当前应该设置的百分比是多少。
1,identifier
2,pageX, pageY
3,clientX, clientY
片 10
我们看一下上面这个列表,Touch 对象一共有三组属性。identifier 是触摸点的标识符。
pageX, pageY 是距离文档左上角的距离,是以文档为基准的,会把滚动的距离也算进去,其中
pageY=clientY+scrollTop
pageX=clientX+scrollLeft
clientX, clientY 是距离页面可显示区域左上角的距离。有时候我们还能看到一组 screenX、screenY 属性,它们表示距离屏幕左上角的距离。
有时候我们还能看到一组 offsetX、offsetY 属性,它们是单击点相对单击对象左上角的偏移量。
start: function(e, ins) {
var state = ins.getState()
state.startPoint = e.changedTouches[0]
var currentPx = state.current / (state.max - state.min) * state.totalHeight
state.currentPx = currentPx
},
move: function(e, ins) {
var state = ins.getState()
var startPoint = state.startPoint
var endPoint = e.changedTouches[0]
var currentPx = state.currentPx + startPoint.pageY - endPoint.pageY
var percent = currentPx / state.totalHeight
...
calculate(ins, state, function(currentValue){
ins.triggerEvent("changing", {
value: currentValue
})
ins.callMethod("setCurrent", currentValue)
})
。
},
end: function(e, ins) {
var state = ins.getState()
ins.triggerEvent("change", {
value: state.current + state.min
})
}
片 11
我们接着继续看代码,再看看滑动是怎么实现的,这个功能也利用了 TouchEvent 事件。在 touchstart 事件中,先记下起始坐标 startPoint,以及当前滑动的相对距离 currentPx,这个时候就把起始坐标和起始距离绑定在一起了,即使我们按下的不是滑块的中心,是滑块的任何地方都没有关系(见上面代码)。
在滑块移动的过程中,再根据当前的触控点坐标 endPoint,与起始坐标 startPoint,计算出滑动的差值,再以这个差值算出百分比。
.slider-middle {
flex-shrink: 1;
width: 0;
height: 0;
display: flex;
align-items: center;
justify-content: center;
}
压缩计算公式:单个组件压缩量 = 总压缩量 x (单个 flex-shrink 值 / 总 flex-shrink 值)
片 12
(可以从本课视频中查看示例和运行效果)
在这个组件的实现中,还有一个有关 flex 布局的样式十分值得一提:flex-shrink,shrink([ʃrɪŋk])是收缩的意思,它是处理flex元素的缩放策略的,它的默认值是1,可以是2、或3甚至更大,值是大于等于0的。当值等于0时,代表元素是刚性的,不支持被动压缩下的缩放。
在这个组件的 wxml 源码中,组件容器默认高度为 200px,如果我们将高度在外围修改为 150px,这个时候会发生什么事情呢?将是谁被压缩了?
从测试结果来看,4 个一级子组件,只有.slider-container 被压缩了,其它组件如顶部、底部的.slider-append 区域,还有百分比数字区域,都维持了原来的高度。因为只有.slider-container 的 flex-shrink 样式是默认值 1,其它三个组件的 flex-shrink 样式都是 0。
flex-shrink 这个样式不是平分空间的权重,而是承担被压缩空间的权重。从高度 200px 减至 150px,被压缩了 50px,这个50的压缩量,要所有子组件一起承担,计算公式就是:单个组件压缩量 = 总压缩量 x (单个组件的flex-shrink 值 / 总的 flex-shrink 值)
如果所有子组件都设置了 flex-shrink 等于 0,都表示不接受被动压缩,那怎么办?
从测试效果来看,在这种情况下,所有子组件都会被压缩,和同时为 1 的情况是一样的。在.slider-container 中,中间滑块容器的宽高都是 0,它的 flex-shrink 样式设置为 0 或是 1,结果都是一样的。
(可以从本课视频中查看示例和运行效果)
以上就是关于竖向 slider 组件源码的解读。
这节课给你留一个作业,参照本课学习的竖向的 slider 组件源码,实现一个横向的 slider 组件,也使用 wxs 脚本去写。可以把现有的这个组件拷贝一份源码,在它的基础之上进行修改。虽然官方已经有了一个 slider 组件了,重写这个组件,意义在于练习使用 wxs 脚本。
好,这节课我们就学到这里。这节课主要讲了 slider 组件,以及如何在 wxs 脚本中实现一个竖向的 slider 组件。下节课我们学习页面链接组件,及如何自定义一个小程序页面的导航栏。
2020 年 5 月 28 日