1. 程式人生 > >打造自己的下拉重新整理庫(Ultra-Pull-To-Refresh)(一)

打造自己的下拉重新整理庫(Ultra-Pull-To-Refresh)(一)

上一篇博文打造自己的圖片載入快取庫(Picasso OR Glide)發表之後,非常榮幸得到了部落格專家拭心的肯定,並被轉載到了他的公眾號“安卓進化論”,同時也得到了小組同事們的轉載,在這也非常感謝他們。其實回過頭來看看,實際上自己還是有很多不足和可以改進的地方。故人告訴我們,吾日三省吾身,只有在不斷的踩坑中不斷的總結,才能提升自己。就好比悟空在被敵人打得半死不活之後,不斷地激發自己的潛能,才能進化成超級賽亞人1代、2代、3代……
扯遠了……回到今天的主題。在上一篇文章裡面,我們使用了建造者模式和策略模式對第三方的圖片載入快取庫進行了封裝,形成了自己的一套圖片載入框架。今天我們接著上次的話題——打造自己的一套架構,給大家帶來下拉重新整理庫的打造過程。由於篇幅問題和為了描述得更加清晰,這次的打造過程我們會分3篇文章給大家介紹。

下拉重新整理庫對比

古人云,站在巨人的肩膀上才能看得更遠,所以我們需要先挑選一個功能強大、擴充套件性好的庫,才能為我們後面的工作鋪好路子。跟上次一樣,這裡也推薦一篇目前安卓下拉重新整理庫的對比文章。

Repo 自定義頂部檢視 支援的內容佈局
不支援,只能改程式碼。
由於僅支援其中實現的 LoadingLayout 作為頂檢視,
改程式碼實現自定義工作量較大。
任意檢視,內建: GridView , ListView ,
HorizontalScrollView , ScrollView , WebView
任意檢視。通過繼承 PtrUIHandler
並呼叫 PtrFrameLayout.addPtrUIHandler() 得到最大支援。
任意檢視
不支援,只能改程式碼。
程式碼僅一個 ListView ,耦合度太高,改動工作量較大。
無法擴充套件,自身為 ListView
不支援,此控制元件特點就是頂部檢視及動畫。 任意檢視,只顯示最後一個巢狀的子檢視。
不支援,此控制元件特點就是頂部檢視及動畫。 任意檢視

上面列出的基本上是GitHub裡面比較流行的安卓下拉重新整理庫,當然點開連結看看最後一次提交的時間,確實有些尷尬,但優秀的程式碼總是經得起時間考驗的!
裡面有我們比較熟悉的Android-PullToRefresh,在下拉重新整理剛流行起來的那段時間裡,那可是此詫風雲的。內建了常用的ListView和ScrollView的下拉和上拉載入,頂部、底部載入檢視也是當時最常用的“下拉立即重新整理”、“鬆開馬上載入”帶“菊花”動畫之類的,用起來槓槓的。
相對來說Ultra-Pull-To-Refresh(下面簡稱:Ultra-PTR)是後起之秀,可它擴充套件性卻更加強大,載入的內容佈局因為用的是View,所以支援任意檢視,而頂部、底部檢視則通過PtrUIHandler介面實現,能提供非常好的定製性。短短的1000來行的關鍵程式碼,就實現了一個功能強大的下拉載入庫,自然也受到了後來很多開發者的喜歡。不過唯有他不支援上拉載入更多這點,可能會讓有些人放棄對它的選用。
這裡就不一一對其他的庫進行說明了,詳細可以參考上面的連結。
好了,又到了我們思考的時間。選用哪一個庫,首先考慮我們要實現的目的。我們希望這個庫能支援越多的載入內容View越好,我們希望應對射雞師能非常方便的自定義重新整理頭部和底部,我們希望它既能下拉重新整理也能上拉載入……


綜上所述,我們選用Ultra-PTR,畢竟這也是個靠star吃飯的年代,除了早已不更新的Android-PullToRefresh,就數Ultra-PTR的擴充套件性最好(Star最多)了。而至於上拉載入更多的問題,網上也提供了很多的解決方案,這對於充分實踐拿來主義的我們來說都不是問題。

設計

