47

[UI组件] 来做一个可配置的滑块进度条吧

 5 years ago
source link: https://segmentfault.com/a/1190000019066044?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

emmm,一看这样的设计需求就意味着小程序原生的 slider 组件就不能用了。因为这玩意在样式上就不能自由的配置,只好来手动实现一个。

结构设计

vUB7Fnq.jpg!web

行吧,那说干就干。首先滑动条可以从俯视图角度来看,分为三层。分别是 底部滑轨区域进度条区域 以及供用户操作的 滑块 本身。

在结构设计中,可以将 底部滑轨区域进度条区域 分为一块,这样 进度条区域 可以根据随着滑动条的高度变化而变化, 宽度则由 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

6V3iuuB.jpg!web

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK