2025年浅写一个下拉刷新组件

浅写一个下拉刷新组件在移动端开发中 不管是 iOS 还是 android 下拉列表加载的情况都是经常出现 所以实现这个功能的必要性也是不言而喻 端上也有很多库可以做这个 一般来说实现下拉刷新或者上拉加载都是通过添加滑动事件监听 判断页面滑动的距离和手势来确定何时加载数据并给出反馈

大家好,我是讯享网,很高兴认识大家。

       在移动端开发中,不管是iOS还是android ,下拉列表加载的情况都是经常出现,所以实现这个功能的必要性也是不言而喻,端上也有很多库可以做这个。一般来说实现下拉刷新或者上拉加载都是通过添加滑动事件监听,判断页面滑动的距离和手势来确定何时加载数据并给出反馈,除此之外对于iOS和android的环境差别,可能需要在必要的地方做个容错处理。

核心逻辑是借助touchstart touchmove touchend 事件监听做处理,

  1. 触摸开始就会执行 touchstart事件,记录当前的Y坐标 startY;
  2. 随着手指移动便监听touchMove事件:手指移动实时记录当前纵向Y坐标 pageY , 而pageY - startY就是移动的距离distance;此时最新的top值等于开始top再加鼠标移动距离: element.style.top = startTop + distance ;
  3. 松开手指触发 touchend事件,刚才如果移动超过一定阈值threshold就会执行回调(刷新数据的事件)实现回弹效果。 

下面用vue实现:

template:

 <div :class="{ container: refreshEnable }" :style="containerStyle"> <div v-if="refreshEnable && [1, 2, 3, 4].indexOf(refreshStatus) >= 0"> <div class="top-tip" v-if="refreshStatus === 1" :class="{ transition: !noAnimation }" :style="{ transform: 'translateY(' + (refreshing ? refreshingY : pullY) + 'px)' }" > <span class="refresh-icon" v-if="pullY > threshold"><img src="@assets/img/up.png" /></span> <span class="refresh-icon" v-else><img src="@assets/img/down.png" /></span> <span class="refresh-msg">{ 
  
    
  { pullY > threshold ? '松开' : '下拉' }}刷新</span> </div> <!-- 刷新提示信息:状态2:开始加载数据,未完成状态 --> <div class="top-loading" v-else-if="refreshStatus === 2"> 正在加载… </div> <!-- 刷新提示信息:状态3:数据已经加载完成并成功显示 --> <div class="top-success" v-else-if="refreshStatus === 3"> 已为你发现最新内容 </div> <!-- 刷新提示信息:状态4:数据已经加载完成但没有新内容 --> <div class="top-norefresh" v-else-if="refreshStatus === 4"> 前方暂未发现新消息~ </div> </div> <div class="wrapper" ref="wrapper" :class="{ transition: !noAnimation }" :style="wrapperY ? { transform: 'translateY(' + wrapperY + 'px)' } : {}" > </div> <!-- 底部永远保留空间防止闪顿 --> <div class="common-bottom" v-if="loadMoreEnable && [1, 2].indexOf(loadingStatus) >= 0"> <!-- 底部加载提示信息:状态1: 正在加载数据 --> <div class="bottom-loading" v-show="loadingStatus === 1"> 正在加载… </div> <!-- 底部加载提示信息:状态2: 提示已经加载到最后一条数据 --> <div class="bottom-limit" v-show="loadingStatus === 2"> 你居然看完了全部内容 换个地点继续发现吧 ~ </div> </div> </div>

讯享网

script: 

讯享网import { throttle } from '@util/throttle'; //节流函数 import scroll from '@mixins/scroll'; // 监听滚动事件 

data:

 data() { return { refreshStatus: 0, loadingStatus: 0, pullY: 0, refreshing: false, loading: false, noAnimation: false, threshold: 70, refreshingY: 0, }; },

props:


讯享网

讯享网 props: { // 是否允许下拉刷新 refreshEnable: { type: Boolean, default: true, }, // 下滑的回调函数 onRefresh: { type: Function, default() {}, }, // 上滑的回调函数 onLoad: { type: Function, default() {}, }, loadMoreEnable: { type: Boolean, default: true, }, hasMore: { type: Boolean, default: true, }, mpsTipHeight: { type: Number, default: 0, }, navigatorHeight: { type: Number, default: 0, }, mpsTipShow: { type: Boolean, default: false, }, },

watch:

 watch: { hasMore: { handler(newValue) { if (this.loadMoreEnable) { this.loadingStatus = newValue ? 0 : 2; } }, immediate: true, }, },

computed:

讯享网 computed: { containerStyle() { let marginTop = 0; if (this.refreshEnable) { if (this.mpsTipShow) { let baseMarginTop = 29; marginTop = `${this.mpsTipHeight + baseMarginTop}px`; } else { // 随tips高度变化 marginTop = `${(this.navigatorHeight || 86) - 5}px`; } } return { marginTop, }; }, wrapperY() { return this.refreshing ? this.refreshingY : this.pullY; }, },

methods:

 methods: { touchEndHandlers(type) { if (!this.refreshEnable) { return; } // debugger; let vm = this; if (vm.refreshing) return; vm.refreshStatus = 2; vm.refreshing = true; let r = vm.onRefresh(type); if (r && r.then) { r.then((res) => { vm.refreshing = false; vm.pullY = (54 / 375) * getClientWidth(); if (res && res === 4) { vm.refreshStatus = 4; } else if (res && res === 3) { vm.refreshStatus = 3; } setTimeout(() => { vm.refreshStatus = 0; vm.pullY = 0; }, 500); }).catch((error) => { vm.refreshStatus = 0; vm.refreshing = false; vm.pullY = 0; console.error(error); }); } }, // 滑动触发的函数:加节流 scrollHandlers() { let vm = this; let scrollTop = this.getScrollTop(); let bodyHeight = document.documentElement.clientHeight || document.body.clientHeight; let bodyWidth = document.documentElement.clientWidth || document.body.clientWidth; if (!vm.$refs.wrapper || !this.hasMore || !this.loadMoreEnable) return; let containerHeight = vm.$refs.wrapper.clientHeight; // 如果到底了加载数据,距离底端150px时开始加载数据 if ((scrollTop + bodyHeight) > (containerHeight - ((200 / 375) * bodyWidth)) && !vm.loading) { vm.loading = true; // onLoad为一个返回promise对象的函数 vm.loadingStatus = 1; let l = vm.onLoad(); if (l && l.then) { l.then((res) => { if (res === -2) { vm.loadingStatus = 2; vm.loading = false; } else if (res === -1) { vm.loadingStatus = 0; vm.loading = false; } else { vm.loading = false; } }).catch((error) => { vm.loadingStatus = 0; vm.loading = false; console.error(error); }); } } }, forceRefresh(type) { window.scrollTo(0, 0); this.touchEndHandlers(type); }, },

mounted:

讯享网 mounted() { let vm = this; // 添加滑动事件监听,用于判断何时加载数据,节流 window.addEventListener('scroll', throttle(vm.scrollHandlers, 20), true); if (this.refreshEnable) { // 添加滑动事件,用于判断监控scrollTop:1. 判断滑动方向 2.判断是否快到底端了 let start = false; let startY = null; let lastMove = null; // 下拉回弹之后,刷新过程中,即refreshing为true时,列表回弹到一定高度 vm.refreshingY = (54 / 375) * getClientWidth(); // 如果滑动的只是一小块区域,则触摸开始的地方需要在这个区域内 this.$refs.wrapper.addEventListener('touchstart', () => { start = true; }); window.addEventListener('touchstart', (ev) => { lastMove = ev.changedTouches[0].pageY; }); window.addEventListener( 'touchmove', (ev) => { let scrollTop = this.getScrollTop(); if (start && scrollTop <= 0 && lastMove !== null) { let now = ev.changedTouches[0].pageY; let diff = now - lastMove; // 判断瞬时滑动的方向 if (diff > 0) { if (ev.cancelable) { ev.preventDefault(); } // 阻止浏览器默认的scroll事件 vm.refreshStatus = 1; } else { // 瞬时滑动向上,整体滑动向下 if (vm.pullY > 0) { if (ev.cancelable) { ev.preventDefault(); } vm.refreshStatus = 1; } } if (!startY) { startY = now; } let targetHeight = now - startY; vm.pullY = Math.max(targetHeight, 0) / 2; } // 手指下拉的时候停止动画 vm.noAnimation = true; lastMove = ev.changedTouches[0].pageY; }, { passive: false } ); window.addEventListener('touchend', () => { if (vm.refreshing) return; if (vm.pullY > vm.threshold) { vm.touchEndHandlers(); } else { vm.pullY = 0; vm.refreshStatus = 0; } startY = null; lastMove = null; start = false; vm.noAnimation = false; }); } },

mixins:

/
 * 页面滚动
 * @module mixins/scroll
 */
export default {
  methods: {
    /
     * 是否为iOS
     * @ignore
     * @return {Boolean} 是否为iOS
     */
    isIOS() {
      const UA = window.navigator.userAgent.toLowerCase();
      return UA && /iphone|ipad|ipod|ios/.test(UA);
    },

    /
     * 为页面添加持续滚动的事件监听
     * 由于ios的UIWebview存在滚动bug,因此做兼容处理
     * @ignore
     * @param {Function} callback 滚动发生时的回调函数
     */
    addBodyScrollListener(callback) {
      document.addEventListener('scroll', callback);
      if (this.isIOS && typeof window.ontouchmove !== 'undefined') {
        document.addEventListener('touchmove', callback);
      }
    },

    /
     * 获取页面的scrollTop
     * @ignore
     * @return {Number} 页面的scrollTop
     */
    getScrollTop() {
      return document.body.scrollTop || document.documentElement.scrollTop;
    },

    /
     * 令页面滚动到指定位置
     * @ignore
     * @param {Number} top 目标位置横坐标
     */
    scrollTop(top) {
      document.body.scrollTop = top;
      document.documentElement.scrollTop = top;
    },

    /
     * rem转换为px
     * @ignore
     * @param {String} rem 待转换的rem值
     * @return {Number} 转换后的px值
     */
    rem2px(rem) {
      const { fontSize } = document.getElementsByTagName('html')[0].style;
      return `${parseFloat(rem) * parseFloat(fontSize)}px`;
    },

    /
     * px转换为rem
     * @ignore
     * @param {String} px 待转换的px值
     * @return {Number} 转换后的rem值
     */
    px2rem(px) {
      const { fontSize } = document.getElementsByTagName('html')[0].style;
      return `${parseFloat(px) / parseFloat(fontSize)}rem`;
    },

    /
     * 无论是rem还是px单位,均转换成整数型的px数值
     * @ignore
     * @param {String} value 输入值
     * @return {Number} 转换后的px数值
     */
    remOrPxToPxNumber(value) {
      if (value.toString().indexOf('rem') > -1) {
        // rem转px
        return parseFloat(this.rem2px(value));
      }
      return parseFloat(value);
    },
  },
};

 

 

小讯
上一篇 2025-04-07 14:54
下一篇 2025-01-24 20:41

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/29799.html