引入Ultra-PTR我們就能非常容易為我們的介面實現下拉重新整理,他的用法十分簡單,只需要在佈局檔案裡配置,為我們需要下拉重新整理的View外面套一層PtrFrameLayout即可,下面是關鍵程式碼。

    <in.srain.cube.views.ptr.PtrFrameLayout
        xmlns:ptr="http://schemas.android.com/apk/res-auto"
        android:id="@+id/ptr_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        ptr:ptr_duration_to_close_either="1000"
        ptr:ptr_keep_header_when_refresh="true"
        ptr:ptr_pull_to_fresh="false"
        ptr:ptr_resistance="1.7">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/myRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </in.srain.cube.views.ptr.PtrFrameLayout>

上面涉及ptr的api這裡就不作解釋,點選上面的GitHub連結,大家可以去深入瞭解。
知道了Ultra-PTR的用法,我們就開始進行封裝設計。呼叫第三方庫,我們首要原則就是低耦合接入,一個可以保持我們專案程式碼的架構清晰,第二可以為以後切換這個庫對原專案做最少的修改。因此我們考慮用一個基類封裝Ultra-PTR,這個基類將提供下拉重新整理(Ultra-PTR)的功能,並提供一個方法設定滾動內容View,以及一些對下拉重新整理的定製方法。下面是我們設計的完整結構類圖。
PullToRefresh完整結構類圖

  • PullToRefreshBaseView
    下拉重新整理抽象基類,封裝Ultra-PTR,繼承於LinearLayout,主要提供2個抽象方法onInitMainView()和onInitContent(),其中onInitMainView()用於初始化與Ultra-PTR同層級的View,可以是載入失敗圖、載入進度框之類的View,onInitContent()則初始化Ultra-PTR的mContenView,由子類例項化內容View並返回。

  • PullToRefreshRecyclerView
    繼承PullToRefreshBaseView,內部實現了RecyclerView的嵌入,目前只支援LinearLayoutManager佈局方式,主要用於列表模式。實現介面OnPullBothListener,提供下拉重新整理、上拉載入的功能。

  • PullToRefreshScrollView
    繼承PullToRefreshBaseView,內部實現了ScollView的嵌入。這裡並沒有實現OnPullBothListener,或者OnPullRefreshListener(僅下拉重新整理)介面,而交由外部client呼叫時實現,主要是考慮給外部更自由的定製。和RecyclerView不同,因為RecyclerView內部封裝了資料自動裝置和重新整理的邏輯,所以就得在內部處理重新整理事件,繼而需要實現OnPullBothListener介面了。

  • MyPullToRefreshListView
    繼承PullToRefreshRecyclerView,加入了網路請求異常情感圖的View,和一些業務耦合的資料載入、重新整理,主要是針對我們自己專案定製的。

  • 介面
    OnPullBothListener:提供下拉、上拉重新整理方法
    OnPullRefreshListener:僅提供下拉重新整理方法
    OnLastPageHintListener:提供上拉時提示最後一頁的回撥
    OnPullListActionListener:提供資料載入、item點選、item初始化、重新整理完成的回撥方法,主要用於封裝統一的adapter,使介面只需要關係介面請求和介面佈局。

在這一篇裡面,我們主要介紹PullToRefreshBaseView、PullToRefreshRecyclerView和PullToRefreshScrollView的封裝。其中PullToRefreshRecyclerView中RecyclerView的實現會在下一篇介紹,剩下的上層模組呼叫則會在第三篇中介紹。

難點攻克

1、Ultra-PTR只支援xml靜態佈局,無法在PullToRefreshBaseView中單獨解耦

在寫Demo的時候發現Ultra-PTR僅支援在xml靜態佈局,要呼叫只能像上面程式碼中那樣寫死在xml中,這樣就導致我們沒法單獨在基類PullToRefreshBaseView中初始化Ultra-PTR的例項,然後動態地通過onInitContent()方法在子類中初始化mContent了。迴歸根本,這個問題還是得從Ultra-PTR的原始碼入手。

    @Override
    protected void onFinishInflate() {
        final int childCount = getChildCount();
        if (childCount > 3) {
            throw new IllegalStateException("PtrFrameLayout only can host 3 elements");
        } else if (childCount == 3) {
            ......
        } else if (childCount == 2) { // ignore the footer by default
            ......
        } else if (childCount == 1) {
            mContent = getChildAt(0);
        } else {
            TextView errorView = new TextView(getContext());
            ......
            mContent = errorView;
            addView(mContent);
        }
        if (mHeaderView != null) {
            mHeaderView.bringToFront();
        }
        if (mFooterView != null) {
            mFooterView.bringToFront();
        }
        super.onFinishInflate();
    }

