公司要在安卓软件中,添加H5网页包,实现订单卡片列表,要求有上拉加载、下拉刷新的功能。
经过搜索资料后,实现如下:
创建一个类PullData
class PullData() {
/**
* 类的构造函数,在new PullData({xx: 'xxx'})时,参数会从consctructor中传入PullData类中
* options就是传入的{xx: 'xxx'},
* 先用一个变量将传入的参数存起来
*/
constructor(options) {
this.data = options;
}
}
实现订单列表的上拉、下拉,需要操作 dom 节点,所以我们要传入一个dom进行操作。
class PullData() {
constructor(options) {
this.data = options;
// 用domWrapper来存储dom节点,如果没有传入就手动设置为body节点
if(!options.dom) {
// 如果没有传入dom,默认是body
this.domWrapper = document.body;
} else {
// 传入的dom必须是 Nodelist 节点,或者是属性选择器字符串
this.domWrapper = typeof options.dom === 'string' ? document.querySelector(options.dom) : options.dom;
}
}
}
除了订单列表的dom节点,还要初始化显示刷新中...
、加载中...
文案的dom节点,因为我的订单列表是铺整页的,所以写的刷新、加载用 position: fixed
分别在页面顶部、页面底部。
class PullData() {
constructor(options) {
this.data = options;
// 用domWrapper来存储dom节点,如果没有传入就手动设置为body节点
if(!options.dom) {
// 如果没有传入dom,默认是body
this.domWrapper = document.body;
} else {
// 传入的dom必须是 Nodelist 节点,或者是属性选择器字符串
this.domWrapper = typeof options.dom === 'string' ? document.querySelector(options.dom) : options.dom;
}
// 调用初始化方法,进行数据、节点初始化等
this.init();
}
// 初始化
init() {
this.renderTextDom();
}
// 创建渲染【刷新】【加载】文案所在的节点
renderTextDom() {
// 刷新提示语的节点
let refreshTextDom = document.createElement('div');
refreshTextDom.id = "refresh-text";
refreshTextDom.innerHTML = "刷新中...";
refreshTextDom.style = "display:none;position:fixed;top:0.2rem;left: 50%;transform: translateX(-50%);";
// 加载提示语的节点
let loadTextDom = document.createElement('div');
loadTextDom.id = "load-text";
loadTextDom.innerHTML = "加载中...";
loadTextDom.style = "display:none;position:fixed;bottom:0.2rem;left: 50%;transform: translateX(-50%);";
document.body.appendChild(refreshTextDom);
document.body.appendChild(loadTextDom);
}
}
上拉、下拉要通过touchstart、touchmove、touchend去监听用户的触摸动作。在初始化的时候,同时开启监听事件
init() {
// 触摸开始事件
this.domWrapper.addEventListener('touchstart', function (e) {
// 监听操作
}, false);
// 触摸移动时间
this.domWrapper.addEventListener('touchmove', function (e) {
// 监听操作
}, false);
// 触摸结束事件
this.domWrapper.addEventListener("touchend", function (e) {
// 监听操作
}, false);
}
用户不需要使用这个上拉下拉功能后,需要清除监听事件,否则监听事件只增不减,就会影响浏览器性能。所以在开启监听事件时,方法不能使用匿名函数,否则无法销毁对应的监听事件。
init() {
this.renderTextDom();
// 触摸开始事件
this.domWrapper.addEventListener('touchstart', this.start, false);
// 触摸移动时间
this.domWrapper.addEventListener('touchmove', this.move, false);
// 触摸结束事件
this.domWrapper.addEventListener("touchend", this.end , false)
}
// 触摸开始事件回调
start(e) {}
// 触摸移动事件回调
move (e) {}
// 触摸结束事件回调
end(e) {}
// 销毁监听函数
destroy() {
this.domWrapper.removeEventListener('touchmove', this.move);
this.domWrapper.removeEventListener('touchstart', this.start);
this.domWrapper.removeEventListener('touchend', this.end);
}
注意:
addEventListener监听 this.domWrapper.addEventListener('touchstart', this.start, false);
,此时的 this.start 函数内部的this
指向的是 this.domWrapper,如果我要在this.start
内部调用PullData内部的方法 this.beforeAction
,就会报错Uncaught TypeError: this.beforeAction is not a function
。所以我们要修正
这些方法的this指向
。
class PullData() {
constructor(options) {
// ...
// 将this.scroll 的this指向 PullData 这个类(原来指向nodelist节点)
this.scroll = this.scroll.bind(this);
// 将this.start 的this指向 PullData 这个类(原来指向nodelist节点)
this.start = this.start.bind(this);
// 将this.move 的this指向 PullData 这个类(原来指向nodelist节点)
this.move = this.move.bind(this);
// 将this.end 的this指向 PullData 这个类(原来指向nodelist节点)
this.end = this.end.bind(this);
// 调用初始化方法,进行数据、节点初始化等
this.init();
}
// ...
}
touchstart、touchmove、touchend监听回调事件
开始时,记录手指/鼠标点击触碰到屏幕的位置,移动时,通过translateY
修改下拉/上拉时卡片移动的距离,手指/鼠标松开后,将拉动的距离逐渐恢复到translateY(0px)
。
// 构造函数
constructor(options) {
// ...
this.rule = options.rule || 50; // 定义触发加载/刷新事件的拉伸长度
this.startY = 0; // 记录鼠标/手指点击的位置
this.moveY = 0; // 记录鼠标/手指移动的位置
this.distance = 0; // 记录鼠标/手指移动的距离
this.clientHeight = 0; // 滚动的可视区
this.scrollTop = 0; // 滚动的距离
this.scrollHeight = 0; // 滚动的总高度
// 上拉加载事件回调函数
this.pullUpCallback = options.pullUpCallback.bind(this) || function() {};
// 下拉刷新事件回调函数
this.pullDownCallback = options.pullDownCallback.bind(this) || function() {};
// ...
}
// 触摸开始事件回调
start (e) {
this.startY = e.touches[0].screenY - this.distance;
}
// 触摸移动事件回调
move (e) {
e.stopPropagation();
this.moveY = e.touches[0].screenY;
this.distance = Math.floor(this.moveY - this.startY) / 5;
this.beforeAction(this.distance);
// 只有在触顶或者触底时,才操作domWrapper的样式
if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
// 这一步才让人有被“拉”下来/上去的视觉效果
this.domWrapper.style.transform = `translateY(${this.distance}px)`
}
}
// 触摸结束事件回调
end(e) {
if(this.timer) clearInterval(this.timer);
this.timer = setInterval(() => {
if(Math.floor(this.distance) < 0 || Math.ceil(this.distance) < 10) {
// 移动距离恢复到 10 以内时,将domWrapper的translateY设置为0,清除定时器
this.distance = 0;
// 这一步是“回弹到原位”的视觉效果
this.domWrapper.style.transform = `translateY(0px)`;
clearInterval(this.timer);
} else {
// 每10ms 减去 this.distance/10的距离
this.distance -= this.distance / 10;
// 只有在触顶或者触底时,才操作domWrapper的样式
if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
// 这一步是“回弹”过程的视觉效果
this.domWrapper.style.transform = `translateY(${this.distance}px)`;
}
}
}, 10);
// 触顶时才触发刷新回调,避免touchmove滚动过程中显示相关提示、接口请求
if(this.scrollTop <= 0) {
if(this.distance > this.rule) {
this.refresh && this.refresh(this.pullDownCallback);
}
}
// 触底时才触发加载回调,避免touchmove滚动过程中显示相关提示、接口请求
if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
if(this.distance < -this.rule) {
this.loading && this.loading(this.pullUpCallback);
}
}
}
// UI处理
beforeAction ( distance) {
// 在列表触顶时,才显示【刷新中】的文案
if(this.scrollTop - this.rule <= 0) {
if (distance > this.rule) {
var el = document.getElementById('refresh-text')
el.innerHTML = '<p>刷新中...</p>'
el.style.display = 'block'
} else {
document.getElementById('refresh-text').style.display = 'none'
}
}
// 在列表触底(距离底部this.rule高度)时,才显示【加载中】的文案
if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
if (distance < -this.rule) {
document.getElementById('load-text').style.display = 'block'
} else {
document.getElementById('load-text').style.display = 'none'
}
}
}
完整代码:
// 上拉加载,wrapper里必须只有一个子元素,值举例:"#xxid"
export class PullData {
constructor(options) {
this.data = options;
if(!options.dom) {
// 如果没有传入dom,默认是body
this.domWrapper = document.body;
} else {
// 传入的dom必须是 Nodelist 节点,或者是属性选择器字符串
this.domWrapper = typeof options.dom === 'string' ? document.querySelector(options.dom) : options.dom;
}
this.rule = options.rule || 50; // 定义触发加载/刷新事件的拉伸长度
this.startY = 0; // 记录鼠标/手指点击的位置
this.moveY = 0; // 记录鼠标/手指移动的位置
this.distance = 0; // 记录鼠标/手指移动的距离
this.clientHeight = 0; // 滚动的可视区
this.scrollTop = 0; // 滚动的距离
this.scrollHeight = 0; // 滚动的总高度
// 上拉加载事件回调函数
this.pullUpCallback = options.pullUpCallback.bind(this) || function() {};
// 下拉刷新事件回调函数
this.pullDownCallback = options.pullDownCallback.bind(this) || function() {};
// 将this.scroll 的this指向 PullData 这个类(原来指向nodelist节点)
this.scroll = this.scroll.bind(this);
// 将this.start 的this指向 PullData 这个类(原来指向nodelist节点)
this.start = this.start.bind(this);
// 将this.move 的this指向 PullData 这个类(原来指向nodelist节点)
this.move = this.move.bind(this);
// 将this.end 的this指向 PullData 这个类(原来指向nodelist节点)
this.end = this.end.bind(this);
this.timer = null; // 上拉/下拉样式回弹的定时器
this.init(); // 初始化PullData的数据和节点
}
// 初始化
init() {
this.renderTextDom();
this.scrollTop = this.domWrapper.scrollTop;
this.scrollHeight = this.domWrapper.scrollHeight;
this.clientHeight = this.domWrapper.clientHeight;
// 滚动事件
this.domWrapper.addEventListener('scroll', this.scroll, false);
// 触摸开始事件
this.domWrapper.addEventListener('touchstart', this.start, false);
// 触摸移动时间
this.domWrapper.addEventListener('touchmove', this.move, false);
// 触摸结束事件
this.domWrapper.addEventListener("touchend", this.end , false)
}
// 创建渲染【刷新】【加载】文案所在的节点
renderTextDom() {
// 刷新提示语的节点
let refreshTextDom = document.createElement('div');
refreshTextDom.id = "refresh-text";
refreshTextDom.innerHTML = "刷新中...";
refreshTextDom.style = "display:none;position:fixed;top:0.2rem;left: 50%;transform: translateX(-50%);";
// 加载提示语的节点
let loadTextDom = document.createElement('div');
loadTextDom.id = "load-text";
loadTextDom.innerHTML = "加载中...";
loadTextDom.style = "display:none;position:fixed;bottom:0.2rem;left: 50%;transform: translateX(-50%);";
document.body.appendChild(refreshTextDom);
document.body.appendChild(loadTextDom);
}
// 滚动事件回调
scroll(e) {
this.scrollTop = e.target.scrollTop;
this.scrollHeight = e.target.scrollHeight;
}
// 触摸开始事件回调
start (e) {
this.startY = e.touches[0].screenY - this.distance;
}
// 触摸移动事件回调
move (e) {
e.stopPropagation();
this.moveY = e.touches[0].screenY;
this.distance = Math.floor(this.moveY - this.startY) / 5;
this.beforeAction(this.distance);
// 只有在触顶或者触底时,才操作domWrapper的样式
if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
this.domWrapper.style.transform = `translateY(${this.distance}px)`
}
}
// 触摸结束事件回调
end(e) {
if(this.timer) clearInterval(this.timer);
this.timer = setInterval(() => {
if(Math.floor(this.distance) < 0 || Math.ceil(this.distance) < 10) {
// 移动距离恢复到 10 以内时,将domWrapper的translateY设置为0,清除定时器
this.distance = 0;
this.domWrapper.style.transform = `translateY(0px)`;
clearInterval(this.timer);
} else {
// 每10ms 减去 this.distance/10的距离
this.distance -= this.distance / 10;
// 只有在触顶或者触底时,才操作domWrapper的样式
if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
this.domWrapper.style.transform = `translateY(${this.distance}px)`;
}
}
}, 10);
// 触顶时才触发刷新回调,避免touchmove滚动过程中显示相关提示、接口请求
if(this.scrollTop <= 0) {
if(this.distance > this.rule) {
this.refresh && this.refresh(this.pullDownCallback);
}
}
// 触底时才触发加载回调,避免touchmove滚动过程中显示相关提示、接口请求
if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
if(this.distance < -this.rule) {
this.loading && this.loading(this.pullUpCallback);
}
}
}
// 刷新逻辑在此处理
refresh (callback) {
callback && callback();
var el = document.getElementById('refresh-text')
el.innerHTML = '<p>刷新成功!</p>'
setTimeout(() => {el.style.display = 'none'}, 300)
}
// 加载逻辑在此处理
loading (callback) {
callback && callback();
setTimeout(() => {
document.getElementById('load-text').style.display = 'none'
},300)
}
// UI处理
beforeAction ( distance) {
// 在列表触顶时,才显示【刷新中】的文案
if(this.scrollTop - this.rule <= 0) {
if (distance > this.rule) {
var el = document.getElementById('refresh-text')
el.innerHTML = '<p>刷新中...</p>'
el.style.display = 'block'
} else {
document.getElementById('refresh-text').style.display = 'none'
}
}
// 在列表触底(距离底部this.rule高度)时,才显示【加载中】的文案
if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
if (distance < -this.rule) {
document.getElementById('load-text').style.display = 'block'
} else {
document.getElementById('load-text').style.display = 'none'
}
}
}
// 销毁监听函数
destroy() {
this.domWrapper.removeEventListener('scroll', this.scroll);
this.domWrapper.removeEventListener('touchmove', this.move);
this.domWrapper.removeEventListener('touchstart', this.start);
this.domWrapper.removeEventListener('touchend', this.end);
}
}