一、微信开放社区的自定义竖向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"
}
}