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