前段時間在做小程式到H5的遷移,其中小程式中下拉重新整理的功能引起了產品的注意。他說到,哎,我們遷移後的H5頁面怎麼沒有下拉重新整理,於是乎,我就急忙將這部分的內容給填上。
本來是計劃使用成熟的元件庫來實現,嘗試之後發現這些元件和我們H5頁面的其他邏輯有衝突(H5還有吸頂、錨點、滑動高亮、橫向滾動),小小H5頁面上承載了太多的功能,相容起來非常麻煩,想著下拉重新整理功能也不復雜,乾脆我自己寫一個好了。
流程圖示
正常資料展示狀態 --> 手指觸控螢幕下拉 --> 手指鬆開 --> 資料獲取 --> 恢復正常資料展示狀態
功能梳理
要實現這個功能,主要分為兩部分。
監聽手指觸控事件
透過監聽事件,我們可以得知以下的資料
- 手指滑動的時機(手指開始觸控,結束觸控時間)
- 滑動方向(是橫向滑動還是縱向滑動)
- 操作軌跡(手指操作從下往上還是從上往下滑動)
- 是否首屏(如果非首屏進行滑動時是正常滑動操作)
只有在向下滑動、首屏、非載入狀態、縱向滾動並且有高度時,才能進行上述重新整理流程。
css 和 提示文案
- 手指按住螢幕由上往下滑動未鬆開時,展示滑動的高度和提示【釋放重新整理】文案
- 手指鬆開後高度回彈,顯示【資料更新中】文案
- 資料請求介面成功後,顯示【更新成功】文案,loading 內容和圖示緩緩消失
具體實現
觸控的步驟可以分為: 手指按下(開始觸控)、手指移動不離開螢幕(觸控中)、手指離開螢幕(觸控結束),正好對應著三個 js 原生事件,touchstart、touchmove 和 touchend。
觸控事件執行時機
touchstart 和 touchmove 在一次觸控流程只會執行一次,標誌著開始和結束,但是 touchmove 不一樣,只要你的手指還在螢幕上滑動沒有鬆開,就會一直執行。如下圖的輸出的執行次數一樣。
下拉元素繫結
首先需要給需要設定下拉重新整理的區域繫結上這些事件,對於我們業務場景來說,頭部區域無論你如何操作,都需要保留展示的,那麼我們只需要將事件繫結到下方開始顯示下拉重新整理的區域。
// html元素
<div className="refreshWrap">
{/* 下拉時文字提示 */}
<div className={`pullDownContent`} style={{ height: pullDownHeight }}>
{loading ? "" : "釋放重新整理"}
</div>
{/* 載入時動畫 */}
<div className={`loadingFlex ${loading ? "" : "loadingHidden"}`}>
<div className="flexCenter">
<div className="loadingRing" />
<div className="loadingText">
{loading ? "資料更新中..." : "更新成功"}
</div>
</div>
</div>
<div className="middleArea">重新整理區域下方內容區域</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 物件來記錄觸控的位置,包含 x 、y 軸。
bindTouchstart = (event) => {
this.startTouch = event.touches[0];
};
觸控中
使用者觸控中需要給他一個反饋,隨著下拉的距離,螢幕上圈出的下拉區域會隨之變大(下拉展示的區域會設定一個最大高度,如果能無限擴大展示不好看)
用 endTouch 來儲存觸控中的座標值,因為觸控中的事件會執行多次,所以 endTouch 也會不斷的更新,用來更新下拉時滑動的高度。
bindTouchMove = (event) => {
const { loading } = this.state;
this.endTouch = event.touches[0];
if (!loading && this.isInOneScreenPull() && this.isVerticalSliding()) {
const pullDownHeight = this.getPullDownHeight();
this.setState({
pullDownHeight,
});
}
};
根據 endTouch 的值可以判斷出滑動距離、橫向還是縱向滑動,滑動的高度、再獲取滑動元素是否在首屏。
// 判斷滑動的距離
calcDeltaY = () => Math.abs(this.endTouch.pageY - this.startTouch.pageY);
// 判斷是否縱向滾動
isVerticalSliding = () => {
const deltaY = this.calcDeltaY();
const deltaX = Math.abs(this.endTouch.pageX - this.startTouch.pageX);
if (deltaY > deltaX && deltaY > 50) return true;
};
// 下拉展示高度最多展示為100,不能讓載入區域無限制的擴大
getPullDownHeight = () => {
const deltaY = this.calcDeltaY();
return Math.min(deltaY, 100);
};
// 是否在首屏
isInOneScreenPull() {
const pulldownElement = document.querySelector(pullDownClassName);
return pulldownElement.scrollTop <= 0;
}
觸控結束
觸控結束時,將 pulldownHeight 設定為0,非同步載入資料,載入資料時設定變數 loading 表示開始更新、結束更新,防止不停的下拉重新整理呼叫介面。
bindTouched = (e) => {
const { loading, pullDownHeight } = this.state;
// 首屏、非載入狀態、縱向滾動有高度時
if (!loading && pullDownHeight) {
this.setState({
pullDownHeight: 0,
});
this.getData();
// 重置觸控Y軸座標點
this.startTouch = {};
this.endTouch = {};
}
};
平滑過渡動畫
當下拉高度發生變化時,直接修改高度效果會比較生硬,使用 css transition 屬性進行平滑過渡、animation 設定動畫緩慢進入/消失。
.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;
}
}
完整程式碼
以上便是滑動觸發、高度提示、資料重新整理的下拉重新整理功能解析,完整程式碼我放在了 github 上,戳 drop-down-refresh 可檢視,歡迎大家點個 star~