以上是PtrFrameLayout初始化mContent的關鍵程式碼,可以看到它是重寫了onFinishInflate()方法,難怪它只能在xml中靜態初始化了。onFinishInflate()方法就是在View中所有的子控制元件被對映成xml後觸發的。所以我們如果要做到動態佈局,則需要我們在addView完mContent之後,手動呼叫一下onFinishInflate()內的程式碼。我們對原始碼稍作修改。

    @Override
    protected void onFinishInflate() {
        doFinishInflate();
        super.onFinishInflate();
    }

    public void doFinishInflate() {
        ......
    }

提供了doFinishInflate()給PullToRefreshBaseView在獲得onInitContent()返回的View之後呼叫,這樣就不必在xml中靜態初始化了。

2、無奈的上拉載入更多

Ultra-PTR不支援上拉載入更多是眾所周知的,作者認為下拉重新整理和上拉載入是無關的兩個功能,而且上拉更多的是與mContent有關,因此應該由具體的呼叫環境自己實現。
這個說法也不無道理,從設計的角度分開實現可能更加靈活,利於擴充套件。所以包括作者在後來提供的方案,以及其他的一些例子都是針對RecyclerView、ListView的上拉載入方案,而對於RecyclerView的上拉實現,基本上都是判斷滾動到最後一個adapter,然後顯示“載入更多…”,然後自動載入更多的item。但結合到我們專案本身,所需要的是有回彈的“載入更多…”,因此這些方案明顯不適用。
在GitHub上面搜了一下,發現一個從Ultra-PTR fork過去的庫,相容了Load-More的情況,而且也是從PtrFrameLayout本身實現的,可以相容所有mContent的上拉載入,看過原始碼和測試幾次之後並未發現重點的bug和效能問題,就決定先拿來主義。

3、RecyclerView的Item點選事件偶爾丟失,導致點選無反應

這個問題可以等整合完之後自己測試一下,回頭再來改完試試。

在整合到專案中實際執行起來之後,我們小組的同事測出了PullToRefreshRecyclerView中的item點選事件偶爾會丟失,需要多點幾次才能觸發的bug。
我先是到Issues裡面看了下,發現了有人也遇到了類似的問題。而單單使用RecyclerView是不會復現這個bug的,開始我懷疑是不是換了Ultra-PTR-Load-More引入的這個bug,我重新換回Ultra-PTR原本的庫也有同樣的問題,所以可以斷定是Ultra-PTR庫本身存在的bug。
一般來說這種點選事件的丟失都是由於滑動和點選事件衝突導致的,如果沒有做好滑動事件的觸發判斷,點選事件就很容易被滑動事件消耗掉。
我們又回到Ultra-PTR的原始碼中,點選事件主要是在dispatchTouchEvent()裡面處理,對比了一下歷史版本,發現作者其實已經對這個問題做了一些的修改,但他只處理了水平滑動的闕值判斷。

    public PtrFrameLayout(Context context, AttributeSet attrs, int defStyle) {
        ......
        final ViewConfiguration conf = ViewConfiguration.get(getContext());
        mTouchSlop = conf.getScaledTouchSlop();
        mPagingTouchSlop = conf.getScaledTouchSlop() * 2;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent e) {
        ......
        int action = e.getAction();
        switch (action) {
            case MotionEvent.ACTION_MOVE:
                ......
                if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) {
                    if (mPtrIndicator.isInStartPosition()) {
                        mPreventForHorizontal = true;
                    }
                }
                ......
        }
    }

其中ViewConfiguration.getScaledTouchSlop() 就是系統判斷手指移動觸發控制元件滑動的距離。這就簡單,我們再處理一下offsetY的滾動判斷就可以了,為了保險起見,我們還加上了ACTION_DOWN 到 ACTION_MOVE 之間的時間判斷,如果時間非常短的話,我們就認為是點選事件,交由子View處理。

    public static final int TimeInterval = 100;

    @Override
    public boolean dispatchTouchEvent(MotionEvent e) {
        if (!isEnabled() || mContent == null || mHeaderView == null) {
            return dispatchTouchEventSupper(e);
        }
        int action = e.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                downTime = System.currentTimeMillis();
                ......
            case MotionEvent.ACTION_MOVE:
                ......
                long moveInterval = System.currentTimeMillis() - downTime;
                if (Math.abs(offsetY) < mTouchSlop && moveInterval < TimeInterval) {
                    return dispatchTouchEventSupper(e);
                }
                ......
        }
    }

再重新執行一下,問題解決。

關鍵類實現

1、PullToRefreshBaseView.java

下拉重新整理抽象基類,封裝Ultra-PTR。

    public abstract class PullToRefreshBaseView extends LinearLayout {

    private final float HEADER_REFRESH_POSITION = 55f;     //重新整理Header停留距離
    private float REAL_HEADER_REFRESH_POSITION;

    {
        float scale = getResources().getDisplayMetrics().density;
        REAL_HEADER_REFRESH_POSITION = HEADER_REFRESH_POSITION * scale + 0.5f;
    }

    //下拉、上拉重新整理控制元件(Ultra-Pull-To-Refresh)
    private PtrFrameLayout ptrLayout;                       
    private View mContent;

    private boolean hadGetWindowFocus = false;
    private int currentHeaderHeight = 0;


    public PullToRefreshBaseView(Context context) {
        super(context);
        onInitMainView();
        mContent = onInitContent();
        initView();
    }

    public PullToRefreshBaseView(Context context, AttributeSet attrs) {
        super(context, attrs);
        onInitMainView();
        mContent = onInitContent();
        initView();
    }

    /**
     * 初始化 PullToRefresh 同層級的View(如:異常提示View)
     * +++++++++++++++++++++++++++++++++++++++++
     * +           必須寫在該方法內            +
     * +++++++++++++++++++++++++++++++++++++++++
     */
    public abstract void onInitMainView();
    /**
     * 初始化 PullToRefresh 的 Context
     */
    public abstract View onInitContent();



    private void initView() {
        if (mContent == null) {
            throw new NullPointerException("Content is null : onInitContent() must called and return valid View");
        }

        setOrientation(VERTICAL);

        ptrLayout = new PtrFrameLayout(getContext());
        ptrLayout.setDurationToCloseHeader(500);
        ptrLayout.setDurationToCloseFooter(500);
        ptrLayout.setKeepHeaderWhenRefresh(true);
        ptrLayout.setPullToRefresh(false);
        ptrLayout.setResistance(1.7f);
        ptrLayout.disableWhenHorizontalMove(true);
        ptrLayout.addView(mContent);
        setMode(Mode.BOTH);

        //佈局
        super.addView(ptrLayout, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        //初始化PtrFrameLayout的HeaderView、mContent、FooterView
        ptrLayout.doFinishInflate();

        if (ptrLayout != null) {
            ptrLayout.setPtrHandler(new PtrDefaultHandler2() {
                @Override
                public void onRefreshBegin(PtrFrameLayout frame) {
                    if (mOnPullRefreshListener != null) {
                        mOnPullRefreshListener.onRefresh();
                    }
                    if (mOnPullBothListener != null) {
                        mOnPullBothListener.onPullDownToRefresh();
                    }
                }

                @Override
                public void onLoadMoreBegin(PtrFrameLayout frame) {
                    if (mOnPullBothListener != null) {
                        mOnPullBothListener.onPullUpToRefresh();
                    }
                }

            });
        }
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if(mContent != null && mContent instanceof ViewGroup && !(child instanceof PtrFrameLayout)) {
            ((ViewGroup) mContent).addView(child, index, params);
        }else {
            super.addView(child, index, params);
        }
    }



    /**
     * 顯示下拉的頂部圖片
     */
    public void showLoadingHeaderImg() {
        if (ptrLayout != null && ptrLayout.getHeaderView() != null) {
            if (ptrLayout.getHeaderView() instanceof CommonPtrDefaultHeader) {
                ((CommonPtrDefaultHeader) ptrLayout.getHeaderView()).showIvTDPtrLoadingHeader();
            }
        }
    }


    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (!hadGetWindowFocus && ptrLayout != null) {
            hadGetWindowFocus = true;
            currentHeaderHeight = ptrLayout.getHeaderHeight();
            ptrLayout.setRatioOfHeaderHeightToRefresh(REAL_HEADER_REFRESH_POSITION / currentHeaderHeight);
            ptrLayout.setOffsetToKeepHeaderWhileLoading((int) REAL_HEADER_REFRESH_POSITION);
        }
    }

    /**
     * 設定預設頭部
     */
    public void setDefaultLoadingHeaderView() {
        CommonPtrDefaultHeader myPtrDefaultHeader = new CommonPtrDefaultHeader(getContext());
        myPtrDefaultHeader.setheaderHeightUpdateListener(new CommonPtrDefaultHeader.HeaderHeightUpdateListener() {
            @Override
            public void headerHeightUpdate() {
                if (ptrLayout != null && ptrLayout.getHeaderHeight() != currentHeaderHeight) {
                    currentHeaderHeight = ptrLayout.getHeaderHeight();
                    ptrLayout.setRatioOfHeaderHeightToRefresh(REAL_HEADER_REFRESH_POSITION / currentHeaderHeight);
                    ptrLayout.setOffsetToKeepHeaderWhileLoading((int) REAL_HEADER_REFRESH_POSITION);
                }
            }
        });
        setLoadingHeaderView(myPtrDefaultHeader);
    }

    /**
     * 設定預設底部
     */
    public void setDefaultLoadingFooterView() {
        setLoadingFooterView(new CommonPtrDefaultFooter(getContext()));
    }

    /**
     * 設定頭部HeaderView
     */
    public void setLoadingHeaderView(PtrUIHandler ptrUIHandler) {
        if (ptrUIHandler != null) {
            if (ptrUIHandler instanceof View) {
                if (ptrLayout != null) {
                    ptrLayout.setHeaderView((View) ptrUIHandler);
                    ptrLayout.addPtrUIHandler(ptrUIHandler);
                }
            } else {
                throw new UnsupportedOperationException("ptrUIHandler is not a View so can't setHeaderView");
            }
        }
    }

    /**
     * 設定底部FooterView
     */
    public void setLoadingFooterView(PtrUIHandler ptrUIHandler) {
        if (ptrUIHandler != null) {
            if (ptrUIHandler instanceof View) {
                if (ptrLayout != null) {
                    ptrLayout.setFooterView((View) ptrUIHandler);
                    ptrLayout.addPtrUIHandler(ptrUIHandler);
                }
            } else {
                throw new UnsupportedOperationException("ptrUIHandler is not a View so can't setFooterView");
            }
        }
    }

    /**
     * 自動重新整理
     */
    public void autoRefresh() {
        if (ptrLayout != null) {
            ptrLayout.autoRefresh();
        }
    }

    /**
     * 重新整理完成,回彈
     */
    public void onRefreshComplete() {
        if (ptrLayout != null) {
            ptrLayout.refreshComplete();
        }
    }


    private OnPullRefreshListener mOnPullRefreshListener;
    private OnPullBothListener mOnPullBothListener;

    public void setOnRefreshListener(OnPullRefreshListener onPullRefreshListener) {
        mOnPullRefreshListener = onPullRefreshListener;
    }

    public void setOnRefreshListener(OnPullBothListener onPullBothListener) {
        mOnPullBothListener = onPullBothListener;
    }

    /**
     * 僅下拉重新整理介面
     */
    public interface OnPullRefreshListener {

        void onRefresh();
    }

    /**
     * 下拉、上拉重新整理介面
     */
    public interface OnPullBothListener {

        void onPullDownToRefresh();

        void onPullUpToRefresh();

    }

    /**
     * 上拉到最後一頁的提示回撥
     */
    public interface OnLastPageHintListener {

        void onLastPageHint();
    }


    /**
     * 自定義封裝Mode轉第三方庫Mode
     *
     * @param mode PullToRefreshBaseView.Mode
     *             <p>
     *             預設:Mode.BOTH
     */
    public void setMode(byte mode) {
        switch (mode) {
            case Mode.REFRESH:
                setMode(PtrFrameLayout.Mode.REFRESH);
                break;
            case Mode.LOAD_MORE:
                setMode(PtrFrameLayout.Mode.LOAD_MORE);
                break;
            case Mode.BOTH:
                setMode(PtrFrameLayout.Mode.BOTH);
                break;
            default:
                setMode(PtrFrameLayout.Mode.NONE);
                break;

        }
    }

    private void setMode(PtrFrameLayout.Mode mode) {
        if (ptrLayout != null) {
            ptrLayout.setMode(mode);
        }
    }

    /**
     * @return ptrLayout內部可滑動子控制元件是否滑動到頂部
     */
    public boolean checkContentViewScrollTop() {
        return ptrLayout != null && ptrLayout.checkContentViewScrollTop();
    }

    /**
     * @return ptrLayout內部可滑動子控制元件是否滑動到底部
     */
    public boolean checkContentViewScrollBottom() {
        return ptrLayout != null && ptrLayout.checkContentViewScrollBottom();
    }

    private float eventX, eventY;

    /**
     * 處理滑動事件衝突
     *
     * @param event
     * @return
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (ptrLayout != null) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    getParent().requestDisallowInterceptTouchEvent(true);
                    eventX = event.getX();
                    eventY = event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (Math.abs(eventX - event.getX()) > Math.abs(eventY - event.getY())) {
                        // 橫向滑動事件時 滑動事件交予父容器處理
                        getParent().requestDisallowInterceptTouchEvent(false);
                    } else if (ptrLayout.getMode() == PtrFrameLayout.Mode.LOAD_MORE && eventY < event.getY() && checkContentViewScrollTop()) {
                        // 當載入模式為載入更多模式下  可滑動子佈局滑動到頂部時 滑動事件交予父容器處理
                        getParent().requestDisallowInterceptTouchEvent(false);
                    } else if (ptrLayout.getMode() == PtrFrameLayout.Mode.REFRESH && eventY > event.getY() && checkContentViewScrollBottom()) {
                        // 當載入模式為下拉重新整理模式下  可滑動子佈局滑動到底部時 滑動事件交予父容器處理
                        getParent().requestDisallowInterceptTouchEvent(false);
                    } else {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    eventX = event.getX();
                    eventY = event.getY();
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    getParent().requestDisallowInterceptTouchEvent(false);
                    break;
            }
        }
        return super.dispatchTouchEvent(event);
    }

    /**
     * 重新整理型別
     */
    public class Mode {
        /**
         * 不能重新整理
         */
        public static final byte NONE = 1;

        /**
         * 下拉
         */
        public static final byte REFRESH = 2;

        /**
         * 上拉
         */
        public static final byte LOAD_MORE = 3;

        /**
         * 下拉、上拉
         */
        public static final byte BOTH = 4;

    }
}

其中CommonPtrDefaultHeader和CommonPtrDefaultFooter分別是實現PtrUIHandler介面的HeaderView和FooterView。
這裡稍微提一下onInitMainView()這個方法,註釋提到初始化PTR的同級View必須寫到該方法內,也就是類似要加網路載入失敗View到LinearLayout上,必須寫到這個方法裡面來。這是為什麼呢?我們往下面看,PullToRefreshBaseView重寫了addView方法,其目的是為了能相容類似PullToRefreshScrollView的使用,需要在其內部靜態或者動態增加元素。

@Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if(mContent != null && mContent instanceof ViewGroup && !(child instanceof PtrFrameLayout)) {
            ((ViewGroup) mContent).addView(child, index, params);
        }else {
            super.addView(child, index, params);
        }
    }

從程式碼中可以看出,如果在mContent初始化完成之後,addView的子View都會加到mContent中的,所以onInitMainView()我們必須在onInitContent()前呼叫,因此要為LinearLayout增加子View,則必須在onInitMainView()中初始化了。

2、PullToRefreshRecyclerView.java

繼承PullToRefreshBaseView,內部實現了RecyclerView的嵌入。鑑於這裡面涉及比較多了RecyclerView的使用和統一封裝,具體會在下一篇博文中詳細講解,這裡只貼出關鍵的實現程式碼。

    public abstract class PullToRefreshRecyclerView<T> extends PullToRefreshBaseView implements PullToRefreshBaseView.OnPullBothListener {

    private final String TIPS_LOAD_DATA = "載入中…";

    private RecyclerView mRecyclerView;
    private LinearLayoutManager mLinearLayoutManager;
    private PTRRecyclerViewDecoration myDecoration;
    private List<View> mHeaderViewList;
    private List<View> mFooterViewList;
    private CommonBaseAdapter<T> commonBaseAdapter;

    private OnPullListActionListener<T> mOnPullListActionListener;
    private OnLastPageHintListener mOnLastPageHintListener;

    public List<T> mList = new ArrayList<>();
    public int mTotalCount;
    public int mPageIndex;

    @Override
    public View onInitContent() {
        mRecyclerView = new RecyclerView(getContext());
        mRecyclerView.setLayoutParams(new RecyclerView.LayoutParams(-1, -1));
        return mRecyclerView;
    }

    public PullToRefreshRecyclerView(Context context, OnPullListActionListener<T> mPullListActionListener) {
        super(context);
        mOnPullListActionListener = mPullListActionListener;
        initView();
    }

    public PullToRefreshRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public PullToRefreshRecyclerView(Context context) {
        super(context);
        initView();
    }

    private void initView() {
        setDefaultLoadingHeaderView();
        setDefaultLoadingFooterView();
        setOnRefreshListener(this);

        mLinearLayoutManager = new LinearLayoutManager(getContext());

        mRecyclerView.setLayoutManager(mLinearLayoutManager);
        mRecyclerView.setHasFixedSize(true);    //確定每個item高度相同,提高效能
        mRecyclerView.setAdapter(new EmptyRecyclerViewAdapter(getContext()));
    }


    public RecyclerView getRecyclerView() {
        return mRecyclerView;
    }

    @Override
    public void onPullDownToRefresh() {
        loadRefreshData(false);
    }

    @Override
    public void onPullUpToRefresh() {
        if (mPageIndex <= mTotalCount) {
            loadMoreData(false);
        } else {
            if (mOnLastPageHintListener != null) {
                mOnLastPageHintListener.onLastPageHint();
            }

            onRefreshComplete();
            if (mOnPullListActionListener != null) {
                mOnPullListActionListener.onRefreshComplete();
            }
        }
    }

    /**
     * 下拉重新整理載入資料
     */
    public void loadRefreshData(boolean isShowTops) {
        String tips = isShowTops ? TIPS_LOAD_DATA : "";
        mPageIndex = 1;

        if (mOnPullListActionListener != null) {
            mOnPullListActionListener.loadData(getId(), mPageIndex, tips);
        }
    }
    /**
     * 上拉重新整理載入更多資料
     */
    public void loadMoreData(boolean isShowTops) {
        String tips = isShowTops ? TIPS_LOAD_DATA : "";

        if (mOnPullListActionListener != null) {
            mOnPullListActionListener.loadData(getId(), mPageIndex, tips);
        }
    }


    public void setDivider(int padding, int divider) {
        if (padding > 0 && divider >= 0) {
            Drawable _divider = divider != 0 ? getResources().getDrawable(divider) : null;
            myDecoration = new PTRRecyclerViewDecoration(getContext(), PTRRecyclerViewDecoration.VERTICAL_LIST, _divider, (int) getResources().getDimension(padding));
            mRecyclerView.addItemDecoration(myDecoration);
        }
    }

    public void addHeaderView(View headerView) {
        if (mHeaderViewList == null){
            mHeaderViewList = new ArrayList<>();
        }
        mHeaderViewList.add(headerView);
        if(myDecoration != null) {
            myDecoration.isHadHeader = true;
        }
    }
    public void addFooterView(View footerView) {
        if (mFooterViewList == null){
            mFooterViewList = new ArrayList<>();
        }
        mFooterViewList.add(footerView);
        if(myDecoration != null) {
            myDecoration.isHadFooter = true;
        }

        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter != null && !(adapter instanceof EmptyRecyclerViewAdapter)) {
            if (adapter instanceof HeaderAndFooterWrapper) {
                ((HeaderAndFooterWrapper) adapter).addFooterView(footerView);
                adapter.notifyDataSetChanged();
            }else {
                HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
                headerAndFooterWrapper.addFooterView(footerView);
                mRecyclerView.setAdapter(headerAndFooterWrapper);
            }
        }

    }
    public void removeFooterView(View footerView) {
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter != null && (adapter instanceof HeaderAndFooterWrapper)) {
            ((HeaderAndFooterWrapper) adapter).removeFooterView(footerView);

            if (mFooterViewList != null && mFooterViewList.indexOf(footerView) != -1) {
                mFooterViewList.remove(footerView);
            }
        }
    }

    public void scrollToTop() {
        mLinearLayoutManager.scrollToPositionWithOffset(0, 0);
    }

    public RecyclerView.Adapter getAdapter() {
        return mRecyclerView.getAdapter();
    }
    public void setPageIndex(int page) {
        mPageIndex = page;
    }
    public List<T> getList(){
        return mList;
    }

    public void notifyDataSetChanged(){
        if (mRecyclerView != null && mRecyclerView.getAdapter() != null) {
            mRecyclerView.getAdapter().notifyDataSetChanged();
        }
    }

    /**
     * 顯示資料
     */
    public void showAllData(List<T> list, int itemLayoutId) {
        if (commonBaseAdapter == null) {
            commonBaseAdapter = new MyListAdapter(getContext(), list, itemLayoutId);
            mRecyclerView.setAdapter(getWrappedListAdapter(commonBaseAdapter));

        } else {
            getAdapter().notifyDataSetChanged();
        }
    }

    /**
     * 包裝adapter,增加HeaderView,FooterView
     */
    private RecyclerView.Adapter getWrappedListAdapter(RecyclerView.Adapter adapter) {

        if ((mHeaderViewList != null && mHeaderViewList.size() != 0) || (mFooterViewList != null && mFooterViewList.size() != 0)) {
            HeaderAndFooterWrapper headerAndFooterWrapper = new HeaderAndFooterWrapper(adapter);
            //增加HeaderView
            if (mHeaderViewList != null && mHeaderViewList.size() != 0) {
                headerAndFooterWrapper.addHeaderView(mHeaderViewList);
            }
            //增加FooterView
            if (mFooterViewList != null && mFooterViewList.size() != 0) {
                headerAndFooterWrapper.addFooterView(mFooterViewList);
            }
            return headerAndFooterWrapper;
        }
        return adapter;
    }


    /**
     * 封裝RecyclerView.Adapter,相容ViewHolder
     */
    private class MyListAdapter extends CommonBaseAdapter<T> {
        public MyListAdapter(Context context, List<T> mData, int itemLayoutId) {
            super(context, mData, itemLayoutId);
        }

        @Override
        protected void onItemClick(View itemView, int position) {
            if (position >= 0 && mList.size() > 0) {
                T item = mList.get(position);
                if (mOnPullListActionListener != null && item != null) {
                    int numHeaderView = mHeaderViewList != null ? mHeaderViewList.size() : 0;
                    mOnPullListActionListener.clickItem(getId(), item, position + numHeaderView);
                }
            }
        }

        @Override
        protected void convert(ViewHolder holder, T item, List<T> list, int position) {
            if (mOnPullListActionListener != null && item != null) {
                mOnPullListActionListener.createListItem(getId(), holder, item, list, position);
            }
        }
    }

    public void setOnPullListActionListener(OnPullListActionListener mPullListActionListener) {
        mOnPullListActionListener = mPullListActionListener;
    }

    public void setOnLastPageHint(OnLastPageHintListener mLastPageHintListener) {
        mOnLastPageHintListener = mLastPageHintListener;
    }
}

