在一些需要用户填写资料的业务场景中,有时会让用户选择某个业务的范围,这时就需要用到滑块进度条。然后你们最爱的产品经理会说,给我整一个颜色可控,滑块按钮可大可小,滑块边框也要可大可小的滑动条来..

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的思路一样,我们只需监听touchmovetouchstatrtouchend三个事件。

首先先监听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组件】来做一个可配置的滑块进度条吧