zhangfei
2025-06-03 14c90f4513a26dbc30960121c57a3cdf60996aa3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
// https://uniapp.dcloud.io/frame?id=wxs
// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html 
 
// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
var me = {}
 
// ------ 自定义下拉刷新动画 start ------
 
/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
me.onMoving = function (ins, rate, downHight){
    ins.requestAnimationFrame(function () {
        ins.selectComponent('.mescroll-wxs-content').setStyle({
            'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
            'transform': 'translateY(' + downHight + 'px)',
            'transition': ''
        })
        // 环形进度条
        var progress = ins.selectComponent('.mescroll-wxs-progress')
        progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
    })
}
 
/* 显示下拉刷新进度 */
me.showLoading = function (ins){
    me.downHight = me.optDown.offset
    ins.requestAnimationFrame(function () {
        ins.selectComponent('.mescroll-wxs-content').setStyle({
            'will-change': 'auto',
            'transform': 'translateY(' + me.downHight + 'px)',
            'transition': 'transform 300ms'
        })
    })
}
 
/* 结束下拉 */
me.endDownScroll = function (ins){
    me.downHight = 0;
    me.isDownScrolling = false;
    ins.requestAnimationFrame(function () {
        ins.selectComponent('.mescroll-wxs-content').setStyle({
            'will-change': 'auto',
            'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
            'transition': 'transform 300ms'
        })
    })
}
 
/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
me.clearTransform = function (ins){
    ins.requestAnimationFrame(function () {
        ins.selectComponent('.mescroll-wxs-content').setStyle({
            'will-change': '',
            'transform': '',
            'transition': ''
        })
    })
}
 
// ------ 自定义下拉刷新动画 end ------
 
/**
 * 监听逻辑层数据的变化 (实时更新数据)
 */
function propObserver(wxsProp) {
    me.optDown = wxsProp.optDown
    me.scrollTop = wxsProp.scrollTop
    me.bodyHeight = wxsProp.bodyHeight
    me.isDownScrolling = wxsProp.isDownScrolling
    me.isUpScrolling = wxsProp.isUpScrolling
    me.isUpBoth = wxsProp.isUpBoth
    me.isScrollBody = wxsProp.isScrollBody
    me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
}
 
/**
 * 监听逻辑层数据的变化 (调用wxs的方法)
 */
function callObserver(callProp, oldValue, ins) {
    if (me.disabled()) return;
    if(callProp.callType){
        // 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
        if(callProp.callType === 'showLoading'){
            me.showLoading(ins)
        }else if(callProp.callType === 'endDownScroll'){
            me.endDownScroll(ins)
        }else if(callProp.callType === 'clearTransform'){
            me.clearTransform(ins)
        }
    }
}
 
/**
 * touch事件
 */
function touchstartEvent(e, ins) {
    me.downHight = 0; // 下拉的距离
    me.startPoint = me.getPoint(e); // 记录起点
    me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
    me.startAngle = 0; // 初始角度
    me.lastPoint = me.startPoint; // 重置上次move的点
    me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
    me.inTouchend = false; // 标记不是touchend
    
    me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
}
 
function touchmoveEvent(e, ins) {
    var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
    
    if (me.disabled()) return isPrevent;
    
    var scrollTop = me.getScrollTop(); // 当前滚动条的距离
    var curPoint = me.getPoint(e); // 当前点
    
    var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
    
    // 向下拉 && 在顶部
    // mescroll-body,直接判定在顶部即可
    // scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
    // scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
    if (moveY > 0 && (
            (me.isScrollBody && scrollTop <= 0)
            ||
            (!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
        )) {
        // 可下拉的条件
        if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
                me.isUpBoth))) {
    
            // 下拉的角度是否在配置的范围内
            if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
            if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
    
            // 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
            if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
                me.inTouchend = true; // 标记执行touchend
                touchendEvent(e, ins); // 提前触发touchend
                return isPrevent;
            }
            
            isPrevent = false // 小程序是return false
    
            var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
    
            // 下拉距离  < 指定距离
            if (me.downHight < me.optDown.offset) {
                if (me.movetype !== 1) {
                    me.movetype = 1; // 加入标记,保证只执行一次
                    // me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
                    me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
                    me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
                }
                me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
    
                // 指定距离  <= 下拉距离
            } else {
                if (me.movetype !== 2) {
                    me.movetype = 2; // 加入标记,保证只执行一次
                    // me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
                    me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
                    me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
                }
                if (diff > 0) { // 向下拉
                    me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
                } else { // 向上收
                    me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
                }
            }
            
            me.downHight = Math.round(me.downHight) // 取整
            var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
            // me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
            me.onMoving(ins, rate, me.downHight)
        }
    }
    
    me.lastPoint = curPoint; // 记录本次移动的点
    
    return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
}
 
function touchendEvent(e, ins) {
    // 如果下拉区域高度已改变,则需重置回来
    if (me.isMoveDown) {
        if (me.downHight >= me.optDown.offset) {
            // 符合触发刷新的条件
            me.downHight = me.optDown.offset; // 更新下拉区域高度
            // me.triggerDownScroll();
            me.callMethod(ins, {type: 'triggerDownScroll'})
        } else {
            // 不符合的话 则重置
            me.downHight = 0;
            // me.optDown.endDownScroll && me.optDown.endDownScroll(me);
            me.callMethod(ins, {type: 'endDownScroll'})
        }
        me.movetype = 0;
        me.isMoveDown = false;
    } else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
        var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
        // 上滑
        if (isScrollUp) {
            // 需检查滑动的角度
            var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
            if (angle > 80) {
                // 检查并触发上拉
                // me.triggerUpScroll(true);
                me.callMethod(ins, {type: 'triggerUpScroll'})
            }
        }
    }
    me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
}
 
/* 是否禁用下拉刷新 */
me.disabled = function(){
    return !me.optDown || !me.optDown.use || me.optDown.native
}
 
/* 根据点击滑动事件获取第一个手指的坐标 */
me.getPoint = function(e) {
    if (!e) {
        return {x: 0,y: 0}
    }
    if (e.touches && e.touches[0]) {
        return {x: e.touches[0].pageX,y: e.touches[0].pageY}
    } else if (e.changedTouches && e.changedTouches[0]) {
        return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}
    } else {
        return {x: e.clientX,y: e.clientY}
    }
}
 
/* 计算两点之间的角度: 区间 [0,90]*/
me.getAngle = function (p1, p2) {
    var x = Math.abs(p1.x - p2.x);
    var y = Math.abs(p1.y - p2.y);
    var z = Math.sqrt(x * x + y * y);
    var angle = 0;
    if (z !== 0) {
        angle = Math.asin(y / z) / Math.PI * 180;
    }
    return angle
}
 
/* 获取滚动条的位置 */
me.getScrollTop = function() {
    return me.scrollTop || 0
}
 
/* 获取body的高度 */
me.getBodyHeight = function() {
    return me.bodyHeight || 0;
}
 
/* 调用逻辑层的方法 */
me.callMethod = function(ins, param) {
    if(ins) ins.callMethod('wxsCall', param)
}
 
/* 导出模块 */
module.exports = {
    propObserver: propObserver,
    callObserver: callObserver,
    touchstartEvent: touchstartEvent,
    touchmoveEvent: touchmoveEvent,
    touchendEvent: touchendEvent
}