RecyclerView 水滴刷新動畫 加載更多

項目使用RecyclerView代替ListView,爲了方便開發封裝了適配器並給RecyclerView增加了常用方法。這裏感謝XRecyclerView的作者,給了我很大幫助。

水滴刷新效果

對XRecyclerView進行的修改

在實際使用過程中,和開發需求有點差別。進行下面的修改操作

  • 修改:在禁止刷新、加載更多等情況下,空視圖展示的判定有失誤
  • 修改:當沒有更多數據是上滑動不加載數據
  • 修改:沒有新數據時加入時顯示“無更多”的FootView
  • 修改:改變手動調用加載完成、刷新完成的(需要Adapter的配合)
  • 修改:在最後條目可見的情況下,短距離下拉控件也能觸發加載更多行爲,修改後不觸發加載更多行爲

RecyclerView 通用Adapter的封裝

在使用RecyclerView等控件的時候,肯定寫個抽象適配器用來簡化代碼、減少工作量。基本按照需求改變了XRecyclerView和加入適配器後,那麼使用起來估摸着肯定會出現問題,發現在添加頭部的情況下,增添數據時,Item視圖加入RecyclerView的位置有問題。

看下適配器中添加數據的方法:

public void addDatas(List<T> datas) {
        if (datas == null) datas = new ArrayList<>();
        this.mDataSource.addAll(datas);
        this.notifyItemRangeInserted(this.mDataSource.size() - datas.size(), datas.size());
    }

在以前使用時這個添加數據的方法是沒問題的,數據能加到準確的位置上去。但現在配合使用之後
發現數據加入的位置確實有問題,在添加頭部View的時候添加動畫位置是錯的。

我們看下代碼 XRecyclerView中的setAdapter

@Override
    public void setAdapter(Adapter adapter) {
        mWrapAdapter = new WrapAdapter(adapter);
        super.setAdapter(mWrapAdapter);
        mWrapAdapter.registerAdapterDataObserver(mDataObserver);
        if (adapter.getItemCount() != 0)
            mDataObserver.onChanged();
    }

知道了有問題,不用着急,也不用擔心。第一件事是多點點,多看看,然後你就會發現,後面會有更多的問題。簡單的調試和修改過後,果不其然。發現每一次數據變化會進入AdapterDataObservable的同一方法兩次。第一次是有用的,第二次是用來慶祝第一次有用的。

經過查看, XRecyclerView在封裝進入頭部、腳部等View的時候,採用了進一步包裝Adapter。所以並不是直接使用setAdapter方法傳入的適配器對象。那數據變化時我依然在封裝的Adapter裏面調用notify方法,這不是很靠譜。在RecyclerViewsetAdapter方法裏面有一句代碼:

        adapter.registerAdapterDataObserver(mDataObserver);

傳入的Adapter對象註冊了個適配器數據觀察者,在對象調用notify方法的時候這個觀察者會被觸發並回調執行方法,而在這個回調的方法裏面包裝後的WrapAdapter又去notify了,又去notify了,又去notify了。問題是去了也糾正不了第一次的錯誤。

到這裏可以找到解決思路了,就是第一次不去notify,找方法讓WrapAdapter去notify,或者反過來。第一種去做了發現數據變更了,但是如果是在列表中加入數據沒有動畫效果,看了源碼後發是AdapterDataObservable沒寫好。最後採取了反過來的方法去實現,只要解決數據索引就行了,原因後面講。

跟着源碼看下notify的打開方式,這裏選擇notifyItemRemoved(int)方法作爲突破口,其他的方法類似。下文用notify(…)指代方法 notifyItemRemoved(int)

進如notify(…)看代碼

    public static abstract class Adapter<VH extends ViewHolder> {
    ...

        public final void notifyItemRemoved(int position) {
            mObservable.notifyItemRangeRemoved(position, 1);
        }

    ...
    }

激活動作是由AdapterDataObservable對象進行的,繼續進入查看

    static class AdapterDataObservable extends Observable<AdapterDataObserver> {
        ...
        public void notifyItemRangeRemoved(int positionStart, int itemCount) {
                    for (int i = mObservers.size() - 1; i >= 0; i--) {
                        mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
                    }
        }
        ....
    }

這個操作會共享給所有的AdapterDataObserver對象去回調數據發生的改變,那麼我們去看看RecyclerView默認給我門添加的那個AdapterDataObserver是怎麼實現的。在和我們自己實現的對比,就可以找出問題並實現了。

    private class RecyclerViewDataObserver extends AdapterDataObserver {
        ...
        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
                triggerUpdateProcessor();
            }
        }
        ...
    }

其他的不看,就看這個改變的。然後就看了triggerUpdateProcessor()方法,如此耀眼。開啓數據變化時的Item加入或消失的動畫效果。如果不做特效的話,是沒有必要自己去實現添加動畫的,所以採取了第二種解決方案,這樣子也增加了控件和適配器之間的耦合度。

triggerUpdateProcessor() 方法的代碼:

        void triggerUpdateProcessor() {
            if (mPostUpdatesOnAnimation && mHasFixedSize && mIsAttached) {
                ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
            } else {
                mAdapterUpdateDuringMeasure = true;
                requestLayout();
            }
        }

