TwinklingRefreshLayout 源碼分析

項目地址:TwinklingRefreshLayout 本文分析版本:0a9b613

1.簡介

scrollview

以下是 TwinklingRefreshLayout 的官方介紹:
TwinklingRefreshLayout 延伸了 Google 的 SwipeRefreshLayout 的思想,不在列表控件上動刀,而是使用一個 ViewGroup 來包含列表控件,以保持其較低的耦合性和較高的通用性。其主要特性有:

  • 支持 RecyclerView杏瞻、ScrollView调限、AbsListView 系列(ListView、GridView)矾屯、WebView 以及其它可以獲取到 scrollY 的控件
  • 支持加載更多
  • 默認支持 越界回彈,隨手勢速度有不同的效果
  • 可開啟沒有刷新控件的純凈越界回彈模式
  • setOnRefreshListener 中擁有大量可以回調(diào)的方法
  • 將 Header 和 Footer 抽象成了接口,并回調(diào)了滑動過程中的系數(shù),方便實現(xiàn)個性化的 Header 和 Footer

2.使用方法

下面以 scrollView 為例子對下拉刷新和上拉加載來做分析

1初厚、在 XML 文件中聲明
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <include layout="@layout/inc_toolbar" />

    <com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout
        android:id="@+id/refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#afbfff"
            android:overScrollMode="never">

            .
            .
            .

        </ScrollView>
    </com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout>

</LinearLayout>

在 XML 文件中只要在滑動控件 ScrollView 外嵌套 TwinklingRefreshLayout 就可以了件蚕,TwinklingRefreshLayout 包含很多自定義的屬性在下面的分析中我們會看到。

2、在 JAVA 文件中設(shè)置回調(diào)
refreshLayout.setOnRefreshListener(new RefreshListenerAdapter() {
    @Override
    public void onRefresh(final TwinklingRefreshLayout refreshLayout) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                refreshLayout.finishRefreshing();
            }
        }, 4000);
    }
});

TwinklingRefreshLayout 項目中給出了一個控件狀態(tài)的回調(diào)監(jiān)聽排作,上面的代碼中只監(jiān)聽了 onRefresh 的狀態(tài)牵啦,我們來看下 RefreshListenerAdapter 的實現(xiàn),看看有多少狀態(tài)纽绍。

public abstract class RefreshListenerAdapter implements PullListener {
        @Override
        public void onPullingDown(TwinklingRefreshLayout refreshLayout, float fraction) {
        }

        @Override
        public void onPullingUp(TwinklingRefreshLayout refreshLayout, float fraction) {
        }

        @Override
        public void onPullDownReleasing(TwinklingRefreshLayout refreshLayout, float fraction) {
        }

        @Override
        public void onPullUpReleasing(TwinklingRefreshLayout refreshLayout, float fraction) {
        }

        @Override
        public void onRefresh(TwinklingRefreshLayout refreshLayout) {
        }

        @Override
        public void onLoadMore(TwinklingRefreshLayout refreshLayout) {
        }

        @Override
        public void onFinishRefresh() {
        }

        @Override
        public void onFinishLoadMore() {
        }
    }

RefreshListenerAdapter 是一個抽象類蕾久,實現(xiàn)了 PullListener 接口:

public interface PullListener {
        /**
         * 下拉中
         */
        void onPullingDown(TwinklingRefreshLayout refreshLayout, float fraction);

        /**
         * 上拉
         */
        void onPullingUp(TwinklingRefreshLayout refreshLayout, float fraction);

        /**
         * 下拉松開
         */
        void onPullDownReleasing(TwinklingRefreshLayout refreshLayout, float fraction);

        /**
         * 上拉松開
         */
        void onPullUpReleasing(TwinklingRefreshLayout refreshLayout, float fraction);

        /**
         * 刷新中。拌夏。僧著。
         */
        void onRefresh(TwinklingRefreshLayout refreshLayout);

        /**
         * 加載更多中
         */
        void onLoadMore(TwinklingRefreshLayout refreshLayout);

        /**
         * 手動調(diào)用finishRefresh或者finishLoadmore之后的回調(diào)
         */
        void onFinishRefresh();

        void onFinishLoadMore();
}

RefreshListenerAdapter 實現(xiàn)了 PullListener 接口變成一個抽象類,這讓使用者更加的靈活障簿,只關(guān)注使用到的狀態(tài)回調(diào)盹愚,而不需要所有的狀態(tài)回調(diào),抽象類可以做到這一點站故,只覆寫需要的函數(shù)皆怕,而接口不一樣,繼承了接口就必須覆寫接口內(nèi)部所有函數(shù)西篓。這一點很值得學(xué)習(xí)愈腾!

3、核心類 TwinklingRefreshLayout 的分析:

首先看一張官方的類解析圖:

TwinklingRefreshLayout

通過圖中可以看到 TwinklingRefreshLayout 包含了一個 HeaderView 和一個 BottomView 岂津,這兩個 View 分別在上拉和下拉的時候顯示出來虱黄。其中內(nèi)部還包含了一個名字叫 CoProcessor 的類, CoProcessor 又包含三個 Processor ,我們來看下 CoProcessor 的構(gòu)造函數(shù)就明白了:

public class CoProcessor {
    private RefreshProcessor refreshProcessor;
    private OverScrollProcessor overScrollProcessor;
    private AnimProcessor animProcessor;

    public CoProcessor() {
        animProcessor = new AnimProcessor(this);
        overScrollProcessor = new OverScrollProcessor(this);
        refreshProcessor = new RefreshProcessor(this);
    }

    ......
}

CoProcessor相當于是一個總的調(diào)度器吮成,把任務(wù)分配給三個不同的Processor來處理橱乱,從名字上來看AnimProcessor處理動畫,OverScrollProcessor處理滑動中的越界回彈粱甫,RefreshProcessor處理刷新動作泳叠。 CoProcessor的初始化工作是在TwinklingRefreshLayout的構(gòu)造函數(shù)中完成。接下來看一下CoProcessor如何調(diào)度的茶宵,首先從攔截事件開始看:

/**
 * 攔截事件
 *
 *
 @return return true時,ViewGroup的事件有效,執(zhí)行onTouchEvent事件
 * return false時,事件向下傳遞,onTouchEvent無效 */
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercept = cp.interceptTouchEvent(ev);
    return intercept || super.onInterceptTouchEvent(ev);
  }

  @Override
  public boolean onTouchEvent(MotionEvent e) {
    boolean resume = cp.consumeTouchEvent(e);
    return resume || super.onTouchEvent(e);
  }

cp.interceptTouchEvent(ev)調(diào)用了refreshProcessor.interceptTouchEvent(ev)危纫,來看下interceptTouchEvent做了些什么事:

public boolean interceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mTouchX = ev.getX();
            mTouchY = ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            float dx = ev.getX() - mTouchX;
            float dy = ev.getY() - mTouchY;
            if (Math.abs(dx) <= Math.abs(dy)) {//滑動允許最大角度為45度
                if (dy > 0 && !ScrollingUtil.canChildScrollUp(cp.getScrollableView()) && cp.allowPullDown()) {
                    cp.setStatePTD();
                    return true;
                } else if (dy < 0 && !ScrollingUtil.canChildScrollDown(cp.getScrollableView()) && cp.allowPullUp()) {
                    cp.setStatePBU();
                    return true;
                }
            }
            break;
    }
    return false;
}

ACTION_DOWN產(chǎn)生時不攔截事件只記錄坐標,當ACTION_MOVE產(chǎn)生的時候乌庶,計算移動距離dy种蝶、ScrollingUtil.canChildScrollUp(cp.getScrollableView())用來判斷子 View 是否還可以下拉,如果條件都滿足安拟,就調(diào)用cp.setStatePTD()改變狀態(tài),并且返回true表示攔截事件宵喂,接下去的事件就不下發(fā)給子View糠赦,直接交給onTouchEvent來處理:

public boolean consumeTouchEvent(MotionEvent e) {
    if (cp.isRefreshVisible() || cp.isLoadingVisible()) return false;

    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:
            float dy = e.getY() - mTouchY;
            if (cp.isStatePTD()) {
                dy = Math.min(cp.getMaxHeadHeight() * 2, dy);
                dy = Math.max(0, dy);
                cp.getAnimProcessor().scrollHeadByMove(dy);
            } else if (cp.isStatePBU()) {
                //加載更多的動作
                dy = Math.min(cp.getBottomHeight() * 2, Math.abs(dy));
                dy = Math.max(0, dy);
                cp.getAnimProcessor().scrollBottomByMove(dy);
            }
            return true;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            if (cp.isStatePTD()) {
                cp.getAnimProcessor().dealPullDownRelease();
            } else if (cp.isStatePBU()) {
                cp.getAnimProcessor().dealPullUpRelease();
            }
            return true;
    }
    return false;
}

根據(jù)ACTION_MOVE中的 if 條件計算出 dy 再通過 cp 來調(diào)試 AnimProcessor 做相應(yīng)的動畫,我們來看下scrollHeadByMove函數(shù)是如何實現(xiàn)頭部的移動的:

