wxs.wxs 9.7 KB

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