JS+CSS實現的下拉刷新/上拉加載插件


閑來無事,寫了一個當下比較常見的下拉刷新/上拉加載的jquery插件,代碼記錄在這里,有興趣將代碼寫成插件與npm包可以留言。

體驗地址:http://owenliang.github.io/pullToRefresh/

項目地址:https://github.com/owenliang/pullToRefresh

實現注意:

  • 利用transition做動畫時,優先使用transform:translate取代top,后者動畫流暢度存在問題。
  • 各移動瀏覽器對手勢觸摸的處理不同(簡單羅列如下),但是下面的應對方案又會導致部分瀏覽器的overflow:scroll失效,總之難以兼容:
    • 微信瀏覽器下拉自帶回彈動畫:可以禁止document的touchmove事件默認處理行為。
    • 谷歌瀏覽器下拉自帶刷新功能:利用屬性touch-action: none可以禁掉。
  • 針對上述問題,我的建議是滾動一律用iscroll5插件模擬實現(非overflow:scroll),然后利用上面的方法禁掉瀏覽器的默認touchmove行為。
  • transition如果有多個屬性,那么transitionend回調會為每個屬性回調一次,因此遇到其中任意一個回調就應該把css和transitionend回調都刪除掉。
  • 瀏覽器在執行JS代碼時沒有機會重繪UI,因此在使用transition的時候一定要注意把修改動畫終止CSS的代碼通過setTimeout延遲一會執行。

 

貼代碼上首頁,歡迎留言交流,需一位有興趣有時間的朋友合作,主要做2件事:

1)插件改為NPM包。

2)基於pullToRefresh庫,開發類似"今日頭條"的左右滑動UI。

 

pullToRefresh.js:

/**
 * 為指定的容器添加滾動條,支持下拉刷新與上拉加載功能
 * @param container 需要滾動的容器,要求設置css: position!=static,height=
 * @param option 配置項,詳見下方defaultOption說明
 * @return 返回對象用於操控此區域,當前暴露了iscroll的refresh函數,當你在插件之外向滾動區域增加/刪除內容后應該主動調用一次
 * @description
 *
 * 2017-03-29
 * 1)支持上拉加載
 * 2017-03-30
 * 1)改為jquery靜態函數插件
 * 2)支持關閉下拉刷新或上拉加載
 */
