在一些需要用户填写资料的业务场景中,有时会让用户选择某个业务的范围,这时就需要用到滑块进度条。然后你们最爱的产品经理会说,给我整一个颜色可控,滑块按钮可大可小,滑块边框也要可大可小的滑动条来..
emmm,一看这样的设计需求就意味着小程序原生的 slider 组件就不能用了。因为这玩意在样式上就不能自由的配置,只好来手动实现一个。
结构设计
行吧,那说干就干。首先滑动条可以从俯视图角度来看,分为三层。分别是 底部滑轨区域
, 进度条区域
以及供用户操作的 滑块
本身。
在结构设计中,可以将 底部滑轨区域
, 进度条区域
分为一块,这样 进度条区域
可以根据随着滑动条的高度变化而变化, 宽度则由 js
控制。除此之外还需要暴露一些参数给外部,让它自己定义长粗宽。
Component({
/**
* 组件的属性列表
*/
properties: {
// 滑块大小
blockSize: {
type: Number,
value: 32,
},
// 滑块宽度
blockBorderWidth: {
type: Number,
value: 3
},
// 滑轨高度
height: {
type: Number,
value: 2
},
// 滑轨进度
step: {
type: Number,
value: 0,
},
// 进度值小数位
digits: {
type: Number,
value: 0,
},
},
});
<view id="slider-wrap" class="slider-wrap">
<view class="silder-bg" style="height: {{height}}rpx;">
<view class="silder-bg-inner"></view>
</view>
<view
class="silder-block"
style="height: {{blockSize}}rpx; border-width: {{blockBorderWidth}}rpx;"
></view>
</view>
.slider-wrap {
position: relative;
display: flex;
align-items: center;
width: 100%;
}
.silder-bg,
.silder-bg-inner,
.silder-block {
position: absolute;
left: 0;
}
.silder-bg,
.silder-bg-inner {
width: 100%;
height: 2rpx;
flex: 1;
}
.silder-bg {
overflow: hidden;
background-color: #eeeeee;
border-radius: 8rpx;
z-index: 0;
}
.silder-bg-inner {
height: 100%;
background-color: #66a6ff;
/* border-radius: 8rpx; */
z-index: 1;
border-bottom-left-radius: 8rpx;
border-top-left-radius: 8rpx;
}
.silder-block {
width: 32rpx;
height: 32rpx;
background-color: #ffffff;
border: solid 3rpx #66a6ff;
z-index: 2;
border-radius: 50%;
box-sizing: border-box;
}
点击行为事件
滑块进度条的
滑块
是一个听话的小朋友,就是说我们叫它去哪它就听话的过去。所以就不要抓它去煲汤了~
在组件外部容器中绑定一个点击事件,我们必须得要知道用户点击位置,在 bind:tap
事件中取到 clientX
属性。除此之外还需要取到进度条的位置信息。
得到两个关键数据后,将用户点击的位置 ClintX
与进度条组件的偏移量 offset
相减,得出相对于组件内的进度 progress
.
再用组件的宽度 width
减去 progress
乘于 100
得到目前进度的百分比 percentage
。
同时为了防止进度条超出进度条
如下图所示:((191 - 36) / 301) * 100 ≈ 52
<view class="slider-wrap" bindtap="tappingSlider">
<!-- ...other -->
</view>
Component({
// ...
/**
* 组件的初始数据
*/
data: {
containerInfo: null,
percentage: 0,
},
ready() {
// 取到滑块进度条的位置信息
wx.createSelectorQuery().in(this)
.select('.slider-wrap')
.boundingClientRect((rect) => {if (!rect) return;
this.data.container = rect;
this._initBloackPos();}).exec()},
// 点击进度条
tappingSlider(evt) {const { containerInfo} = this.data;
if (!containerInfo) return;
const {clientX} = evt.changedTouches[0];
const {digits, _maxDistance} = this.data;
// 需要做边界处理
const perc = this._computeOffset(clientX, containerInfo.left, 100);
const percentage = this._boundaryHandler(perc);
this.setData({percentage});
this.triggerEvent('change', {value: percentage.toFixed(digits) * 1
});
},
/**
* 计算相对容器的偏移距离
*
* @param {Number} x - X 坐标
* @param {Number} offset - 偏移量
* @param {Number} maxVal - 在 maxVal 范围内求百分比
*/
_computeOffset(x, offset, maxVal) {const { width} = this.data.containerInfo;
// 底层保证一定精度
return (((x - offset) / width) * maxVal).toFixed(4) * 1;
},
/**
* 边界处理
* @param {Number} num - 待处理的最值
* @param {Number} maxNum - num 最大值
* @param {Number} minNum - num 最小值
*/
_boundaryHandler(num, maxNum = 100, minNum = 0) {return num > maxNum ? maxNum : (num < minNum ? minNum : num);
},
});
<view class="slider-wrap" bindtap="tappingSlider" bindtouchmove="onTouchMove">
<view class="silder-bg" style="height: {{height}}rpx;">
<view
class="silder-bg-inner"
style="width: {{percentage}}%; height: {{height}}rpx;"
></view>
</view>
<view
class="silder-block"
style="left: {{percentage}}%;width: {{blockSize}}rpx;height: {{blockSize}}rpx; border-width: {{blockBorderWidth}}rpx;"
></view>
</view>
虽然实现了点击滑动到指定位置的功能,但仔细一看还是有一些瑕疵的~ 当我们点击到百分百时,滑块
超出原先设定的容器宽度。
超出的原因是因为在布局上,我们使用绝对定位 absolute
,通过设置滑块left
属性来控制滑块位置的。
偏移量中还包含了滑块自身的宽度,因此还需要对滑块的偏移量做一定的处理,去掉自身宽度再获取百分比。
在文章开头我们已经暴露了一个 blockSize
的属性,利用该属性可以计算滑块的最大偏移量:
Component({
// ...
data: {
// other data...
_blockOffset: 0,
_maxDistance: 100,
},
methods: {
// 点击进度条
tappingSlider(evt) {const { containerInfo} = this.data;
if (!containerInfo) return;
const {clientX} = evt.changedTouches[0];
const {digits, _maxDistance} = this.data;
const computeOffset = (maxVal) => {return this._computeOffset(clientX, containerInfo.left, maxVal);
}
// 滑块偏移度
const _blockOffset = this._boundaryHandler(computeOffset(_maxDistance), _maxDistance
);
// 实际百分比
const percentage = this._boundaryHandler(computeOffset(100));
this.setData({_blockOffset, percentage});
this.triggerEvent('change', { value: percentage.toFixed(digits) * 1 });
},
}
})
<!-- other code -->
<view
class="silder-block"
style="left: {{_blockOffset}}%;width: {{blockSize}}rpx;height: {{blockSize}}rpx; border-width: {{blockBorderWidth}}rpx;"
></view>
如此,该事件就完成啦~
滑动事件
完成点击事件后,我们还得让它能进行自由的滑动。进度条组件的拖动的流程大致是:点击滑块
-> 拖动滑块
-> 释放滑块
这三个步骤。
因此跟 H5 的思路一样,我们只需监听 touchmove
、touchstatr
、touchend
三个事件。
首先先监听 touchmove
,用户点击滑块后,记录当前的clientX
属性, 随后还需要记录当前 进度
和滑块的 偏移量
;touchmove
事件则由外层容器相关联,并更新滑动的距离。由于 touchmove
里针对 拖动事件
逻辑不能被随便触发,因此需要加一个标识的锁;
在 touchend
事件触发后释放锁即可:
Component({
methods: {onTouchStart(evt) {
this.data.moving = true;
// 记录原始坐标
this.data.originPos = this.data._blockOffset;
this.data.originPercentage = this.data.percentage;
this.data._startTouchX = evt.changedTouches[0].clientX;
},
// 滑块移动
onTouchMove(evt) {const { moving, containerInfo} = this.data;
if (!moving || !containerInfo) return;
const {clientX} = evt.changedTouches[0];
const {
digits,
originPos,
originPercentage,
_startTouchX,
_maxDistance
} = this.data;
// 计算偏移量
const computeOffset = (maxVal) => {return this._computeOffset(clientX, _startTouchX, maxVal);
}
// 实际百分比
const perc = originPercentage + computeOffset(100);
const percentage = this._boundaryHandler(perc);
// 滑块偏移度
const offset = originPos + computeOffset(_maxDistance);
const _blockOffset = this._boundaryHandler(offset, _maxDistance);
this.setData({percentage, _blockOffset});
this.triggerEvent('change', {value: percentage.toFixed(digits) * 1
});
},
onTouchEnd(evt) {this.data.moving = false;},
}
})
<view class="slider-wrap" bindtap="tappingSlider" bindtouchmove="onTouchMove">
<view class="silder-bg" style="height: {{height}}rpx;">
<view
class="silder-bg-inner"
style="width: {{percentage}}%; height: {{height}}rpx;"
></view>
</view>
<view
class="silder-block"
style="left: {{_blockOffset}}%;width: {{blockSize}}rpx;height: {{blockSize}}rpx; border-width: {{blockBorderWidth}}rpx;"
bindtouchstart="onTouchStart"
bindtouchend="onTouchEnd"
></view>
</view>
总结
以上就是 滑块进度条
组件的实现~ 实际上该组件还有更多可供配置的地方,如颜色值,背景控制等这些比较基础的东西就不继续展开讲啦~
本文是以小程序进行示例。但思路是共通的,也可以使用同样思路在 H5
实现,只不过是 API 的差异罢了~
微信代码片段, 可以直接拿来就用。
2019/05/04 更新:
后面又重新看了一遍,发现该组件还是有可优化的空间:
操作不必局限于滑块上,可以将 bindtap
事件废弃,其余的所有事件都代理到最外部的节点中。touchstar
的同时就渲染位置信息,还允许它自由的滑动:
<view class="slider-wrap"
bindtouchstart="onTouchStart"
bindtouchmove="onTouchMove"
bindtouchend="onTouchEnd"
>
<view class="silder-bg" style="height: {{height}}rpx;">
<view
class="silder-bg-inner"
style="width: {{percentage}}%; height: {{height}}rpx;"
></view>
</view>
<view
class="silder-block"
style="left: {{_blockOffset}}%;width: {{blockSize}}rpx;height: {{blockSize}}rpx; border-width: {{blockBorderWidth}}rpx;"
></view>
</view>
Component({
// other options ...
methods: {
// other method ...
onTouchStart(evt) {
this.data.moving = true;
const {containerInfo} = this.data;
if (!containerInfo) return;
const {clientX} = evt.changedTouches[0];
const {digits, _maxDistance} = this.data;
const computeOffset = (maxVal) => {return this._computeOffset(clientX, containerInfo.left, maxVal);
}
// 滑块偏移度
const _blockOffset = this._boundaryHandler(computeOffset(_maxDistance), _maxDistance
);
// 实际百分比
const percentage = this._boundaryHandler(computeOffset(100));
// 记录原始坐标
this.data.originPos = _blockOffset;
this.data.originPercentage = percentage;
this.data._startTouchX = clientX;
this.setData({_blockOffset, percentage});
this.triggerEvent('change', { value: percentage.toFixed(digits) * 1 });
},
}
});
微信代码片段 v0.0.2
原文出自:【UI 组件】来做一个可配置的滑块进度条吧