1. 程式人生 > >easyrecyclerview 重新整理載入功能程式碼分析(填坑之旅)

easyrecyclerview 重新整理載入功能程式碼分析(填坑之旅)

想選一個重新整理載入 又可以新增各種header 的列表控制元件,挑來挑去也就easyrecyclerview 最好用了,
可是重新整理載入 卻也有bug

  • 1.重新整理的時候不能載入,載入的時候不能重新整理,解決重新整理的時候不能載入(我的方案給個變數isRefreshing 重新整理的時候為true
    載入回掉介面的時候,如果是true就不讓他載入),解決載入的 時候不能重新整理(彈出進度對話方塊)這兩種解決方案比較噁心,需要
    寫在介面的程式碼裡,這是我不願意看到的

  • 第二個bug,我在專案裡算是bug,每次重新整理資料的時候,都會自動去載入,每次進入介面不光呼叫重新整理的介面,也會呼叫載入的
    介面。

  • 3.第三個缺陷,不能定製重新整理的樣式,和載入的樣式,這個我後面講怎麼去改它
    EasyRecyclerView繼承FrameLayout,

填充了下面的佈局程式碼
<com.jude.easyrecyclerview.swipe.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ptr_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
> <android.support.v7.widget.RecyclerView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="vertical|horizontal" android:clickable="true"/> <FrameLayout android:id="@+id/empty"
android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" /> <FrameLayout android:id="@+id/progress" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" /> <FrameLayout android:id="@+id/error" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" /> </com.jude.easyrecyclerview.swipe.SwipeRefreshLayout>
  • 所以你可以設定剛進入前面時候的佈局id=progress
    資料為空的時候的佈局 id = empty
    發生錯誤的時候佈局 id = error
    這個控制元件的功能簡直和它的名字一樣感人easy
public void setAdapterWithProgress(RecyclerView.Adapter adapter) {
        mRecycler.setAdapter(adapter);
        adapter.registerAdapterDataObserver(new EasyDataObserver(this));
        //只有Adapter為空時才顯示ProgressView
        if (adapter instanceof RecyclerArrayAdapter){
            if (((RecyclerArrayAdapter) adapter).getCount() == 0){
                showProgress();
            }else {
                showRecycler();
            }
        }else {
            if (adapter.getItemCount() == 0){
                showProgress();
            }else {
                showRecycler();
            }
        }
    }

如果資料為0的時候載入showProgress()方法
如果資料不是0 顯示recyclerview

下面先介紹一下重新整理的原理
重新整理是通過 swiperefreshlayout,這個應該support v4包下面的一個控制元件
通過serRefresh方法設定它是否重新整理和重新整理完畢
我們通過在EasyRecyclerView類裡面找到這個方法,基本就可以跟蹤到它是怎麼重新整理的了,結果你會發現你只找到了一處呼叫setrefresh(false)的方法

 private void hideAll(){
        mEmptyView.setVisibility(View.GONE);
        mProgressView.setVisibility(View.GONE);
        mErrorView.setVisibility(GONE);
        mPtrLayout.setRefreshing(false);
        mRecycler.setVisibility(View.INVISIBLE);
    }

沒有呼叫setrefresh(true)的方法 wtf,沒關係繼續跟蹤這個方法的類swipefreshlayou
你會發現一個方法

public boolean dispatchTouchEvent(MotionEvent ev) {
        return mPtrLayout.dispatchTouchEvent(ev);
 }
 easyrecylerview的事件傳遞給了swiperefreshlayou了,所以推斷setrefresh(true)
 應該是它自己根據滑動事件去處理的。
 我們接下來在swperefreshlayout裡面去找setrefresh(true)的方法
 發現了被這個方法呼叫:
 private void finishSpinner(float overscrollTop) {
        if (overscrollTop > mTotalDragDistance) {
            setRefreshing(true, true /* notify */);
        } else {
            // cancel refresh
            mRefreshing = false;
            mProgress.setStartEndTrim(0f, 0f);
            AnimationListener listener = null;
            if (!mScale) {
                listener = new AnimationListener() {

                    @Override
                    public void onAnimationStart(Animation animation) {
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        if (!mScale) {
                            startScaleDownAnimation(null);
                        }
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }

                };
            }
            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
            mProgress.showArrow(false);
        }
    }

    跟蹤finishSpinner的方法發現兩處呼叫onTouchEvent 和onStopNestedScroll
    證明猜想正確,別問我為什麼不分析這些事件程式碼,因為我還沒有改它的需求,所以我就避開了這個麻煩。
    基本確定setrefresh(true)根據滑動事件主動觸發

    下面我們去看看setrefresh(false)方法什麼時候呼叫呢
     上面說了hidall 接著就去跟蹤hideall方法,

    被這兩個方法呼叫
      public void showProgress() {
        log("showProgress");
        if (mProgressView.getChildCount()>0){
            hideAll();
            mProgressView.setVisibility(View.VISIBLE);
        }else {
            showRecycler();
        }
    }


    public void showRecycler() {
        log("showRecycler");
        hideAll();
        mRecycler.setVisibility(View.VISIBLE);
    }

     public void showEmpty() {
        log("showEmpty");
        if (mEmptyView.getChildCount()>0){
            hideAll();
            mEmptyView.setVisibility(View.VISIBLE);
        }else {
            showRecycler();
        }
    }

 接著你會發現這兩個方法會被setAdapterWithProgress這個方法 呼叫,這個是設定adapter的方法 ,怎麼可能,我可是想要尋求停止重新整理的
 方法,這個只會在初始化的時候呼叫一次啊,說明還有其他地方呼叫了,我們看看findUsages 找找。

