mescroll-uni.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. /* mescroll-uni
  2. * version 1.1.8
  3. * 2019-11-01 wenju
  4. * http://www.mescroll.com
  5. */
  6. export default function MeScroll(options) {
  7. let me = this;
  8. me.version = '1.1.8'; // mescroll版本号
  9. me.options = options || {}; // 配置
  10. me.isDownScrolling = false; // 是否在执行下拉刷新的回调
  11. me.isUpScrolling = false; // 是否在执行上拉加载的回调
  12. let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
  13. // 初始化下拉刷新
  14. me.initDownScroll();
  15. // 初始化上拉加载,则初始化
  16. me.initUpScroll();
  17. // 自动加载
  18. setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
  19. // 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
  20. if (me.optDown.use && me.optDown.auto && hasDownCallback) {
  21. if (me.optDown.autoShowLoading) {
  22. me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
  23. } else {
  24. me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
  25. }
  26. }
  27. // 自动触发上拉加载
  28. me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
  29. }, 30); // 需让me.optDown.inited和me.optUp.inited先执行
  30. }
  31. /* 配置参数:下拉刷新 */
  32. MeScroll.prototype.extendDownScroll = function(optDown) {
  33. // 下拉刷新的配置
  34. MeScroll.extend(optDown, {
  35. use: true, // 是否启用下拉刷新; 默认true
  36. auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
  37. autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
  38. isLock: false, // 是否锁定下拉刷新,默认false;
  39. offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
  40. startTop: 100, // scroll-view滚动到顶部时,此时的scroll-top不一定为0, 此值用于控制最大的误差
  41. fps: 40, // 下拉节流 (值越大每秒刷新频率越高)
  42. inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
  43. outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
  44. bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
  45. minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
  46. textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
  47. textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
  48. textLoading: '加载中 ...', // 加载中的提示文本
  49. inited: null, // 下拉刷新初始化完毕的回调
  50. inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
  51. outOffset: null, // 下拉的距离大于offset那一刻的回调
  52. onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
  53. beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
  54. showLoading: null, // 显示下拉刷新进度的回调
  55. afterLoading: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
  56. endDownScroll: null, // 结束下拉刷新的回调
  57. callback: function(mescroll) {
  58. // 下拉刷新的回调;默认重置上拉加载列表为第一页
  59. mescroll.resetUpScroll();
  60. }
  61. })
  62. }
  63. /* 配置参数:上拉加载 */
  64. MeScroll.prototype.extendUpScroll = function(optUp) {
  65. // 上拉加载的配置
  66. MeScroll.extend(optUp, {
  67. use: true, // 是否启用上拉加载; 默认true
  68. auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
  69. isLock: false, // 是否锁定上拉加载,默认false;
  70. isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
  71. isBounce: false, // 默认禁止橡皮筋的回弹效果, 必读事项: http://www.mescroll.com/qa.html?v=190725#q25
  72. callback: null, // 上拉加载的回调;function(page,mescroll){ }
  73. page: {
  74. num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
  75. size: 10, // 每页数据的数量
  76. time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
  77. },
  78. noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
  79. offset: 80, // 距底部多远时,触发upCallback
  80. textLoading: '加载中 ...', // 加载中的提示文本
  81. textNoMore: '-- END --', // 没有更多数据的提示文本
  82. inited: null, // 初始化完毕的回调
  83. showLoading: null, // 显示加载中的回调
  84. showNoMore: null, // 显示无更多数据的回调
  85. hideUpScroll: null, // 隐藏上拉加载的回调
  86. toTop: {
  87. // 回到顶部按钮,需配置src才显示
  88. src: null, // 图片路径,默认null (建议写成网络图,不必考虑相对路径)
  89. offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
  90. duration: 300, // 回到顶部的动画时长,默认300ms
  91. btnClick: null, // 点击按钮的回调
  92. onShow: null // 是否显示的回调
  93. },
  94. empty: {
  95. use: true, // 是否显示空布局
  96. icon: null, // 图标路径
  97. tip: '~ 暂无相关数据 ~', // 提示
  98. btnText: '', // 按钮
  99. btnClick: null, // 点击按钮的回调
  100. onShow: null // 是否显示的回调
  101. },
  102. onScroll: false // 是否监听滚动事件
  103. })
  104. }
  105. /* 配置参数 */
  106. MeScroll.extend = function(userOption, defaultOption) {
  107. if (!userOption) return defaultOption;
  108. for (let key in defaultOption) {
  109. if (userOption[key] == null) {
  110. let def = defaultOption[key];
  111. if (def != null && typeof def === 'object') {
  112. userOption[key] = MeScroll.extend({}, def); // 深度匹配
  113. } else {
  114. userOption[key] = def;
  115. }
  116. } else if (typeof userOption[key] === 'object') {
  117. MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
  118. }
  119. }
  120. return userOption;
  121. }
  122. /* -------初始化下拉刷新------- */
  123. MeScroll.prototype.initDownScroll = function() {
  124. let me = this;
  125. // 配置参数
  126. me.optDown = me.options.down || {};
  127. me.extendDownScroll(me.optDown);
  128. me.downHight = 0; // 下拉区域的高度
  129. // 在页面中加入下拉布局
  130. if (me.optDown.use && me.optDown.inited) {
  131. // 初始化完毕的回调
  132. setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
  133. me.optDown.inited(me);
  134. }, 0)
  135. }
  136. }
  137. /* 列表touchstart事件 */
  138. MeScroll.prototype.touchstartEvent = function(e) {
  139. if (!this.optDown.use) return;
  140. this.startPoint = this.getPoint(e); // 记录起点
  141. this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
  142. this.lastPoint = this.startPoint; // 重置上次move的点
  143. this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
  144. this.inTouchend = false; // 标记不是touchend
  145. }
  146. /* 列表touchmove事件 */
  147. MeScroll.prototype.touchmoveEvent = function(e) {
  148. if (!this.optDown.use) return;
  149. if (!this.startPoint) return;
  150. let me = this;
  151. // 节流
  152. let t = new Date().getTime();
  153. if (me.moveTime && t - me.moveTime < me.moveTimeDiff) { // 小于节流时间,则不处理
  154. return;
  155. } else {
  156. me.moveTime = t
  157. me.moveTimeDiff = 1000 / me.optDown.fps
  158. }
  159. let scrollTop = me.getScrollTop(); // 当前滚动条的距离
  160. let curPoint = me.getPoint(e); // 当前点
  161. let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
  162. // (向下拉&&在顶部) scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
  163. // scroll-view滚动到顶部时,scrollTop不一定为0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
  164. if (moveY > 0 && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop))) {
  165. // 可下拉的条件
  166. if (me.optDown.use && !me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
  167. me.optUp.isBoth))) {
  168. // 下拉的角度是否在配置的范围内
  169. let x = Math.abs(me.lastPoint.x - curPoint.x);
  170. let y = Math.abs(me.lastPoint.y - curPoint.y);
  171. let z = Math.sqrt(x * x + y * y);
  172. if (z !== 0) {
  173. let angle = Math.asin(y / z) / Math.PI * 180; // 两点之间的角度,区间 [0,90]
  174. if (angle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
  175. }
  176. // 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
  177. if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
  178. me.inTouchend = true; // 标记执行touchend
  179. me.touchendEvent(); // 提前触发touchend
  180. return;
  181. }
  182. me.preventDefault(e); // 阻止默认事件
  183. let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
  184. // 下拉距离 < 指定距离
  185. if (me.downHight < me.optDown.offset) {
  186. if (me.movetype !== 1) {
  187. me.movetype = 1; // 加入标记,保证只执行一次
  188. me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
  189. me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
  190. }
  191. me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
  192. // 指定距离 <= 下拉距离
  193. } else {
  194. if (me.movetype !== 2) {
  195. me.movetype = 2; // 加入标记,保证只执行一次
  196. me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
  197. me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
  198. }
  199. if (diff > 0) { // 向下拉
  200. me.downHight += Math.round(diff * me.optDown.outOffsetRate); // 越往下,高度变化越小
  201. } else { // 向上收
  202. me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
  203. }
  204. }
  205. let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
  206. me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
  207. }
  208. }
  209. me.lastPoint = curPoint; // 记录本次移动的点
  210. }
  211. /* 列表touchend事件 */
  212. MeScroll.prototype.touchendEvent = function(e) {
  213. if (!this.optDown.use) return;
  214. // 如果下拉区域高度已改变,则需重置回来
  215. if (this.isMoveDown) {
  216. if (this.downHight >= this.optDown.offset) {
  217. // 符合触发刷新的条件
  218. this.triggerDownScroll();
  219. } else {
  220. // 不符合的话 则重置
  221. this.downHight = 0;
  222. this.optDown.endDownScroll && this.optDown.endDownScroll(this);
  223. }
  224. this.movetype = 0;
  225. this.isMoveDown = false;
  226. } else if (this.getScrollTop() === this.startTop) { // 到顶/左/右/底的滑动事件
  227. let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
  228. // 上滑 && 检查并触发上拉
  229. isScrollUp && this.triggerUpScroll(true);
  230. }
  231. }
  232. /* 根据点击滑动事件获取第一个手指的坐标 */
  233. MeScroll.prototype.getPoint = function(e) {
  234. if (!e) {
  235. return {
  236. x: 0,
  237. y: 0
  238. }
  239. }
  240. if (e.touches && e.touches[0]) {
  241. return {
  242. x: e.touches[0].pageX,
  243. y: e.touches[0].pageY
  244. }
  245. } else if (e.changedTouches && e.changedTouches[0]) {
  246. return {
  247. x: e.changedTouches[0].pageX,
  248. y: e.changedTouches[0].pageY
  249. }
  250. } else {
  251. return {
  252. x: e.clientX,
  253. y: e.clientY
  254. }
  255. }
  256. }
  257. /* 触发下拉刷新 */
  258. MeScroll.prototype.triggerDownScroll = function() {
  259. if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
  260. //return true则处于完全自定义状态
  261. } else {
  262. this.showDownScroll(); // 下拉刷新中...
  263. this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
  264. }
  265. }
  266. /* 显示下拉进度布局 */
  267. MeScroll.prototype.showDownScroll = function() {
  268. this.isDownScrolling = true; // 标记下拉中
  269. this.downHight = this.optDown.offset; // 更新下拉区域高度
  270. this.optDown.showLoading(this, this.downHight); // 下拉刷新中...
  271. }
  272. /* 结束下拉刷新 */
  273. MeScroll.prototype.endDownScroll = function() {
  274. let me = this;
  275. // 结束下拉刷新的方法
  276. let endScroll = function() {
  277. me.downHight = 0;
  278. me.isDownScrolling = false;
  279. me.optDown.endDownScroll && me.optDown.endDownScroll(me);
  280. me.setScrollHeight(0) // 重置滚动区域,使数据不满屏时仍可检查触发翻页
  281. }
  282. // 结束下拉刷新时的回调
  283. let delay = 0;
  284. if (me.optDown.afterLoading) delay = me.optDown.afterLoading(me); // 结束下拉刷新的延时,单位ms
  285. if (typeof delay === 'number' && delay > 0) {
  286. setTimeout(endScroll, delay);
  287. } else {
  288. endScroll();
  289. }
  290. }
  291. /* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
  292. MeScroll.prototype.lockDownScroll = function(isLock) {
  293. if (isLock == null) isLock = true;
  294. this.optDown.isLock = isLock;
  295. }
  296. /* -------初始化上拉加载------- */
  297. MeScroll.prototype.initUpScroll = function() {
  298. let me = this;
  299. // 配置参数
  300. me.optUp = me.options.up || {
  301. use: false
  302. };
  303. me.extendUpScroll(me.optUp);
  304. if (!me.optUp.isBounce) me.setBounce(false); // 不允许bounce时,需禁止window的touchmove事件
  305. if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
  306. me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
  307. me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
  308. // 初始化完毕的回调
  309. if (me.optUp.inited) {
  310. setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
  311. me.optUp.inited(me);
  312. }, 0)
  313. }
  314. }
  315. /*列表滚动事件*/
  316. MeScroll.prototype.scroll = function(e, onScroll) {
  317. // 更新滚动条的位置
  318. this.setScrollTop(e.scrollTop);
  319. // 更新滚动内容高度
  320. this.setScrollHeight(e.scrollHeight);
  321. // 向上滑还是向下滑动
  322. if (this.preScrollY == null) this.preScrollY = 0;
  323. this.isScrollUp = e.scrollTop - this.preScrollY > 0;
  324. this.preScrollY = e.scrollTop;
  325. // 上滑 && 检查并触发上拉
  326. this.isScrollUp && this.triggerUpScroll(true);
  327. // 顶部按钮的显示隐藏
  328. if (e.scrollTop >= this.optUp.toTop.offset) {
  329. this.showTopBtn();
  330. } else {
  331. this.hideTopBtn();
  332. }
  333. // 滑动监听
  334. this.optUp.onScroll && onScroll && onScroll()
  335. }
  336. /* 触发上拉加载 */
  337. MeScroll.prototype.triggerUpScroll = function(isCheck) {
  338. if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
  339. // 是否校验在底部; 默认不校验
  340. if (isCheck === true) {
  341. let canUp = false;
  342. // 还有下一页 && 没有锁定 && 不在下拉中
  343. if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
  344. if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
  345. canUp = true; // 标记可上拉
  346. }
  347. }
  348. if (canUp === false) return;
  349. }
  350. this.showUpScroll(); // 上拉加载中...
  351. this.optUp.page.num++; // 预先加一页,如果失败则减回
  352. this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
  353. this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
  354. this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
  355. this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
  356. this.optUp.callback(this); // 执行回调,联网加载数据
  357. }
  358. }
  359. /* 显示上拉加载中 */
  360. MeScroll.prototype.showUpScroll = function() {
  361. this.isUpScrolling = true; // 标记上拉加载中
  362. this.optUp.showLoading && this.optUp.showLoading(this); // 回调
  363. }
  364. /* 显示上拉无更多数据 */
  365. MeScroll.prototype.showNoMore = function() {
  366. this.optUp.hasNext = false; // 标记无更多数据
  367. this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
  368. }
  369. /* 隐藏上拉区域**/
  370. MeScroll.prototype.hideUpScroll = function() {
  371. this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
  372. }
  373. /* 结束上拉加载 */
  374. MeScroll.prototype.endUpScroll = function(isShowNoMore) {
  375. if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
  376. if (isShowNoMore) {
  377. this.showNoMore(); // isShowNoMore=true,显示无更多数据
  378. } else {
  379. this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
  380. }
  381. }
  382. this.isUpScrolling = false; // 标记结束上拉加载
  383. }
  384. /* 重置上拉加载列表为第一页
  385. *isShowLoading 是否显示进度布局;
  386. * 1.默认null,不传参,则显示上拉加载的进度布局
  387. * 2.传参true, 则显示下拉刷新的进度布局
  388. * 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
  389. */
  390. MeScroll.prototype.resetUpScroll = function(isShowLoading) {
  391. if (this.optUp && this.optUp.use) {
  392. let page = this.optUp.page;
  393. this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
  394. this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
  395. page.num = this.startNum; // 重置为第一页
  396. page.time = null; // 重置时间为空
  397. if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
  398. if (isShowLoading == null) {
  399. this.removeEmpty(); // 移除空布局
  400. this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
  401. } else {
  402. this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
  403. }
  404. }
  405. this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
  406. this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
  407. this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
  408. this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
  409. this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
  410. }
  411. }
  412. /* 设置page.num的值 */
  413. MeScroll.prototype.setPageNum = function(num) {
  414. this.optUp.page.num = num - 1;
  415. }
  416. /* 设置page.size的值 */
  417. MeScroll.prototype.setPageSize = function(size) {
  418. this.optUp.page.size = size;
  419. }
  420. /* 联网回调成功,结束下拉刷新和上拉加载
  421. * dataSize: 当前页的数据量(必传)
  422. * totalPage: 总页数(必传)
  423. * systime: 服务器时间 (可空)
  424. */
  425. MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) {
  426. let hasNext;
  427. if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
  428. this.endSuccess(dataSize, hasNext, systime);
  429. }
  430. /* 联网回调成功,结束下拉刷新和上拉加载
  431. * dataSize: 当前页的数据量(必传)
  432. * totalSize: 列表所有数据总数量(必传)
  433. * systime: 服务器时间 (可空)
  434. */
  435. MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) {
  436. let hasNext;
  437. if (this.optUp.use && totalSize != null) {
  438. let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
  439. hasNext = loadSize < totalSize; // 是否还有下一页
  440. }
  441. this.endSuccess(dataSize, hasNext, systime);
  442. }
  443. /* 联网回调成功,结束下拉刷新和上拉加载
  444. * dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
  445. * hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
  446. * systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
  447. */
  448. MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) {
  449. let me = this;
  450. // 结束下拉刷新
  451. if (me.isDownScrolling) me.endDownScroll();
  452. // 结束上拉加载
  453. if (me.optUp.use) {
  454. let isShowNoMore; // 是否已无更多数据
  455. if (dataSize != null) {
  456. let pageNum = me.optUp.page.num; // 当前页码
  457. let pageSize = me.optUp.page.size; // 每页长度
  458. // 如果是第一页
  459. if (pageNum === 1) {
  460. if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
  461. }
  462. if (dataSize < pageSize || hasNext === false) {
  463. // 返回的数据不满一页时,则说明已无更多数据
  464. me.optUp.hasNext = false;
  465. if (dataSize === 0 && pageNum === 1) {
  466. // 如果第一页无任何数据且配置了空布局
  467. isShowNoMore = false;
  468. me.showEmpty();
  469. } else {
  470. // 总列表数少于配置的数量,则不显示无更多数据
  471. let allDataSize = (pageNum - 1) * pageSize + dataSize;
  472. if (allDataSize < me.optUp.noMoreSize) {
  473. isShowNoMore = false;
  474. } else {
  475. isShowNoMore = true;
  476. }
  477. me.removeEmpty(); // 移除空布局
  478. }
  479. } else {
  480. // 还有下一页
  481. isShowNoMore = false;
  482. me.optUp.hasNext = true;
  483. me.removeEmpty(); // 移除空布局
  484. }
  485. }
  486. // 隐藏上拉
  487. me.endUpScroll(isShowNoMore);
  488. }
  489. }
  490. /* 回调失败,结束下拉刷新和上拉加载 */
  491. MeScroll.prototype.endErr = function() {
  492. // 结束下拉,回调失败重置回原来的页码和时间
  493. if (this.isDownScrolling) {
  494. let page = this.optUp.page;
  495. if (page && this.prePageNum) {
  496. page.num = this.prePageNum;
  497. page.time = this.prePageTime;
  498. }
  499. this.endDownScroll();
  500. }
  501. // 结束上拉,回调失败重置回原来的页码
  502. if (this.isUpScrolling) {
  503. this.optUp.page.num--;
  504. this.endUpScroll(false);
  505. }
  506. }
  507. /* 显示空布局 */
  508. MeScroll.prototype.showEmpty = function() {
  509. this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
  510. }
  511. /* 移除空布局 */
  512. MeScroll.prototype.removeEmpty = function() {
  513. this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
  514. }
  515. /* 显示回到顶部的按钮 */
  516. MeScroll.prototype.showTopBtn = function() {
  517. if (!this.topBtnShow) {
  518. this.topBtnShow = true;
  519. this.optUp.toTop.onShow && this.optUp.toTop.onShow(true);
  520. }
  521. }
  522. /* 隐藏回到顶部的按钮 */
  523. MeScroll.prototype.hideTopBtn = function() {
  524. if (this.topBtnShow) {
  525. this.topBtnShow = false;
  526. this.optUp.toTop.onShow && this.optUp.toTop.onShow(false);
  527. }
  528. }
  529. /* 获取滚动条的位置 */
  530. MeScroll.prototype.getScrollTop = function() {
  531. return this.scrollTop || 0
  532. }
  533. /* 记录滚动条的位置 */
  534. MeScroll.prototype.setScrollTop = function(y) {
  535. this.scrollTop = y;
  536. }
  537. /* 滚动到指定位置 */
  538. MeScroll.prototype.scrollTo = function(y, t) {
  539. this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
  540. }
  541. /* 自定义scrollTo */
  542. MeScroll.prototype.resetScrollTo = function(myScrollTo) {
  543. this.myScrollTo = myScrollTo
  544. }
  545. /* 滚动条到底部的距离 */
  546. MeScroll.prototype.getScrollBottom = function() {
  547. return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
  548. }
  549. /* 计步器
  550. star: 开始值
  551. end: 结束值
  552. callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
  553. t: 计步时长,传0则直接回调end值;不传则默认300ms
  554. rate: 周期;不传则默认30ms计步一次
  555. * */
  556. MeScroll.prototype.getStep = function(star, end, callback, t, rate) {
  557. let diff = end - star; // 差值
  558. if (t === 0 || diff === 0) {
  559. callback && callback(end);
  560. return;
  561. }
  562. t = t || 300; // 时长 300ms
  563. rate = rate || 30; // 周期 30ms
  564. let count = t / rate; // 次数
  565. let step = diff / count; // 步长
  566. let i = 0; // 计数
  567. let timer = setInterval(function() {
  568. if (i < count - 1) {
  569. star += step;
  570. callback && callback(star, timer);
  571. i++;
  572. } else {
  573. callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
  574. clearInterval(timer);
  575. }
  576. }, rate);
  577. }
  578. /* 滚动容器的高度 */
  579. MeScroll.prototype.getClientHeight = function(isReal) {
  580. let h = this.clientHeight || 0
  581. if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
  582. h = this.getBodyHeight()
  583. }
  584. return h
  585. }
  586. MeScroll.prototype.setClientHeight = function(h) {
  587. this.clientHeight = h;
  588. }
  589. /* 滚动内容的高度 */
  590. MeScroll.prototype.getScrollHeight = function() {
  591. return this.scrollHeight || 0;
  592. }
  593. MeScroll.prototype.setScrollHeight = function(h) {
  594. this.scrollHeight = h;
  595. }
  596. /* body的高度 */
  597. MeScroll.prototype.getBodyHeight = function() {
  598. return this.bodyHeight || 0;
  599. }
  600. MeScroll.prototype.setBodyHeight = function(h) {
  601. this.bodyHeight = h;
  602. }
  603. /* 阻止浏览器默认滚动事件 */
  604. MeScroll.prototype.preventDefault = function(e) {
  605. // 小程序不支持e.preventDefault
  606. // app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止
  607. // cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
  608. if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
  609. }
  610. /* 是否允许下拉回弹(橡皮筋效果); true或null为允许; false禁止bounce */
  611. MeScroll.prototype.setBounce = function(isBounce) {
  612. // #ifdef H5
  613. if (isBounce === false) {
  614. this.optUp.isBounce = false; // 禁止
  615. // 标记当前页使用了mescroll (需延时,确保page已切换)
  616. setTimeout(function() {
  617. let uniPageDom = document.getElementsByTagName('uni-page')[0];
  618. uniPageDom && uniPageDom.setAttribute('use_mescroll', true)
  619. }, 30);
  620. // 避免重复添加事件
  621. if (window.isSetBounce) return;
  622. window.isSetBounce = true;
  623. // 需禁止window的touchmove事件才能有效的阻止bounce
  624. window.bounceTouchmove = function(e) {
  625. let el = e.target;
  626. // 当前touch的元素及父元素是否要拦截touchmove事件
  627. let isPrevent = true;
  628. while (el !== document.body && el !== document) {
  629. if (el.tagName === 'UNI-PAGE') { // 只扫描当前页
  630. if (!el.getAttribute('use_mescroll')) {
  631. isPrevent = false; // 如果当前页没有使用mescroll,则不阻止
  632. }
  633. break;
  634. }
  635. let cls = el.classList;
  636. if (cls) {
  637. if (cls.contains('mescroll-touch')) { // 采用scroll-view 此处不能过滤mescroll-uni,否则下拉仍然有回弹
  638. isPrevent = false; // mescroll-touch无需拦截touchmove事件
  639. break;
  640. } else if (cls.contains('mescroll-touch-x') || cls.contains('mescroll-touch-y')) {
  641. // 如果配置了水平或者垂直滑动
  642. let curX = e.touches ? e.touches[0].pageX : e.clientX; // 当前第一个手指距离列表顶部的距离x
  643. let curY = e.touches ? e.touches[0].pageY : e.clientY; // 当前第一个手指距离列表顶部的距离y
  644. if (!this.preWinX) this.preWinX = curX; // 设置上次移动的距离x
  645. if (!this.preWinY) this.preWinY = curY; // 设置上次移动的距离y
  646. // 计算两点之间的角度
  647. let x = Math.abs(this.preWinX - curX);
  648. let y = Math.abs(this.preWinY - curY);
  649. let z = Math.sqrt(x * x + y * y);
  650. this.preWinX = curX; // 记录本次curX的值
  651. this.preWinY = curY; // 记录本次curY的值
  652. if (z !== 0) {
  653. let angle = Math.asin(y / z) / Math.PI * 180; // 角度区间 [0,90]
  654. if ((angle <= 45 && cls.contains('mescroll-touch-x')) || (angle > 45 && cls.contains('mescroll-touch-y'))) {
  655. isPrevent = false; // 水平滑动或者垂直滑动,不拦截touchmove事件
  656. break;
  657. }
  658. }
  659. }
  660. }
  661. el = el.parentNode; // 继续检查其父元素
  662. }
  663. // 拦截touchmove事件:是否可以被禁用&&是否已经被禁用 (这里不使用me.preventDefault(e)的方法,因为某些情况下会报找不到方法的异常)
  664. if (isPrevent && e.cancelable && !e.defaultPrevented && typeof e.preventDefault === "function") e.preventDefault();
  665. }
  666. window.addEventListener('touchmove', window.bounceTouchmove, {
  667. passive: false
  668. });
  669. } else {
  670. this.optUp.isBounce = true; // 允许
  671. if (window.bounceTouchmove) {
  672. window.removeEventListener('touchmove', window.bounceTouchmove);
  673. window.bounceTouchmove = null;
  674. window.isSetBounce = false;
  675. }
  676. }
  677. // #endif
  678. }