然後自己去點吧,這不是本文重點。最後我還是準備在傳入的Adapter裏面調用激活方法,但是先進行數據變化是索引的計算。所以進行如下修改:

    ...

    private XRecyclerView.WrapAdapterDataManager dataManager;

    public void setDataManager(XRecyclerView.WrapAdapterDataManager dataManager) {
        this.dataManager = dataManager;
    }

    public void setDatas(List<T> datas) {
        if (datas == null) datas = new ArrayList<>();
        this.mDataSource.clear();
        this.mDataSource.addAll(datas);
        if (dataManager == null) {
            this.notifyDataSetChanged();
        } else {
            dataManager.notifyDataSetChanged1();
        }
    }

    public void addDatas(List<T> datas) {
        if (datas == null) datas = new ArrayList<>();
        this.mDataSource.addAll(datas);
        if (dataManager == null) {
            this.notifyItemRangeInserted(this.mDataSource.size() - datas.size() + 1, datas.size());
        } else {
            dataManager.notifyItemRangeInserted1(this.mDataSource.size() - datas.size() + 1, datas.size());
        }
    }
    ...

有一個XRecyclerView裏面的接口對象,當它不爲null的時候我們就調用他的方法取代Adapter的激活方法,當它爲null的時候就調用Adapter的激活方法。那他什麼時候賦值呢?

    public class WrapAdapter extends Adapter<ViewHolder> implements WrapAdapterDataManager {
        ...
        public WrapAdapter(Adapter adapter) {
            this.adapter = adapter;
            if (this.adapter instanceof CommomRecyclerAdapter) {
                ((CommomRecyclerAdapter) this.adapter).setDataManager(this);
            }
        }
        ...
          @Override
        public void notifyItemRangeInserted1(int positionStart, int itemCount) {
            //這裏進行激活條目的計算,然後在用傳入的Adapter對象激活數據
            adapter.notifyItemRangeInserted(positionStart, itemCount);
        }
        ...
    }

這個是封裝適配器WrapAdapter的構造方法。檢測如果傳入的Adapter是我們封裝的CommomRecyclerAdapter的時候,給那個對象賦值。並且在WrapAdapterDataManager抽象方法的實現裏面直接利用傳入的Adapter對象去調用激活,當然之前的計算好數據變化的索引位置。這樣也可以不用去註冊自己實現的AdapterDataObserver了,也不會因爲一次數據變化多次調用數據觀察者同一方法。
這種改動無疑增加了適配器和RecyclerView之間的耦合度,經過這次對XRecyclerView的使用和改造,我已經有了想法,不增加耦合度的情況下達到目的,下次去定義RecyclerView控件時實現。

水滴刷新動畫的繪製

詳細思路點擊

水滴動畫的設計

部分代碼

@Override
    protected void onDraw(Canvas canvas) {
        makeBezierPath();
        //畫頂部
        mPaint.setColor(Color.parseColor("#2abb9c"));
        canvas.drawPath(mPath, mPaint);
        canvas.drawCircle(topCircle.getX(), topCircle.getY(), topCircle.getRadius(), mPaint);
        //畫底部
        mPaint.setColor(Color.parseColor("#2abb9c"));
        canvas.drawCircle(bottomCircle.getX(), bottomCircle.getY(), bottomCircle.getRadius(), mPaint);
        RectF bitmapArea = new RectF(
                topCircle.getX() - 0.5f * topCircle.getRadius(),
                topCircle.getY() - 0.5f * topCircle.getRadius(),
                topCircle.getX() + 0.5f * topCircle.getRadius(),
                topCircle.getY() + 0.5f * topCircle.getRadius());
        canvas.drawBitmap(arrowBitmap, null, bitmapArea, mPaint);
        super.onDraw(canvas);
    }


    private void makeBezierPath() {
        mPath.reset();
        //獲取兩圓的兩個切線形成的四個切點
        double angle = getAngle();
        float top_x1 = (float) (topCircle.getX() - topCircle.getRadius() * Math.cos(angle));
        float top_y1 = (float) (topCircle.getY() + topCircle.getRadius() * Math.sin(angle));

        float top_x2 = (float) (topCircle.getX() + topCircle.getRadius() * Math.cos(angle));
        float top_y2 = top_y1;

        float bottom_x1 = (float) (bottomCircle.getX() - bottomCircle.getRadius() * Math.cos(angle));
        float bottom_y1 = (float) (bottomCircle.getY() + bottomCircle.getRadius() * Math.sin(angle));

        float bottom_x2 = (float) (bottomCircle.getX() + bottomCircle.getRadius() * Math.cos(angle));
        float bottom_y2 = bottom_y1;

        mPath.moveTo(topCircle.getX(), topCircle.getY());

        mPath.lineTo(top_x1, top_y1);

        mPath.quadTo((bottomCircle.getX() - bottomCircle.getRadius()),
                (bottomCircle.getY() + topCircle.getY()) / 2,
                bottom_x1,
                bottom_y1);
        mPath.lineTo(bottom_x2, bottom_y2);

        mPath.quadTo((bottomCircle.getX() + bottomCircle.getRadius()),
                (bottomCircle.getY() + top_y2) / 2,
                top_x2,
                top_y2);
        mPath.close();
    }

項目源碼下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章