$.installPullToRefresh =
function (container, option) {
    // 起始觸摸位置
    var touchStartY = 0;
    // 起始圖標位置
    var pullStartY = 0;
    // 當前的觸摸事件
    var touchEvent = null;
    // 當前的刷新事件
    var refreshEvent = null;
    // 當前圖標位置
    var curY = -55;
    // 當前的加載事件
    var loadEvent = null;

    // 默認參數
    var defaultOption = {
        // 刷新相關
        noRefresh: false, // 關閉下拉刷新特性
        pauseBound: 40,  // 觸發刷新的位置(也是圖標loading暫停的位置)
        lowerBound: 80, // 最大下拉到多少px
        loadImg: "load.png", // loading圖片
        pullImg: "pull.png", // 下拉圖片
        onRefresh: function (refreshDone) { // 刷新數據回調
            setTimeout(function() { // 默認不做任何事
                refreshDone();
            }, 0);
        },

        // 加載相關
        noLoad: false, // 關閉上拉加載特性
        bottomHeight: 1, // 距離滾動條底部多少px發起刷新
        onLoad: function (loadDone) {
            setTimeout(function() {
                loadDone();
            }, 0);
        },
    };
    var finalOption = $.extend(true, defaultOption, option);

    // 創建iscroll5滾動區域
    var iscroll = new IScroll(container, {
        bounce: false,
    });

    // 關閉上拉加載特性
    if (!finalOption.noLoad) {
        // 監聽滾動結束事件,用於上拉加載
        iscroll.on('scrollEnd', function () {
            // 有滾動條的情況下,才允許上拉加載
            if (iscroll.maxScrollY < 0) { // maxScrollY<0表明出現了滾動條
                var bottomDistance = (iscroll.maxScrollY - iscroll.y) * -1;
                // 距離底部足夠近,觸發加載
                if (bottomDistance <= finalOption.bottomHeight) {
                    // 當前沒有刷新和加載事件正在執行
                    if (!loadEvent && !refreshEvent) {
                        loadEvent = {}; // 生成新的加載事件
                        finalOption.onLoad(function (error, msg) {
                            loadEvent = null; // 清理當前的加載事件
                            // 延遲重繪滾動條
                            setTimeout(function () {
                                iscroll.refresh();
                            }, 0);
                        });
                    }
                }
            }
        });
    }

    // 關閉下拉刷新特性
    if (!finalOption.noRefresh) {
        // 緊鄰滾動區域,容納刷新圖標
        var pullContainer = $('<div class="pullContainer"></div>')
        // 創建小圖標
        var pullToRefresh = $('<div class="pullToRefresh"><img src="' + finalOption.pullImg + '"></div>');
        // 保留小圖標的快捷方式
        var pullImg = pullToRefresh.find("img");
        // 小圖標加入到容器
        pullContainer.append(pullToRefresh);
        // 小圖標容器添加到滾動區域之前
        $(container).before(pullContainer);
        // 預加載loadImg
        $('<img src="' + finalOption.loadImg + '">');

        // 設置transform的函數
        function cssTransform(node, content) {
            node.css({
                '-webkit-transform' : content,
                '-moz-transform'    : content,
                '-ms-transform'     : content,
                '-o-transform'      : content,
                'transform'         : content,
            });
        }

        // 調整小圖標位置,角度,透明度
        function goTowards(translateY, rotate, opcaticy) {
            // 更新當前小圖標的位置,獲取css(transform)比較麻煩,所以每次變更時自己保存
            curY = translateY;

            // 旋轉圖標(根據抵達lowerBound的比例旋轉,最大轉1圈)
            if (rotate === undefined) {
                rotate = (curY / finalOption.lowerBound) * 360;
            }
            // 透明度根據抵達pauseBound的比例計算
            if (opcaticy === undefined) {
                opcaticy = (curY / finalOption.pauseBound) * 1;
                if (opcaticy > 1) {
                    opcaticy = 1;
                }
            }
            // 改變位置和旋轉角度
            cssTransform(pullToRefresh, "translateY(" + translateY + "px) translateZ(0)" + "rotateZ(" + rotate + "deg)");
            // 改變透明度
            pullToRefresh.css("opacity", opcaticy);
        }

        // 開啟回彈動畫
        function tryStartBackTranTop() {
            // 啟動回彈動畫
            pullToRefresh.addClass("backTranTop");
            // 判斷是否觸發刷新
            if (curY >= finalOption.pauseBound) {
                goTowards(finalOption.pauseBound);
                // 回彈動畫結束發起刷新
                pullToRefresh.on('transitionend webkitTransitionEnd oTransitionEnd', function (event) {
                    // 由於transitionend會對每個屬性回調一次,所以只處理其中一個
                    if (event.originalEvent.propertyName == "transform") {
                        // 暫停動畫
                        pullToRefresh.removeClass("backTranTop");
                        pullToRefresh.unbind();
                        // 透明度重置為1
                        goTowards(finalOption.pauseBound, undefined, 1);
                        // 切換圖片為loading圖
                        pullImg.attr("src", finalOption.loadImg);
                        // 因為anamition會覆蓋transform的原因,使用top臨時定位元素
                        pullToRefresh.addClass("loadingAnimation");
                        pullToRefresh.css("top", finalOption.pauseBound + "px");
                        // 回調刷新數據,最終應將refreshEvent傳回校驗
                        finalOption.onRefresh(function (error, msg) {
                            // 用戶回調時DOM通常已經更新, 需要通知iscroll調整(官方建議延遲執行,涉及到瀏覽器重繪問題)
                            setTimeout(function () {
                                iscroll.refresh();
                            }, 0);
                            // 重置角度,切換為pull圖
                            goTowards(finalOption.pauseBound);
                            // 取消animation,重置top
                            pullToRefresh.removeClass("loadingAnimation");
                            pullToRefresh.css("top", "");
                            // 延遲過渡動畫100毫秒,給瀏覽器重繪的機會
                            setTimeout(function () {
                                // 切換為pull圖
                                pullImg.attr("src", finalOption.pullImg);
                                // 恢復動畫
                                pullToRefresh.addClass("backTranTop");
                                // 刷新完成
                                refreshEvent = null;
                                // 彈回頂部
                                goTowards(-55);
                            }, 100);
                        });
                    }
                });
            } else {
                goTowards(-55); // 彈回頂部
                refreshEvent = null; // 未達成刷新觸發條件
            }
        }

        // 父容器注冊下拉事件
        $(container).on("touchstart", function (event) {
            // 新的觸摸事件
            touchEvent = {};
            // 有一個刷新事件正在進行
            if (refreshEvent) {
                return;
            }
            // 只有滾動軸位置接近頂部, 才可以生成新的刷新事件
            if (iscroll.y < -1 * finalOption.lowerBound) {
                return;
            }

            // 一個新的刷新事件
            refreshEvent = touchEvent;

            touchStartY = event.originalEvent.changedTouches[0].clientY;
            pullStartY = curY;
            // 如果存在,則關閉回彈動畫與相關監聽
            pullToRefresh.removeClass("backTranTop");
            pullToRefresh.unbind();
            // 切換為pull圖
            pullImg.attr("src", finalOption.pullImg);
        }).on("touchmove", function (event) {
            // 在刷新未完成前觸摸,將被忽略
            if (touchEvent != refreshEvent) {
                return;
            }
            var touchCurY = event.originalEvent.changedTouches[0].clientY;
            var touchDistance = touchCurY - touchStartY; // 本次移動的距離
            var curPullY = pullStartY + touchDistance; // 計算圖標應該移動到的位置
            // 向下不能拉出范圍
            if (curPullY > finalOption.lowerBound) {
                curPullY = finalOption.lowerBound;
            }
            // 向上不能拉出范圍
            if (curPullY <= -55) {
                curPullY = -55;
            }
            // 更新圖標的位置
            goTowards(curPullY);
        }).on("touchend", function (event) {
            // 在刷新未完成前觸摸,將被忽略
            if (touchEvent != refreshEvent) {
                return;
            }
            // 嘗試啟動回彈動畫
            tryStartBackTranTop();
        });
    }

    // 初始化iscroll
    setTimeout(function() {
        iscroll.refresh();
    }, 0);

    // 返回操作此區域的工具對象
    return {
        // 用戶如果在下拉刷新之外修改了滾動區域的內容,需要主動調用refresh
        refresh: function() {
            // 延遲以便配合瀏覽器重繪
            setTimeout(function() {
                iscroll.refresh();
            }, 0);
        },
        // 觸發下拉刷新
        triggerPull: function() {
            // 正在刷新或者禁止刷新
            if (refreshEvent || finalOption.noRefresh) {
                return false;
            }
            // 暫停可能正在進行的最終階段回彈動畫
            pullToRefresh.removeClass("backTranTop");
            // 小圖標移動到lowerbound位置
            goTowards(finalOption.lowerBound);
            // 創建新的刷新事件,占坑可以阻止在setTimeout之前的觸摸引起刷新
            refreshEvent = {};
            // 延遲到瀏覽器重繪
            setTimeout(function() {
                tryStartBackTranTop();
            }, 100);
        },
    };
};
Contact GitHub API Training Shop Blog About
© 2017 GitHub, Inc. Terms Privacy Security Status Help

