z-paging-wxs.wxs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. // [z-paging]微信小程序、QQ小程序、app-vue、h5上使用wxs实现自定义下拉刷新,降低逻辑层与视图层的通信折损,提升性能
  2. var currentDis = 0;
  3. var isPCFlag = -1;
  4. var startY = -1;
  5. // 监听js层传过来的数据
  6. function propObserver(newVal, oldVal, ownerIns, ins) {
  7. var state = ownerIns.getState() || {};
  8. state.currentIns = ins;
  9. var dataset = ins.getDataset();
  10. var loading = dataset.loading == true;
  11. // 如果是下拉刷新结束,更新transform
  12. if (newVal && newVal.indexOf('end') != -1) {
  13. var transition = newVal.split('end')[0];
  14. _setTransform('translateY(0px)', ins, false, transition);
  15. state.moveDis = 0;
  16. state.oldMoveDis = 0;
  17. currentDis = 0;
  18. } else if (newVal && newVal.indexOf('begin') != -1) {
  19. // 如果是下拉刷新开始,更新transform
  20. var refresherThreshold = ins.getDataset().refresherthreshold;
  21. _setTransformValue(refresherThreshold, ins, state, false);
  22. }
  23. }
  24. // touch开始
  25. function touchstart(e, ownerIns) {
  26. var ins = _getIns(ownerIns);
  27. var state = {};
  28. var dataset = {};
  29. ownerIns.callMethod('_handleListTouchstart');
  30. if (ins) {
  31. state = ins.getState();
  32. dataset = ins.getDataset();
  33. if (_touchDisabled(e, ins, 0)) return;
  34. }
  35. var isTouchEnded = state.isTouchEnded;
  36. state.oldMoveDis = 0;
  37. var touch = _getTouch(e);
  38. var loading = _isTrue(dataset.loading);
  39. state.startY = touch.touchY;
  40. startY = state.startY;
  41. state.lastTouch = touch;
  42. if (!loading && isTouchEnded) {
  43. state.isTouchmoving = false;
  44. }
  45. state.isTouchEnded = false;
  46. // 通知js层touch开始
  47. ownerIns.callMethod('_handleRefresherTouchstart', touch);
  48. }
  49. // touch中
  50. function touchmove(e, ownerIns) {
  51. var touch = _getTouch(e);
  52. var ins = _getIns(ownerIns);
  53. var dataset = ins.getDataset();
  54. var refresherThreshold = dataset.refresherthreshold;
  55. var refresherF2Threshold = dataset.refresherf2threshold;
  56. var refresherF2Enabled = _isTrue(dataset.refresherf2enabled);
  57. var isIos = _isTrue(dataset.isios);
  58. var state = ins.getState();
  59. var watchTouchDirectionChange = _isTrue(dataset.watchtouchdirectionchange);
  60. var moveDisObj = {};
  61. var moveDis = 0;
  62. var prevent = false;
  63. // 如果需要监听touch方向的改变
  64. if (watchTouchDirectionChange) {
  65. moveDisObj = _getMoveDis(e, ins);
  66. moveDis = moveDisObj.currentDis;
  67. prevent = moveDisObj.isDown;
  68. var direction = prevent ? 'top' : 'bottom';
  69. // 确保只在touch方向改变时通知一次js层,而不是touchmove中持续通知
  70. if (prevent == state.oldTouchDirection && prevent != state.oldEmitedTouchDirection) {
  71. ownerIns.callMethod('_handleTouchDirectionChange', { direction: direction });
  72. state.oldEmitedTouchDirection = prevent;
  73. }
  74. state.oldTouchDirection = prevent;
  75. }
  76. // 判断是否允许下拉刷新
  77. if (_touchDisabled(e, ins, 1)) {
  78. _handlePullingDown(state, ownerIns, false);
  79. return true;
  80. }
  81. // 判断下拉刷新的角度是否在要求范围内
  82. if (!_getAngleIsInRange(e, touch, state, dataset)) {
  83. _handlePullingDown(state, ownerIns, false);
  84. return true;
  85. }
  86. moveDisObj = _getMoveDis(e, ins);
  87. moveDis = moveDisObj.currentDis;
  88. prevent = moveDisObj.isDown;
  89. if (moveDis < 0) {
  90. // moveDis小于0,将transform重置为0
  91. _setTransformValue(0, ins, state, false);
  92. _handlePullingDown(state, ownerIns, false);
  93. return true;
  94. }
  95. if (prevent && !state.disabledBounce) {
  96. // 如果是用户下拉并且需要触发下拉刷新,需要通知js层将列表禁止滚动,防止在下拉刷新过程中列表也可以滚动导致的下拉刷新偏移过大的问题(在下拉刷新过程中仅通知一次)
  97. ownerIns.callMethod('_handleScrollViewBounce', { bounce: false });
  98. state.disabledBounce = true;
  99. _handlePullingDown(state, ownerIns, prevent);
  100. return !prevent;
  101. }
  102. // 更新transform
  103. _setTransformValue(moveDis, ins, state, false);
  104. var oldRefresherStatus = state.refresherStatus;
  105. var oldIsTouchmoving = _isTrue(dataset.oldistouchmoving);
  106. var hasTouchmove = _isTrue(dataset.hastouchmove);
  107. var isTouchmoving = state.isTouchmoving;
  108. state.refresherStatus = moveDis >= refresherThreshold ? (refresherF2Enabled && moveDis > refresherF2Threshold ? 'goF2' : 'releaseToRefresh') : 'default';
  109. if (!isTouchmoving) {
  110. state.isTouchmoving = true;
  111. isTouchmoving = true;
  112. }
  113. if (state.isTouchEnded) {
  114. state.isTouchEnded = false;
  115. }
  116. // 如果需要实时监听下拉位置偏移,则需要实时通知js层,此操作会使wxs层与js层频繁通信从而导致在一些性能较差设备中下拉刷新卡顿
  117. if (hasTouchmove) {
  118. ownerIns.callMethod('_handleWxsPullingDown', { moveDis: moveDis, diffDis: moveDisObj.diffDis });
  119. }
  120. // 在下拉刷新状态改变时通知js层
  121. if (oldRefresherStatus == undefined || oldRefresherStatus != state.refresherStatus || oldIsTouchmoving != isTouchmoving) {
  122. ownerIns.callMethod('_handleRefresherTouchmove', moveDis, touch);
  123. }
  124. _handlePullingDown(state, ownerIns, prevent);
  125. return !prevent;
  126. }
  127. // touch结束
  128. function touchend(e, ownerIns) {
  129. var touch = _getTouch(e);
  130. var ins = _getIns(ownerIns);
  131. var dataset = ins.getDataset();
  132. var state = ins.getState();
  133. if (state.disabledBounce) {
  134. // 通知js允许列表滚动
  135. ownerIns.callMethod('_handleScrollViewBounce', { bounce: true });
  136. state.disabledBounce = false;
  137. }
  138. if (_touchDisabled(e, ins, 2)) return;
  139. state.reachMaxAngle = true;
  140. state.hitReachMaxAngleCount = 0;
  141. state.fixedIsTopHitCount = 0;
  142. if (!state.isTouchmoving) return;
  143. var oldRefresherStatus = state.refresherStatus;
  144. var oldMoveDis = state.moveDis;
  145. var refresherThreshold = ins.getDataset().refresherthreshold;
  146. var moveDis = _getMoveDis(e, ins).currentDis;
  147. if (!(moveDis >= refresherThreshold && oldRefresherStatus === 'releaseToRefresh')) {
  148. state.isTouchmoving = false;
  149. }
  150. // 通知js层touch结束
  151. ownerIns.callMethod('_handleRefresherTouchend', moveDis);
  152. state.isTouchEnded = true;
  153. if (oldMoveDis < refresherThreshold) return;
  154. var animate = false;
  155. if (moveDis >= refresherThreshold) {
  156. moveDis = refresherThreshold;
  157. animate = true;
  158. }
  159. _setTransformValue(moveDis, ins, state, animate);
  160. }
  161. // #ifdef H5
  162. // 判断是否是pc平台
  163. function isPC() {
  164. if (!navigator) return false;
  165. if (isPCFlag != -1) return isPCFlag;
  166. var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
  167. isPCFlag = agents.every(function(item) { return navigator.userAgent.indexOf(item) < 0 });
  168. return isPCFlag;
  169. }
  170. var movable = false;
  171. // 在pc平台监听mousedown、mousemove、mouseup等相关事件并转为对应touch事件处理,使得在pc平台也支持通过鼠标进行下拉刷新
  172. function mousedown(e, ins) {
  173. if (!isPC()) return;
  174. touchstart(e, ins);
  175. movable = true;
  176. }
  177. function mousemove(e, ins) {
  178. if (!isPC() || !movable) return;
  179. touchmove(e, ins);
  180. }
  181. function mouseup(e, ins) {
  182. if (!isPC()) return;
  183. touchend(e, ins);
  184. movable = false;
  185. }
  186. function mouseleave(e, ins) {
  187. if (!isPC()) return;
  188. movable = false;
  189. }
  190. // #endif
  191. // 修改视图层transform
  192. function _setTransformValue(value, ins, state, animate) {
  193. value = value || 0;
  194. if (state.moveDis == value) return;
  195. state.moveDis = value;
  196. _setTransform('translateY(' + value + 'px)', ins, animate, '');
  197. }
  198. // 设置视图层transform,直接在视图层操作下拉刷新,使得js层不需要频繁和视图层通信,从而大大提升下拉刷新性能
  199. function _setTransform(transform, ins, animate, transition) {
  200. var dataset = ins.getDataset();
  201. if (_isTrue(dataset.refreshernotransform)) return;
  202. transform = transform == 'translateY(0px)' ? 'none' : transform;
  203. ins.requestAnimationFrame(function() {
  204. var stl = { 'transform': transform };
  205. if (animate) {
  206. stl['transition'] = 'transform .1s linear';
  207. }
  208. if (transition.length) {
  209. stl['transition'] = transition;
  210. }
  211. ins.setStyle(stl);
  212. })
  213. }
  214. // 进一步处理下拉刷新的偏移数据
  215. function _getMoveDis(e, ins) {
  216. var state = ins.getState();
  217. var refresherThreshold = parseFloat(ins.getDataset().refresherthreshold);
  218. var refresherOutRate = parseFloat(ins.getDataset().refresheroutrate);
  219. var refresherPullRate = parseFloat(ins.getDataset().refresherpullrate);
  220. var touch = _getTouch(e);
  221. var currentStartY = !state.startY || state.startY == 'NaN' ? startY : state.startY;
  222. var moveDis = touch.touchY - currentStartY;
  223. var oldMoveDis = state.oldMoveDis || 0;
  224. state.oldMoveDis = moveDis;
  225. // 获取当前下拉刷新位置与上次的偏移量
  226. var diffDis = moveDis - oldMoveDis;
  227. if (diffDis > 0) {
  228. // 对偏移量进行进一步处理,通过refresherPullRate等配置进行约束
  229. diffDis = diffDis * refresherPullRate;
  230. if (currentDis > refresherThreshold) {
  231. diffDis = diffDis * (1 - refresherOutRate);
  232. }
  233. }
  234. // 控制diffDis过大的情况,比如进入页面突然猛然下拉,此时diffDis不应进行太大的偏移
  235. diffDis = diffDis > 100 ? diffDis / 100 : (diffDis > 20 ? diffDis / 2.2 : diffDis);
  236. currentDis += diffDis;
  237. currentDis = Math.max(0, currentDis);
  238. return {
  239. currentDis: currentDis,
  240. diffDis: diffDis,
  241. isDown: diffDis > 0
  242. };
  243. }
  244. // 获取经过统一格式包装的当前touch对象
  245. function _getTouch(e) {
  246. var touch = e;
  247. if (e.touches && e.touches.length) {
  248. touch = e.touches[0];
  249. } else if (e.changedTouches && e.changedTouches.length) {
  250. touch = e.changedTouches[0];
  251. } else if (e.datail && e.datail != {}) {
  252. touch = e.datail;
  253. }
  254. return {
  255. touchX: touch.clientX,
  256. touchY: touch.clientY
  257. };
  258. }
  259. // 获取当前currentIns
  260. function _getIns(ownerIns) {
  261. var ins = ownerIns.getState().currentIns;
  262. if (!ins) {
  263. ownerIns.callMethod('_handlePropUpdate');
  264. }
  265. return ins;
  266. }
  267. // 判断当前状态是否允许下拉刷新
  268. function _touchDisabled(e, ins, processTag) {
  269. var dataset = ins.getDataset();
  270. var state = ins.getState();
  271. var loading = _isTrue(dataset.loading);
  272. var useChatRecordMode = _isTrue(dataset.usechatrecordmode);
  273. var refresherEnabled = _isTrue(dataset.refresherenabled);
  274. var useCustomRefresher = _isTrue(dataset.usecustomrefresher);
  275. var usePageScroll = _isTrue(dataset.usepagescroll);
  276. var pageScrollTop = parseFloat(dataset.pagescrolltop);
  277. var scrollTop = parseFloat(dataset.scrolltop);
  278. var finalScrollTop = usePageScroll ? pageScrollTop : scrollTop;
  279. var fixedIsTop = false;
  280. // 是否要处理滚动到顶部scrollTop不为0时候的容错,为解决在安卓中scroll-view有概率滚动到顶部时scrollTop不为0导致下拉刷新判断异常,但此方案会导致某些情况(例如滚动到距离顶部10px处)下拉抖动,因此改为通过获取zp-scroll-view的节点信息中的scrollTop进行验证的方案
  281. var handleFaultTolerantMove = false;
  282. if (handleFaultTolerantMove && finalScrollTop == (state.startScrollTop || 0) && finalScrollTop <= 105) {
  283. fixedIsTop = true;
  284. }
  285. var fixedIsTopHitCount = state.fixedIsTopHitCount || 0;
  286. if (fixedIsTop) {
  287. fixedIsTopHitCount ++;
  288. if (fixedIsTopHitCount <= 2) {
  289. fixedIsTop = false;
  290. }
  291. state.fixedIsTopHitCount = fixedIsTopHitCount;
  292. } else {
  293. state.fixedIsTopHitCount = 0;
  294. }
  295. if (handleFaultTolerantMove && processTag === 0) {
  296. state.startScrollTop = finalScrollTop || 0;
  297. }
  298. if (handleFaultTolerantMove && processTag === 2) {
  299. fixedIsTop = true;
  300. }
  301. return loading || useChatRecordMode || !refresherEnabled || !useCustomRefresher ||
  302. ((usePageScroll && useCustomRefresher && pageScrollTop > 5) && !fixedIsTop) ||
  303. ((!usePageScroll && useCustomRefresher && scrollTop > 5) && !fixedIsTop);
  304. }
  305. // 判断下拉刷新的角度是否在要求范围内
  306. function _getAngleIsInRange(e, touch, state, dataset) {
  307. var maxAngle = dataset.refreshermaxangle;
  308. var refresherAecc = _isTrue(dataset.refresheraecc);
  309. var lastTouch = state.lastTouch;
  310. var reachMaxAngle = state.reachMaxAngle;
  311. var moveDis = state.oldMoveDis;
  312. if (!lastTouch) return true;
  313. if (maxAngle >= 0 && maxAngle <= 90 && lastTouch) {
  314. // 考虑下拉刷新手势由水平移动转为垂直方向移动的情况,此时不应当只判断垂直方向角度是否符合要求,应当直接禁止以避免在swiper中使用下拉刷新时,横向切换swiper途中手未离开屏幕还可以下拉刷新的问题
  315. if ((!moveDis || moveDis < 1) && !refresherAecc && reachMaxAngle != null && !reachMaxAngle) return false;
  316. var x = Math.abs(touch.touchX - lastTouch.touchX);
  317. var y = Math.abs(touch.touchY - lastTouch.touchY);
  318. var z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
  319. if ((x || y) && x > 1) {
  320. // 获取下拉刷新前后两次位移的角度
  321. var angle = Math.asin(y / z) / Math.PI * 180;
  322. if (angle < maxAngle) {
  323. // 如果角度小于配置要求,则return,同时通过hitReachMaxAngleCount控制角度判断的灵敏程度以最大程度兼容各种使用场景
  324. var hitReachMaxAngleCount = state.hitReachMaxAngleCount || 0;
  325. state.hitReachMaxAngleCount = ++hitReachMaxAngleCount;
  326. if (state.hitReachMaxAngleCount > 2) {
  327. state.lastTouch = touch;
  328. state.reachMaxAngle = false;
  329. }
  330. return false;
  331. }
  332. }
  333. }
  334. state.lastTouch = touch;
  335. return true;
  336. }
  337. // 进一步处理是否在下拉刷新并通知js层
  338. function _handlePullingDown(state, ins, onPullingDown) {
  339. var oldOnPullingDown = state.onPullingDown || false;
  340. if (oldOnPullingDown != onPullingDown) {
  341. ins.callMethod('_handleWxsPullingDownStatusChange', onPullingDown);
  342. }
  343. state.onPullingDown = onPullingDown;
  344. }
  345. // 判断js层传过来的值是否为true
  346. function _isTrue(value) {
  347. value = (typeof(value) === 'string' ? JSON.parse(value) : value) || false;
  348. return value == true || value == 'true';
  349. }
  350. module.exports = {
  351. touchstart: touchstart,
  352. touchmove: touchmove,
  353. touchend: touchend,
  354. mousedown: mousedown,
  355. mousemove: mousemove,
  356. mouseup: mouseup,
  357. mouseleave: mouseleave,
  358. propObserver: propObserver
  359. }