public void scrollHeadByMove(float moveY) {
    float offsetY = decelerateInterpolator.getInterpolation(moveY / cp.getMaxHeadHeight() / 2) * moveY / 2;
    if (cp.getHeader().getVisibility() != VISIBLE) cp.getHeader().setVisibility(VISIBLE);
    if (cp.isPureScrollModeOn()) cp.getHeader().setVisibility(GONE);

    cp.getHeader().getLayoutParams().height = (int) Math.abs(offsetY);
    cp.getHeader().requestLayout();
    if (!cp.isOpenFloatRefresh()) {
        cp.getContent().setTranslationY(offsetY);
        translateExHead((int) offsetY);
    }
    cp.onPullingDown(offsetY);
}

根據(jù)cp.getAnimProcessor().scrollHeadByMove(dy)之前的計算可以看出dy的最大值是cp.getMaxHeadHeight()的兩倍,所以scrollHeadByMove函數(shù)中offsetY的值最大應(yīng)該等于cp.getMaxHeadHeight()拙泽,這就是為什么你再怎么拖動ScrollView淌山,上面的HeadView最多也就顯示這么點。接下去兩個if判斷是否顯示HeaderView顾瞻。

//如果沒有設(shè)置懸浮泼疑,那么需要手動對 ScrollView 做移動操作
//因為 ACTION_MOVE 已經(jīng)被 TwinklingRefreshLayout 攔截,ScrollView 無法響應(yīng) ACTION_MOVE荷荤。
if (!cp.isOpenFloatRefresh()) {
    cp.getContent().setTranslationY(offsetY);
    translateExHead((int) offsetY);
}

繼續(xù)回到consumeTouchEvent函數(shù)中退渗,ACTION_MOVE動作完了以后,產(chǎn)生ACTION_UP調(diào)用cp.getAnimProcessor().dealPullDownRelease():

public void dealPullDownRelease() {
        if (!cp.isPureScrollModeOn() && getVisibleHeadHeight() >= cp.getHeadHeight() - cp.getTouchSlop()) {
        //下拉出的高度大于Head的高度就做刷新操作
        animHeadToRefresh();
    } else {
        //下拉的距離不夠蕴纳,回彈動畫
        animHeadBack();
    }
}

執(zhí)行animHeadToRefresh函數(shù)時對于HeadView做了会油,回到正常高度動畫的操作。動畫完成后會調(diào)用pullListener.onRefresh(TwinklingRefreshLayout.this);這就是我們在自己的 Activity 可以捕捉到的回調(diào)古毛。
animHeadBack就是一個收起頭部的動畫翻翩。
至此,一次下拉刷新的操作算是完成了稻薇。上拉加載更多嫂冻,原理是一樣的就是方向變化了,有興趣的可以看下實現(xiàn)塞椎,這邊就不做重復(fù)的分析了桨仿。

目前還沒提到的就是OverScrollProcessor這個處理器了,這個處理器是用來做越界回彈的忱屑,代碼比較少蹬敲,有興趣的同學(xué)可以自行分析。

4莺戒、總結(jié)

整個項目伴嗡,代碼邏輯很清晰,亮點在于把整個 ViewGroup 的操作从铲,按三個處理器分拆了瘪校,擴展性很好,可以很容易的去理解整個流程名段,這種分拆方式很值得學(xué)習(xí)阱扬!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市伸辟,隨后出現(xiàn)的幾起案子麻惶,更是在濱河造成了極大的恐慌,老刑警劉巖信夫,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窃蹋,死亡現(xiàn)場離奇詭異卡啰,居然都是意外死亡,警方通過查閱死者的電腦和手機警没,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進店門匈辱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人杀迹,你說我怎么就攤上這事亡脸。” “怎么了树酪?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵浅碾,是天一觀的道長。 經(jīng)常有香客問我嗅回,道長及穗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任绵载,我火速辦了婚禮埂陆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘娃豹。我一直安慰自己焚虱,他們只是感情好,可當我...
    茶點故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布懂版。 她就那樣靜靜地躺著鹃栽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪躯畴。 梳的紋絲不亂的頭發(fā)上民鼓,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天,我揣著相機與錄音蓬抄,去河邊找鬼丰嘉。 笑死,一個胖子當著我的面吹牛嚷缭,可吹牛的內(nèi)容都是我干的饮亏。 我是一名探鬼主播,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼阅爽,長吁一口氣:“原來是場噩夢啊……” “哼路幸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起付翁,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤简肴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后百侧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體砰识,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡杂伟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了仍翰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,683評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡观话,死狀恐怖予借,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情频蛔,我是刑警寧澤灵迫,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站晦溪,受9級特大地震影響瀑粥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜三圆,卻給世界環(huán)境...
    茶點故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一狞换、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧舟肉,春花似錦修噪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至整慎,卻和暖如春脏款,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背裤园。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工撤师, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人比然。 一個月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓丈氓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親强法。 傳聞我的和親對象是個殘疾皇子万俗,可洞房花燭夜當晚...
    茶點故事閱讀 43,566評論 2 349

推薦閱讀更多精彩內(nèi)容