前段時(shí)間在做小程序到H5的遷移,其中小程序中下拉刷新的功能引起了產(chǎn)品的注意。他說到,哎,我們遷移后的H5頁面怎么沒有下拉刷新,于是乎,我就急忙將這部分的內(nèi)容給填上。
本來是計(jì)劃使用成熟的組件庫來實(shí)現(xiàn),嘗試之后發(fā)現(xiàn)這些組件和我們H5頁面的其他邏輯有沖突(H5還有吸頂、錨點(diǎn)、滑動(dòng)高亮、橫向滾動(dòng)),小小H5頁面上承載了太多的功能,兼容起來非常麻煩,想著下拉刷新功能也不復(fù)雜,干脆我自己寫一個(gè)好了。
流程圖示
正常數(shù)據(jù)展示狀態(tài) --> 手指觸摸屏幕下拉 --> 手指松開 --> 數(shù)據(jù)獲取 --> 恢復(fù)正常數(shù)據(jù)展示狀態(tài)

功能梳理
要實(shí)現(xiàn)這個(gè)功能,主要分為兩部分。
監(jiān)聽手指觸摸事件
通過監(jiān)聽事件,我們可以得知以下的數(shù)據(jù)
- 手指滑動(dòng)的時(shí)機(jī)(手指開始觸摸,結(jié)束觸摸時(shí)間)
- 滑動(dòng)方向(是橫向滑動(dòng)還是縱向滑動(dòng))
- 操作軌跡(手指操作從下往上還是從上往下滑動(dòng))
- 是否首屏(如果非首屏進(jìn)行滑動(dòng)時(shí)是正?;瑒?dòng)操作)
只有在向下滑動(dòng)、首屏、非加載狀態(tài)、縱向滾動(dòng)并且有高度時(shí),才能進(jìn)行上述刷新流程。
css 和 提示文案
- 手指按住屏幕由上往下滑動(dòng)未松開時(shí),展示滑動(dòng)的高度和提示【釋放刷新】文案
- 手指松開后高度回彈,顯示【數(shù)據(jù)更新中】文案
- 數(shù)據(jù)請求接口成功后,顯示【更新成功】文案,loading 內(nèi)容和圖標(biāo)緩緩消失
具體實(shí)現(xiàn)
觸摸的步驟可以分為: 手指按下(開始觸摸)、手指移動(dòng)不離開屏幕(觸摸中)、手指離開屏幕(觸摸結(jié)束),正好對(duì)應(yīng)著三個(gè) js 原生事件,touchstart、touchmove 和 touchend。
觸摸事件執(zhí)行時(shí)機(jī)
touchstart 和 touchmove 在一次觸摸流程只會(huì)執(zhí)行一次,標(biāo)志著開始和結(jié)束,但是 touchmove 不一樣,只要你的手指還在屏幕上滑動(dòng)沒有松開,就會(huì)一直執(zhí)行。如下圖的輸出的執(zhí)行次數(shù)一樣。

下拉元素綁定
首先需要給需要設(shè)置下拉刷新的區(qū)域綁定上這些事件,對(duì)于我們業(yè)務(wù)場景來說,頭部區(qū)域無論你如何操作,都需要保留展示的,那么我們只需要將事件綁定到下方開始顯示下拉刷新的區(qū)域。
// html元素
<div className="refreshWrap">
{/* 下拉時(shí)文字提示 */}
<div className={`pullDownContent`} style={{ height: pullDownHeight }}>
{loading ? "" : "釋放刷新"}
</div>
{/* 加載時(shí)動(dòng)畫 */}
<div className={`loadingFlex ${loading ? "" : "loadingHidden"}`}>
<div className="flexCenter">
<div className="loadingRing" />
<div className="loadingText">
{loading ? "數(shù)據(jù)更新中..." : "更新成功"}
</div>
</div>
</div>
<div className="middleArea">刷新區(qū)域下方內(nèi)容區(qū)域</div>
</div>
// js 綁定
const pullDownClassName = ".refreshWrap";
bindPullDown() {
const pulldownElement = document.querySelector(pullDownClassName);
pulldownElement.addEventListener("touchstart", this.bindTouchstart);
pulldownElement.addEventListener("touchmove", this.bindTouchMove);
pulldownElement.addEventListener("touchend", this.bindTouched);
}
觸摸開始
手指觸摸到屏幕的邏輯非常簡單,使用 startTouch 對(duì)象來記錄觸摸的位置,包含 x 、y 軸。
bindTouchstart = (event) => {
this.startTouch = event.touches[0];
};
觸摸中
用戶觸摸中需要給他一個(gè)反饋,隨著下拉的距離,屏幕上圈出的下拉區(qū)域會(huì)隨之變大(下拉展示的區(qū)域會(huì)設(shè)置一個(gè)最大高度,如果能無限擴(kuò)大展示不好看)

用 endTouch 來保存觸摸中的坐標(biāo)值,因?yàn)橛|摸中的事件會(huì)執(zhí)行多次,所以 endTouch 也會(huì)不斷的更新,用來更新下拉時(shí)滑動(dòng)的高度。
bindTouchMove = (event) => {
const { loading } = this.state;
this.endTouch = event.touches[0];
if (!loading && this.isInOneScreenPull() && this.isVerticalSliding()) {
const pullDownHeight = this.getPullDownHeight();
this.setState({
pullDownHeight,
});
}
};
根據(jù) endTouch 的值可以判斷出滑動(dòng)距離、橫向還是縱向滑動(dòng),滑動(dòng)的高度、再獲取滑動(dòng)元素是否在首屏。
// 判斷滑動(dòng)的距離
calcDeltaY = () => Math.abs(this.endTouch.pageY - this.startTouch.pageY);
// 判斷是否縱向滾動(dòng)
isVerticalSliding = () => {
const deltaY = this.calcDeltaY();
const deltaX = Math.abs(this.endTouch.pageX - this.startTouch.pageX);
if (deltaY > deltaX && deltaY > 50) return true;
};
// 下拉展示高度最多展示為100,不能讓加載區(qū)域無限制的擴(kuò)大
getPullDownHeight = () => {
const deltaY = this.calcDeltaY();
return Math.min(deltaY, 100);
};
// 是否在首屏
isInOneScreenPull() {
const pulldownElement = document.querySelector(pullDownClassName);
return pulldownElement.scrollTop <= 0;
}
觸摸結(jié)束
觸摸結(jié)束時(shí),將 pulldownHeight 設(shè)置為0,異步加載數(shù)據(jù),加載數(shù)據(jù)時(shí)設(shè)置變量 loading 表示開始更新、結(jié)束更新,防止不停的下拉刷新調(diào)用接口。
bindTouched = (e) => {
const { loading, pullDownHeight } = this.state;
// 首屏、非加載狀態(tài)、縱向滾動(dòng)有高度時(shí)
if (!loading && pullDownHeight) {
this.setState({
pullDownHeight: 0,
});
this.getData();
// 重置觸摸Y軸坐標(biāo)點(diǎn)
this.startTouch = {};
this.endTouch = {};
}
};
平滑過渡動(dòng)畫
當(dāng)下拉高度發(fā)生變化時(shí),直接修改高度效果會(huì)比較生硬,使用 css transition 屬性進(jìn)行平滑過渡、animation 設(shè)置動(dòng)畫緩慢進(jìn)入/消失。
.pullDownContent {
display: flex;
align-items: flex-end;
justify-content: center;
font-size: 12px;
color: rgba(0, 0, 0, 0.25);
margin: auto;
transition: height 0.3s ease-out; /* 平滑過渡效果 */
overflow: hidden;
}
.loadingHidden {
animation: shrinkHeight 1s forwards;
}
@keyframes shrinkHeight {
100% {
height: 0;
opacity: 0;
overflow: hidden;
}
}
完整代碼
以上便是滑動(dòng)觸發(fā)、高度提示、數(shù)據(jù)刷新的下拉刷新功能解析,完整代碼我放在了 github 上,戳 drop-down-refresh 可查看,歡迎大家點(diǎn)個(gè) star~