業餘時間:RecyclerView的封裝

前言

上一篇已經實現了頭部和尾部的載入標識,接下來只需要將它們與RecyclerView組合封裝就OK了,不得不說自己要去封裝一個好用的重新整理載入控制元件還是得費好多心思去實現和優化的。
PS:更好的封裝方式,並已提交到JCenter上:http://blog.csdn.net/hzwailll/article/details/75285924

Begin

重新整理頭部比較麻煩點,當然也是重點,需要處理touch和控制元件高度,這裡我的思路是:

1. 當頭部處於重新整理狀態時不在接收任何觸控事件

2. 當重新整理頭部處於當前列表在螢幕上的第一個Item並且有下拉手勢時開始響應下拉事件,當然這也是必須的

3. 重新整理頭部內部只處理觸控事件,是否處於頭部在RecyclerView中判斷

4. 通過手指在螢幕Y方向的距離計算重新整理頭部的高度,通過重新整理頭部的高度計算時鐘的角度

重新整理頭部的觸控事件處理如下:

    protected void touch(MotionEvent event, int appbarState) {
//如果當前正在重新整理,不接收任何的觸控事件
if (currentState == STATE_REFRESH) return;
if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (isFirstMove) {
lastY = event.getRawY();
isFirstMove = false;
}
float delaY = (event.getRawY() - lastY) / 3;
if (delaY > 0 || getCurrentHeight() > 0) {
currentHeight = (int) (delaY   getCurrentHeight());
clockView.setClockAngle(currentHeight);//設定時鐘的角度
currentState = STATE_PREPARE;//當前的重新整理狀態為準備狀態
if (delaY > 0 && appbarState == XRecyclerView.APP_BAR_NORMAL) {//下拉
changeHeight(currentHeight);
} else if (delaY < 0) {//上滑
layout(0, currentHeight, width, currentHeight);
changeHeight(currentHeight);
}
changeRefreshState(true);
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
isFirstMove = true;
changeRefreshState(false);
}
lastY = event.getRawY();
}
private void changeHeight(int height) {
LayoutParams params = (LayoutParams) refreshView.getLayoutParams();
params.height = height > 0 ? height : 0;
params.width = width;
refreshView.setLayoutParams(params);
}

這裡的touch事件是從RecyclerView中傳遞過來的,同時傳遞過來的還有AppbarLayout的狀態,這個稍後再說。這裡通過float delaY = (event.getRawY() – lastY) / 3計算在Y方向的移動距離,除以3以減小移動距離。重新整理狀態共有三種,分別為正常狀態,準備狀態,也就是重新整理頭部響應觸控事件且手指還在螢幕上的狀態,還有正在重新整理的狀態。當手指離開螢幕時高度的漸變,通過屬性動畫來實現,並根據起始與結束值在動畫結束時更改重新整理頭部處於的對應狀態

    private void changeHeightAnim(final int start, final int end) {
if (animator == null || !animator.isRunning()) {
animator = ValueAnimator.ofInt(start, end);
animator.setDuration(300).setInterpolator(new DecelerateInterpolator(1.2f));
animator.start();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
changeHeight(value);
if (start == currentHeight && end == initHeight) {
clockView.setClockAngle(value);
}
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (end == (int) refreshHeight) {
currentState = STATE_REFRESH;
} else if (end == initHeight) {
currentState = STATE_NORMAL;
clockView.stopClockAnim();
}
}
});
}
}

為了效果好看點,這裡設定了一個減速的插值器DecelerateInterpolator,並設定因子為1.2,這個通過http://inloop.github.io/interpolator/這個網站可以很方便的檢視不同的插值器對應的曲線,強烈推薦。

接下來就是RecyclerView的封裝,這裡取一個高逼格的名字就叫XRecyclerView,當然此XRecyclerView非彼XRecyclerView,網上的XRecyclerView(https://github.com/jianghejie/XRecyclerView)是github上開源的封裝RecyclerView實現的重新整理載入控制元件,功能齊全,很不錯!

這裡重新整理頭部和載入尾部在RecyclerView的新增有兩種方式,一種是採用裝飾者模式,在RecyclerView內部構建一個介面卡並新增頭部尾部,另外一種嘛就是封裝一個可新增頭部和尾部的介面卡,在RecyclerView內部強轉,然後新增頭部和尾部,但耦合性高。歸根到底,實際上就是一種方式:即通過介面卡新增頭部尾部。這裡使用的是第二種,因為第一種新增方式在實際使用過程中發現發現很不容擴充套件,與封裝的介面卡存在下標衝突,所以當使用裝飾著模式新增頭部尾部時,我只能老老實實的寫RecyclerView的原生的介面卡,而不能用自己封裝的,這個是我忍受不了了的,畢竟會多出很多程式碼。兩者共用的方式我目前沒有好的解決方法,只能用這種笨方法了。

初始化重新整理頭部和載入尾部:

    private void initRefresh() {
isRefreshEnable = true;
if (refreshHeader == null) {
refreshHeader = new RefreshHeader(getContext(), refreshHeight);
}
refreshHeader.setRefreshListener(new RefreshHeader.RefreshListener() {
@Override
public void refresh() {
loadingListener.refresh();
}
});
}
private void initLoading() {
isLoadMoreEnable = true;
if (loadingFooter == null) {
loadingFooter = new LoadingFooter(getContext(), loadingHeight);
}
loadingFooter.setVisibility(GONE);
LayoutManager manager = getLayoutManager();
if (manager instanceof LinearLayoutManager) {
//只支援LinearLayoutManager和GridLayoutManager佈局,不支援StaggeredGridLayoutManager
final LinearLayoutManager layoutManager = (LinearLayoutManager) manager;
addOnScrollListener(new OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
judgeLastItem(layoutManager.findLastVisibleItemPosition(), dy);
}
});
}
}

通過重寫setAdapter方法,新增頭部和尾部,並註冊資料變化的觀察者,通過AdapterDataObserver可以監聽RecyclerView的資料變化,從而設定對應的空值介面的顯示與隱藏:

    private AdapterDataObserver mObserver = new AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
checkEmpty();
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
checkEmpty();
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
super.onItemRangeRemoved(positionStart, itemCount);
checkEmpty();
}
};
    @Override
public void setAdapter(Adapter adapter) {
if (this.getAdapter() != null) {
this.getAdapter().unregisterAdapterDataObserver(mObserver);
}
super.setAdapter(adapter);
if (refreshHeader == null || loadingFooter == null) {
refreshHeader = new RefreshHeader(getContext(), refreshHeight);
loadingFooter = new LoadingFooter(getContext(), loadingHeight);
}
((BaseRVAdapter) getAdapter()).addHeaderView(refreshHeader);
((BaseRVAdapter) getAdapter()).addFooterView(loadingFooter);
adapter.registerAdapterDataObserver(mObserver);
}

繼續:

接下來處理touch事件,但是需要注意的是,當AppbarLayout作為RecyclerView父View與CoordinatorLayout協同處理滑動時會有手勢衝突,可以通過監聽AppbarLayout的滑動來處理,思路很簡單:找到AppbarLayout,設定偏移監聽器:

    @Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (appBarLayout == null) {
ViewParent parent = getParent();
while (parent != null) {
if (parent instanceof CoordinatorLayout) break;
parent = parent.getParent();
}
if (parent != null) {
CoordinatorLayout layout = (CoordinatorLayout) parent;
appBarLayout = null;
for (int i = 0; i < layout.getChildCount(); i  ) {
View child = layout.getChildAt(i);
if (child instanceof AppBarLayout) {
appBarLayout = (AppBarLayout) child;
break;
}
}
if (appBarLayout != null) {
appBarLayout.addOnOffsetChangedListener(this);
}
}
}
}
    @Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
appBarState = verticalOffset == 0 ? APP_BAR_NORMAL : APP_BAR_UP;
}

最後一步:處理RecyclerView的onTouchEvent事件:

    @Override
public boolean onTouchEvent(MotionEvent e) {
if (isRefreshEnable && isTop()) {//處理重新整理頭部
refreshHeader.touch(e, appBarState);
}
return super.onTouchEvent(e);
}

還有一些無關緊要的方法,就不貼程式碼了,整體來說難度不大,但還是要花費不少心思去琢磨細節和優化程式碼,重新整理頭部的觸控事件前前後後寫了好幾個版本,有的版本在使用的時候才發現,從最開始的思路就是錯的,有的版本覺得重新整理頭部和RecyclerView的耦合性太高了,幾乎把重新整理頭部的重新整理處理全寫在了RecyclerView裡,不利於重新整理頭部的更換和擴充套件,當前版本算是比較滿意的一個版本,當然可能也有很多潛在問題,後續會一直改進,說不定哪一天就用在真實專案上了,哈哈!

End

在封裝的實現過程中遇到了很多不明白的地方,在很多方法中尋求最佳的解決方案,以更少的程式碼實現更好的功能,這是我的追求!雖然達不到我師父那種每天除了吃飯睡覺都是在寫程式碼的狀態,但是也不能差太多!

最後附上使用裝飾者模式實現新增頭和尾部的程式碼:

private class WrapperAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int HEADER_TYPE = 100;
private static final int FOOTER_TYPE = 101;
private Adapter adapter;
WrapperAdapter(Adapter adapter) {
this.adapter = adapter;
}
private boolean isHeader(int position) {
return position < getHeaderCount();
}
private boolean isFooter(int position) {
return position >= (getDataCount()   getHeaderCount());
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == HEADER_TYPE) {
return com.hzw.freetime.adapter.ViewHolder.getViewHolder(refreshHeader);
} else if (viewType == FOOTER_TYPE) {
return com.hzw.freetime.adapter.ViewHolder.getViewHolder(loadingFooter);
}
return adapter.onCreateViewHolder(parent, viewType);
}
@SuppressWarnings("unchecked")
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (isHeader(position) || isFooter(position)) return;
adapter.onBindViewHolder(holder, position - getHeaderCount());
}
@Override
public int getItemViewType(int position) {
if (isHeader(position)) {
return HEADER_TYPE;
} else if (isFooter(position)) {
return FOOTER_TYPE;
}
return adapter.getItemViewType(position - getHeaderCount());
}
@Override
public int getItemCount() {
return getDataCount()   getHeaderCount()   getFooterCount();
}
private int getHeaderCount() {
return refreshHeader == null ? 0 : 1;
}
private int getFooterCount() {
return loadingFooter == null ? 0 : 1;
}
private int getDataCount() {
return adapter.getItemCount();
}
}

    @Override
public void setAdapter(Adapter adapter) {
if (this.getAdapter() != null) {
this.getAdapter().unregisterAdapterDataObserver(mObserver);
}
if (refreshHeader == null || loadingFooter == null) {
refreshHeader = new RefreshHeader(getContext(), refreshHeight);
loadingFooter = new LoadingFooter(getContext(), loadingHeight);
}
WrapperAdapter adapter1 = new WrapperAdapter(adapter);
super.setAdapter(adapter1);
adapter.registerAdapterDataObserver(mObserver);
}

OVER