3、PullToRefreshScrollView.java

繼承PullToRefreshBaseView,內部實現了ScollView的嵌入。這部分程式碼比較簡單,主要就是在onInitContent()裡面例項化了有一個ScrollView,然後返回給PullToRefreshBaseView加入到Ultra-PTR的mContent中。由此也可以看出,我們封裝完PullToRefreshBaseView之後,要實現一些View的下拉重新整理功能跟原本用Ultra-PTR一樣都是非常簡單的。

    public class PullToRefreshScrollView extends PullToRefreshBaseView {

    private ScrollView mScrollView;

    @Override
    public void onInitMainView() {
    }

    @Override
    public View onInitContent() {
        mScrollView = new ScrollView(getContext());
        mScrollView.setLayoutParams(new ScrollView.LayoutParams(-1, -1));
        return mScrollView;
    }

    public PullToRefreshScrollView(Context context) {
        super(context);
        initView();
    }

    public PullToRefreshScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        mScrollView.setVerticalFadingEdgeEnabled(false);
        mScrollView.setVerticalScrollBarEnabled(false);

        setDefaultLoadingHeaderView();
        setMode(Mode.REFRESH);
    }

    public void fullScroll(int direction) {
        mScrollView.fullScroll(direction);
    }
}

xml佈局中呼叫

    <com.commonlib.pulltorefresh.PullToRefreshScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:cacheColorHint="@android:color/transparent"
        android:scrollbars="none">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/white"
                android:gravity="center_vertical"
                android:orientation="horizontal">
                ......
                ......
            </LinearLayout>

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="1px"
                android:paddingLeft="@dimen/dp_14"
                android:paddingRight="@dimen/dp_14"
                android:background="@drawable/dividing_padding_line"
                android:focusable="true"
                android:focusableInTouchMode="true" />
        </LinearLayout>
    </com.commonlib.pulltorefresh.PullToRefreshScrollView>

結語

以上就給大家介紹了下拉重新整理庫的設計和封裝過程,核心的部分還是在於PullToRefreshBaseView,通過Ultra-PTR-Load-More實現了下拉重新整理、上拉載入的功能,提供onInitContent()抽象方法,給子類實現載入的內容View。同時我們繼承PullToRefreshBaseView實現了PullToRefreshRecyclerView和PullToRefreshScrollView,可以直接使用PullToRefreshRecyclerView就能實現RecyclerView列表模式和ScrollView的下拉和上拉重新整理,用起來非常簡單,完全可以不用關心RecyclerView如何呼叫。到底如果使用,我們會在後面的兩篇文章中繼續為大家介紹。
回顧一下,我們主要用到的是Ultra-PTR這個第三方庫,但真正呼叫起來