小程序可以利用<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,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='); } \*/

@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,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'); \*/

  

background-size: 100% 100%;

}

  
  

.img{

width:130rpx;

height:130rpx;

}

半亩花田
30 声望4 粉丝