pullToRefresh.css:

.pullToRefresh {
    position:absolute;
    left:0;
    right:0;
    margin:auto;
    width: 50px;
    height: 50px;
    z-index: 10;
    opacity: 1;

    transform:translateY(-55px) translateZ(0) rotateZ(0deg);
    -ms-transform:translateY(-55px) translateZ(0) rotateZ(0deg);     /* IE 9 */
    -moz-transform:translateY(-55px) translateZ(0) rotateZ(0deg);     /* Firefox */
    -webkit-transform:translateY(-55px) translateZ(0) rotateZ(0deg); /* Safari 和 Chrome */
    -o-transform:translateY(-55px) translateZ(0) rotateZ(0deg);     /* Opera */
}
.backTranTop
{
    transition: transform 0.8s ease, opacity 0.8s ease;
    -moz-transition: transform 0.8s ease, opacity 0.8s ease; /* Firefox 4 */
    -webkit-transition: transform 0.8s ease, opacity 0.8s ease; /* Safari 和 Chrome */
    -o-transition: transform 0.8s ease, opacity 0.8s ease; /* Opera */
}
.pullContainer {
    position:relative;
}
.pullToRefresh img {
    display:block;
    width: 40px;
    height: 40px;

    /* 讓img居中在.pullToRefresh中 */
    position: absolute;
    top: 0;
    bottom: 0;
    left:0;
    right:0;
    margin:auto;
}
/* loading旋轉動畫 */
.loadingAnimation
{
    animation: loadingFrame 1s infinite;
    -moz-animation: loadingFrame 1s infinite;    /* Firefox */
    -webkit-animation: loadingFrame 1s infinite;    /* Safari 和 Chrome */
    -o-animation: loadingFrame 1s infinite;    /* Opera */
}
@keyframes loadingFrame
{
    from {
        transform: rotateZ(360deg);
    }
    to {
        transform: rotateZ(0deg);
    }
}

@-moz-keyframes loadingFrame /* Firefox */
{
    from {
        transform: rotateZ(360deg);
    }
    to {
        transform: rotateZ(0deg);
    }
}

@-webkit-keyframes loadingFrame /* Safari 和 Chrome */
{
    from {
        transform: rotateZ(360deg);
    }
    to {
        transform: rotateZ(0deg);
    }
}

@-o-keyframes loadingFrame /* Opera */
{
    from {
        transform: rotateZ(360deg);
    }
    to {
        transform: rotateZ(0deg);
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM