項目使用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方法,這不是很靠譜。在RecyclerView的setAdapter方法裏面有一句代碼:
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();
}