你會發先easydataObserver類裡面有一個update方法
 //自動更改Container的樣式
    private void update() {
        int count;
        if (recyclerView.getAdapter() instanceof RecyclerArrayAdapter) {
            count = ((RecyclerArrayAdapter) recyclerView.getAdapter()).getCount();
        } else {
            count = recyclerView.getAdapter().getItemCount();
        }
        if (count == 0) {
            recyclerView.showEmpty();
        } else {
            recyclerView.showRecycler();
        }
    }

    class EasyDataObserver extends RecyclerView.AdapterDataObserver 
    這個類繼承了 RecyclerView.AdapterDataObserver ,此時你需要弄清一個原理
    recyclerView 和listview一樣資料填充都是通過adapter,adapter的實現通過觀察者模式,資料更新的時候會發一個通知
    註冊了這個通知的類就會收到資料更新的通知。

    我們跟蹤EasyDataObserver 這個類看看,
     public void setAdapter(RecyclerView.Adapter adapter) {
        ...
        adapter.registerAdapterDataObserver(new EasyDataObserver(this));
        ...
    }

    /**
     * 設定介面卡,關閉所有副view。展示進度條View
     * 介面卡有更新,自動關閉所有副view。根據條數判斷是否展示EmptyView
     *
     * @param adapter
     */
    public void setAdapterWithProgress(RecyclerView.Adapter adapter) {
        mRecycler.setAdapter(adapter);
        adapter.registerAdapterDataObserver(new EasyDataObserver(this));
        ...
    }

    看見了吧設定adapter的時候 會註冊一個數據更新的監聽器

    所以每次adapter 新增資料的時候都會通知EasyDataObserver,然後呼叫update
    再去呼叫showempty或者showrecycer方法 他們裡面會有一個hideall方法 呼叫setrefres(false)去取消重新整理
    我們看看RecyclerArrayAdapter 裡面新增資料的方法  他們總會呼叫一個方法notifyItemInserted 去給EasyDataObserver
    傳送通知
      public void add(T object) {
        ...
        if (mNotifyOnChange) notifyItemInserted(headers.size()+getCount());
        ...
    }

    public void addAll(Collection<? extends T> collection) {
        ...
        if (mNotifyOnChange) notifyItemRangeInserted(headers.size()+getCount()-dataCount,dataCount);
        ...

    }


    public void addAll(T[] items) {
        ....
        if (mNotifyOnChange) notifyItemRangeInserted(headers.size()+getCount()-dataCount,dataCount);
        ...
    }

    到這裡重新整理的 原理就搞定了,


    下面我介紹載入的原理
     在介紹之前我得說一個載入的時候的bug 之前提到的 bug2
     當你的資料條數沒有填充全屏的時候,就會呼叫一次載入介面,第二次填充的資料沒有填充全屏,繼續呼叫載入的回撥
     這個bug看你的需求,反正對我來說是很噁心的東西,

     載入是完全通過RecyclerArrayAdapter去控制的,所以載入重新整理是兩個相互獨立的東西,這就造成了重新整理的時候是可以載入的,
     載入的時候可以重新整理的時候bug

     你要實現載入功能需要呼叫下面方法
     public void setMore(final int res, final OnLoadMoreListener listener){
        getEventDelegate().setMore(res, new OnMoreListener() {
            @Override
            public void onMoreShow() {
                listener.onLoadMore();
            }

            @Override
            public void onMoreClick() {

            }
        });
    }
    你會發先onLoadMoreListener 傳遞給了EventDelegate 一個代理的類
    我們在看一下getEventDelegate方法
     EventDelegate getEventDelegate(){
        if (mEventDelegate == null)mEventDelegate  = new DefaultEventDelegate(this);
        return mEventDelegate;
    }
    這個代理的類有個預設的實現,也可以自己去拓展實現它的介面,不過你必須按照它規定的原理去實現所以我們去找
    DefaultEventDelegate 的setMore方法

     @Override
    public void setMore(int res, RecyclerArrayAdapter.OnMoreListener listener) {
        this.footer.setMoreViewRes(res);
        this.onMoreListener = listener;
        hasMore = true;
        // 為了處理setMore之前就添加了資料的情況
        if (adapter.getCount()>0){
            addData(adapter.getCount());
        }
        log("setMore");
    }
    你會發先一個方法addData()
     @Override
    public void addData(int length) {
        log("addData" + length);
        if (hasMore){
            if (length == 0){
                //當新增0個時,認為已結束載入到底
                if (status==STATUS_INITIAL || status == STATUS_MORE){
                    footer.showNoMore();
                    status = STATUS_NOMORE;
                }
            }else {
                //當Error或初始時。新增資料,如果有More則還原。
                footer.showMore();
                status = STATUS_MORE;
                hasData = true;
            }
        }else{
            if (hasNoMore){
                footer.showNoMore();
                status = STATUS_NOMORE;
            }
        }
        isLoadingMore = false;
    }
    這個作者是根據每次填充資料的多少來判定載入狀,如果你新增資料是0 則認為你載入完成了不需要去載入資料了
    新增資料個數>0 則認為還有資料就會自動去呼叫footer.showMore 方法
        public void showMore(){
            Log.d("addData", "showMore: count:"+adapter.getItemCount());
            flag = ShowMore;
            if (adapter.getItemCount()>0)
                adapter.notifyItemChanged(adapter.getItemCount()-1);
        }
        在這裡會傳送一個通知出去
        傳送通知後就會呼叫DefaultEventDelegate 類的OnBindView方法 你跟蹤onBindView方法 會發它在
        recyclerarrayAdapter   OnBindView方法裡被呼叫
          public final void onBindViewHolder(BaseViewHolder holder, int position) {
            ...
           if (footers.size()!=0 && i>=0){
               footers.get(i).onBindView(holder.itemView);
               return ;
          }
          ....
        }
        也就是說每次傳送通知會先呼叫 recyclerarrayAdapter 的onBindViewHolder方法,然後在呼叫DefaultEventDelegate
        onBindView方法 通過這個方法去設定狀態 因為載入更多 就會呼叫onMoreViewShowed方法

          @Override
        public void onBindView(View headerView) {
            Log.d("addData", "onBindView: notifyed: flag"+flag);
            headerView.post(new Runnable() {
                @Override
                public void run() {
                    switch (flag){
                        case ShowMore:
                            onMoreViewShowed();//
                            break;
                        case ShowNoMore:
                            if (!skipNoMore)onNoMoreViewShowed();skipNoMore = false;
                            break;
                        case ShowError:
                            if (!skipError) onErrorViewShowed();skipError = false;
                            break;
                    }
                }
            });
        }

         onMoreViewShowed 就會呼叫 onMoreListener.onMoreShow();
         public void onMoreViewShowed() {
            log("onMoreViewShowed");
            if (!isLoadingMore&& onMoreListener !=null){
                isLoadingMore = true;
                onMoreListener.onMoreShow();
            }
        }

        onMoreListener是什麼東西呢?
        RecyclerArrayAdapter.OnMoreListener onMoreListener;
       也就是RecyclerArrayAdapter  類裡面的 public void setMore(int res, RecyclerArrayAdapter.OnMoreListener listener) {


        至此我們來分析一下剛才的bug,每次RecyclerArrayAdapter 呼叫add方法的 時候,
           public void addAll(Collection<? extends T> collection) {
                if (mEventDelegate!=null)mEventDelegate.addData(collection == null ? 0 : collection.size());
                ....

            }
            就會呼叫mEventDelegate.addData方法
            addData方法 根據你新增的個數來判段載入的狀態是0的時候則沒有資料,
            如果不是0的時候 就會呼叫載入更多的回撥方法,footer.showMore
          public void showMore(){
            Log.d("addData", "showMore: count:"+adapter.getItemCount());
            flag = ShowMore;
            if (adapter.getItemCount()>0)
                adapter.notifyItemChanged(adapter.getItemCount()-1);
          }

          這個方法會發送通知,呼叫了OnBindView方法繼而去回撥onLoadMore方法讓你調分頁的介面
          你會不會發先一個問題,第一次重新整理資料data>0 就會調載入的介面,data>0 繼續呼叫介面,只要有資料就會不斷的
          調載入的介面,這個我測試了,不會發生的,載入的資料沒有超出螢幕就會呼叫載入介面,超出屏幕後就不會呼叫了,
          也就是說你每次翻頁的時候,就會自動呼叫載入資料的介面,所以每次呼叫重新整理介面,資料少的話,沒有超出螢幕,就會
          繼續呼叫載入介面,直到資料超出螢幕。


          如果你想實現第一次進去的時候,不要呼叫載入更多,你就改程式碼 addData(int length,boolean isLoading)
          自己手動去控制它什麼時候載入更多,什麼時候不去載入

          還有這個控制元件不能拓展重新整理的介面和載入介面你可以使用superswiperefreshlayout 把swiperefreshlayout
          給換掉哦

          這個控制元件儘管不是很完美,但是畢竟比較好用,所以我寧可去改也不願意去換它。
          只要搞懂它的原理,一切都不是事情。