mescroll-uni.js 32 KB

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