小程序可以利用<scroll-view>实现自定义的下拉刷新和上拉加载效果,但是有一个坑,<scroll-view>中不能出现fixed元素,要注意
废话不多说,直接上代码,可以根据具体需求去做调整。效果还是可以的~
实际项目中应用过.
如果不追求样式的变化的话,我是觉得原生的很好用~

scrollList.js
const STATS = {    init: '',    pulling: 'pulling',    enough: 'pulling enough',    refreshing: 'refreshing',    refreshed: 'refreshed',    reset: 'reset',    loading: 'loading',}let lastY = null;    //记录上一次滚动的位置let scrollTop = 0;Component({    properties: {        leftList: [],        rightList: [],        feed: []    },    data: {        onRefresh: true,        loaderState: STATS.init,        pullHeight: 0,        progressed: 0,        pullDownHeight: 0,        animate: {},        //scroll-view的纵向是否滚动        scrollY: true,    },    properties: {        height: {            type: String        },        alreadyLoadData: {            type: Boolean,            value: true,            observer: function (e) {                this.isChange(e)            }        },        isEmpty: {            type: Boolean,            value: false        },        isScroll: {            type: Boolean,            value: false        }    },    methods: {        isChange: function (e) {            if (e) {                this.setData({                    loaderState: STATS.refreshed                })                setTimeout(() => {                    this.setData({                        loaderState: STATS.reset,                        pullDownHeight: 0                    }, this.initSTATS)                }, 500);            }        },        initSTATS: function () {            setTimeout(() => {                this.setData({                    loaderState: STATS.init                })            }, 500);        },        onScroll: function (e) {            scrollTop = e.detail.scrollTop            // console.log('组件scrollTop',scrollTop)            if(e.currentTarget.dataset.isscroll){                console.log('组件scrollTop',scrollTop)                var myEventDetail = {                    scrollTop:e.detail.scrollTop                }                // 触发事件的选项                var myEventOption = {}                // 使用 triggerEvent 方法触发自定义组件事件,指定事件名、detail对象和事件选项                        this.triggerEvent('onScroll',myEventDetail,{})            }                    },        isEnd: function () {            this.triggerEvent('loadMore')        },        calculateDistance: function (touch) {            return touch.clientY - this._initialTouch.clientY;        },        touchStart: function (e) {            //记录滑动开始的Y坐标            let clientY = e.touches[0].clientY            lastY = clientY;            if (!this.canRefresh()) return;            if (e.touches.length == 1) {                this._initialTouch = {                    clientY: e.touches[0].clientY,                    scrollTop: scrollTop                };            }        },        touchMove: function (e) {            //根据滑动位置及滑动方向 更新scrollY             //下拉刷新时使用scrollY-false禁止组件滚动的原因:防止iOS系统下拉过程中scrollTop瞬间闪动到0 导致页面闪动            let clientY = e.touches[0].clientY,                scrollY = this.data.scrollY;            let newScrollY = false;            if ((clientY - lastY) > 0 && scrollTop <= 0) {//在页面顶部 进行下拉操作                newScrollY = false            } else {                newScrollY = true            }            if (scrollY !== newScrollY) {                this.setData({                    scrollY: newScrollY                })            }            if (!this.canRefresh() || scrollTop > 0) return;            var calculateDistanceVal = this.calculateDistance(e.touches[0]);            var distance = calculateDistanceVal >= 150 ? 150 : calculateDistanceVal;            if (distance > 0 && scrollTop <= 5) {                var pullDistance = distance - this._initialTouch.scrollTop;                if (pullDistance < 0) {                    pullDistance = 0;                    this._initialTouch.scrollTop = distance;                }                // var pullHeight = this.easing(pullDistance);                var pullHeight = pullDistance / 4;                this.setData({                    loaderState: pullHeight > 30 ? STATS.enough : STATS.pulling,                    pullDownHeight: pullHeight                });            }        },        touchEnd: function (e) {            //滑动结束后 列表应该是可滚动的状态            this.setData({                scrollY: true            })            if (!this.canRefresh()) return;            // if (this.data.ifScroll > 0) return;            var endState = {                loaderState: STATS.reset,                pullDownHeight: 0            };            if (this.data.loaderState == STATS.enough) {                this.setData({                    loaderState: STATS.refreshing,                });                setTimeout(() => {                    this.triggerEvent('onRefresh')                }, 300);            } else {                this.setData(endState)            }        },        easing: function (distance) {            // t: current time, b: begInnIng value, c: change In value, d: duration            var t = distance;            var b = 0;            var d = 170; // 允许拖拽的最大距离            var c = d / 2.5; // 提示标签最大有效拖拽距离            return c * Math.sin(t / d * (Math.PI / 2)) + b;        },        canRefresh: function () {            let {                onRefresh,                loaderState            } = this.data            return onRefresh && [STATS.refreshing, STATS.loading].indexOf(loaderState) < 0;        },    }})
scrollList.json
{    "component": true,    "usingComponents": {    }}
scrollList.wxml
<scroll-view style="height:{{height}}" scroll-y="{{scrollY}}" upper-threshold="0" lower-threshold="200"    enable-back-to-top="true" class="tloader state-{{loaderState}}" bindscroll="onScroll" data-isscroll="{{isScroll}}" bindscrolltolower="isEnd"    bindtouchstart="touchStart" bindtouchend="touchEnd">    <view class="tloader-symbol">        <view class="tloader-msg">            <!-- <image class="img"></image> -->            <text/>        </view>        <view class="tloader-loading">            <!-- <text class="ui-loading" /> -->            <image src="../../images/gray-loading.png" class="ui-loading-img"></image>        </view>    </view>    <view class="tloader-body" bindtouchmove="touchMove" style="transform: translate3D(0,{{pullDownHeight+'px'}},0)">        <slot wx:if="{{!isEmpty}}"></slot>        <view class="empty" wx:else>            <!-- <view class="icon-empty" /> -->            <van-loading type="spinner" size="120rpx" color="#c9c9c9" />            <view>                <text>暂时没有数据</text>            </view>        </view>    </view></scroll-view>
scrollList.wxss
.tloader-msg:after {content: '下拉刷新';}.state-reset .tloader-msg:after {content: '';}.state-pulling.enough .tloader-msg:after {content: '释放更新';}.state-refreshed .tloader-msg:after {/\* content: '刷新成功'; \*/content: '';  }.tloader-loading:after {content: '正在加载...';}.tloader-symbol .tloader-loading:after {content: '加载更新';}.tloader-btn:after {content: '点击加载更多';}.tloader {position: relative;overflow-y: scroll;\-webkit-overflow-scrolling: touch;}/\* .tloader.state-pulling {overflow-y: hidden;} \*/.tloader-symbol {position: absolute;top: 0;left: 0;right: 0;color: #B0B0CE;text-align: center;height: 30px;/\* background-color: #FFF; \*/overflow: hidden;}.state- .tloader-symbol,.state-reset .tloader-symbol {height: 0;}.state-reset .tloader-symbol {transition: height 0s 0.2s;}.state-loading .tloader-symbol {display: none;}.tloader-msg {line-height: 30px;font-size: 12px;}.state-pulling .tloader-msg text {display: inline-block;font-size: 12px;margin-right: 8px;vertical-align: middle;height: 1em;border-left: 1px solid;position: relative;transition: transform .3s ease;}.state-pulling .tloader-msg text:before,.state-reset .tloader-msg text:before,.state-pulling .tloader-msg text:after,.state-reset .tloader-msg text:after {content: '';position: absolute;font-size: .5em;width: 1em;bottom: 0px;border-top: 1px solid;}.state-pulling .tloader-msg text:before,.state-reset .tloader-msg text:before {right: 1px;transform: rotate(50deg);transform-origin: right;}  .state-pulling .tloader-msg text:after,.state-reset .tloader-msg text:after {left: 0px;transform: rotate(-50deg);transform-origin: left;}.state-pulling.enough .tloader-msg text {transform: rotate(180deg);}.state-refreshing .tloader-msg {height: 0;opacity: 0;}/\* .state-refreshed .tloader-msg {opacity: 1;transition: opacity 1s;}.state-refreshed .tloader-msg text {display: inline-block;box-sizing: content-box;vertical-align: middle;margin-right: 10px;font-size: 20px;height: 1em;width: 1em;border: 1px solid;border-radius: 100%;position: relative;}.state-refreshed .tloader-msg text:before {content: '';position: absolute;top: 3px;left: 7px;height: 12px;width: 5px;border: solid;border-width: 0 1px 1px 0;transform: rotate(40deg);} \*/.tloader-body {margin-top: -1px;padding-top: 1px;}.state-refreshing .tloader-body {transform: translate3d(0, 60px, 0);transition: transform 0.2s;}.state-reset .tloader-body {transition: transform 0.2s;}.state-refreshing .tloader-footer {display: none;}.tloader-footer .tloader-btn {color: #B0B0CE;font-size: .9em;text-align: center;line-height: 60px;}.state-loading .tloader-footer .tloader-btn {display: none;}.tloader-loading {display: none;text-align: center;line-height: 30px;font-size: 12px;color: #B0B0CE;}.tloader-loading .ui-loading {font-size: 20px;margin-right: .6rem;}.state-refreshing .tloader-symbol .tloader-loading,.state-loading .tloader-footer .tloader-loading {display: block;}@keyframes circle {100% {transform: rotate(360deg);}}.ui-loading {margin-top: 6px;/\* display: inline-block;vertical-align: middle;font-size: 1.5rem;width: 1em;height: 1em;border: 2px solid #9494b6;border-top-color: #fff;border-radius: 100%;animation: circle .8s infinite linear; \*/}.ui-loading-img{width:30rpx;height:30rpx;position: absolute;bottom: 14rpx;left: 290rpx;display: inline-block;/\* vertical-align: middle; \*/animation: a 1s steps(12) infinite;background-size: 100%;}/\* background-image:url('data:image/png;base64,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'); } \*/@keyframes a {0% {\-webkit-transform: rotate(0deg);transform: rotate(0deg);}to {\-webkit-transform: rotate(1turn);transform: rotate(1turn);}}.empty{color: #B0B0CE;text-align: center;margin: 0 auto;padding: 100rpx 100rpx;background-color: #FFF;}.icon-empty{width: 120rpx;height: 120rpx;display: inline-block;background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAABACAYAAACndwGZAAABdUlEQVR4Xu2bwU3DQBBFZ4SbgAIoACiABjhZWrsBQgE0QBtJB163QAmkARqgCeRFzgFBnHX0Eafd56PXPvznN39Odstc4zg+p5Qecuel3A8h3J/K4rmAwzDs3P2xFAC5HCGEkwwA8wcw8xjdlm5M13Uv0iiVDuRcvuwonXux9PNfYGKMm5TSVemhc/maptm2bfsxnx+DeauhV1Y+/F0IYQ+YJaEsmK2ZXdc6StM0PfV9/74wplYgrGvhy7OuM7A8xvhpZhcCzCoencGkKpKKIQGzMko/jfne4yLgIh4fhmHv7jeHdX00SoABzFJyjMkMPmAAo+0EjMEYjNEIYIzGi47BGIzRCGCMxouOwRiM0QhgjMaLjsEYjNEIYIzGi47BGIzRCGCMxouOwRiM0QhgjMaLjsEYjNEIYIzGi47BGIzRCGCMxouOwRiM0QhgjMaLjsGY/zNml1I6/EVa4+XuGzO7nLPz90nGAMCsgHk1s6bG0VnL/AW4jKZ4roy8ugAAAABJRU5ErkJggg==) no-repeat;/\* background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAGI0lEQVRoQ9WaaWyUVRSGnzPMVCpYUHYCakTFBTGItKEzLRUigiYIKkRQY4KiMYhijBsom8APJUQlwSBRjBsIiAYVAj+g7XwDtoigCBJAQVkSdqKEVNqZY+6db8ZhOl2m0HZ6f97vLu97l3PPec8nXIKiRbQlQj5h/Ah9gOuBDkAb4HIgApwBDqPsxsNWoIQg2wT0YiBIQztrf3y0ZhQwDuEeoHUDxjqEsAJhsZTyWwP6kzYBF/hEhJeA7g2ZNEUfswvriDBNNrElnTHTIqAFjER5C7ghaRIDYAfgoPyE8DsRDqJUoPjIIpswXYHrEG5DKQRuR/CkGGcpwmQJcrw+ROpFQPPIwccS4IELBlX24WExEZZJiL/qM2GsjRbRlTBjUSYANyf1PYHyhIRYXdeYdRLQAvqirExa9T0IUwmySqIXtMFFQfAzCmEOcFPCQGZX54jDG7UNXisB9XM3wjeuJTHj/Iswk3PMk61UNhh1qktwC1lcZe/VdMCX0GQJ3ZggKwinmq9GAhqgCPg+Afx+YJQ4/HwpgSePpQHygBVAz0QS4jC+3gQ0n4F4WA+0dTuVc557pZyTjQk+fj/y6IGPtWDflFiZKQ4zkuevtgOaSwey2A70sI2FbShDxOF0U4CPkyigE0oQ6O3WhfEwWEopTcRRnUCAr4GRbqNf8XKXFHOiKcHHSeRzDR77LnRy6w4a85u4mBcQ0AImoHzgNj5NhH6yiT+bA3ycRCFDCbM2/mYIn0uQR2Pf4wS0P+3IZh/Q0T06oyVozWezFw2wEHjGBaIo+RLih+gJj9UGmA1MdcGvliD3NzvyGLbo4u6NHyVhvQSt/xUlYL3JKvuSXglUAbeKw55MIWAxBngdeDO+CxHukE1sjxII8BSwyP34iTg8nkngXYxmcc0ix0z7QnGYGCNgTFOBBd2KXClJzyNsKrLq5z2ESe58p/DSRazd93HMveU7xKFvUwFKdx4tYBBKcbyfUCQaYDSw3K00zpM5axlZtAgvVdbNbu8CnCnqZy7Ca7bCw6Dkly7TmGiAL4ExrrVcY3ZglXHSgDBe2ksxZzMNdCIe9fMCwny37qghsMsGFMI+CVaLtDKOixaQi1LmAjtsCBgnzZypjeIwOOMQpwCkAZ5zw9J5hoAJTLzASnHshW5RxRA470ZAS8VhXItCb1wJDVizZBy4VeLwYEskYEJEE7iXSohBLZHAGmA4cEicC+LQjOaiRbSWYirMEZoHvGh8OpR2EuKfjEbuglM/AQnhiKu2mTDSONfDJMi6TCdghTYvYyXEItEiOlLFMQtfeFuCvJzxBPw8jYcDZrFj7rR52XKB/Tj0uljJu7EXQANsc8WGMzECzwPvtASHTvMZiofp4uCPnvpoRGaiHSNZmITEt+IworFXsaHja4AgwnIJsiBOwJIoYAHKs9YawQBxbBYlo4oGGAZ8ZUS3mDb0vyph5O4qG/m3RdhMEH8m3QWbWMnmF2CDiYVjK5ssbE1BrcxtymRxeDdTtkADVrV+BQ+9pdQed9fyJyC0IVsl5Qj9gAoiDDTSRXOT0HwG4CGEMEOCzE3EU10b9dMboRzIMe4FSp6EONJcJDSqVBszf4RT+GWX9Z7jJWV+wH2djazYCthJJUOkjKNNTULz6YyHDUA3vPSXYg4kY6g5wWFeO+F919TuBu4Thz+aioQW0pOIzRH0QhkqISu1Vyu1p5iiip0RVs1OnMDDI1JqEx+NWlz9x6gP7YDR4vBdTRPWneQrZARhPkXsnTBvxEIqmSJl/H2pWWhf2pDDHJRJCGdQHpIQG2ubp04CprNGL/ZnwJ3uYMb5m4WXD41PfrFEdDiXcZbxKNOArihb8DEm1Zmv9x1IbuiqYiZuMBL8Fe734yiLgS8kxM50iWg+ffDwMNhccWfgLMosujO/pqxkgwnEOmoeXcjiVZQnE5Ri83kvSglQhmLyyAfw2WNWQRXZRMihFdcCNxrTDDZbH834K+fw8BHnmZ2utavXEUq1sq4DaFSMx1AGpPhtoK4NUYQfUZYR5mPZzKm6OqRtheo7oJqMIgxB7R0x6vbVqP03ItuGSdGjcRLB5JrNXyllhCmRzRyu7xw1tfsPIHjcXpliZvQAAAAASUVORK5CYII='); \*/  background-size: 100% 100%;}    .img{width:130rpx;height:130rpx;}