拆解輪子之XRecyclerView

簡介

這個輪子是對RecyclerView的封裝,主要完成了下拉刷新上拉加載更多RecyclerView頭部。在個人Material Design學習項目中使用到了項目地址,感受還不錯。趁着畢業答辯還有2個星期,先把這個輪子拆了看看,這個項目地址在XRecyclerView,先貼個效果圖,更多效果圖請進入項目中查看。 
這裏寫圖片描述
java

使用

使用起來也比較簡單,首先向普通RecyclerView那樣:android

LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity()); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); mRecyclerView.setLayoutManager(layoutManager); mRecyclerView.setAdapter(mAdapter);

下拉刷新和加載更多須要實現其接口便可:git

mRecyclerView.setLoadingListener(new XRecyclerView.LoadingListener() { @Override public void onRefresh() { //refresh data here } @Override public void onLoadMore() { // load more data here } });

這裏要注意的是須要人爲的通知刷新和加載都已經完成,經過以下代碼github

mRecyclerView.refreshComplete(); //下拉刷新完成 mRecyclerView.loadMoreComplete();//加載更多完成

類關係圖

首先梳理了一下框架,用UML圖畫了這個輪子的結構,這樣有利於幫助我理解,右擊-查看圖像 能夠查看清晰大圖)設計模式

這裏寫圖片描述

能夠看出主要的類只有3個 XRecyclerView,LoadingMoreFooter,ArrowRefreshHeader,而AVLoadingIncatorViewSimpleViewSwitcher是用來輔助刷新或者加載時候的動畫。框架

下面分析源碼時限於篇幅緣由只展示出關鍵代碼,具體能夠參考項目源碼。ide

  1. XRecyclerView的實現
 
  • XRecyclerView 的head和footer的view實現 
    XRecyclerView在RecyclerView的基礎上作了進一步的工做於是須要繼承RecyclerView,因爲支持RecyclerView Header而不一樣的header能夠本身實現,所以須要對外暴露,而footerView則是固定的,所以在init初始化時候直接初始化了。此外這裏使用了兩個ArrayList存儲不一樣的view,而且記錄了viewType
private ArrayList<View> mHeaderViews = new ArrayList<>(); private ArrayList<View> mFootViews = new ArrayList<>(); …… private void init() { if (pullRefreshEnabled) { //若支持下拉刷新則加入Headerview列表,設置加載圖標 ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext()); mHeaderViews.add(0, refreshHeader);//從這裏看出headerView能夠添加多個 mRefreshHeader = refreshHeader; mRefreshHeader.setProgressStyle(mRefreshProgressStyle); } //加載更多無需觸發 LoadingMoreFooter footView = new LoadingMoreFooter(getContext()); footView.setProgressStyle(mLoadingMoreProgressStyle); addFootView(footView);//加入footerView mFootViews.get(0).setVisibility(GONE); } …… /** * @param view 對外提供添加header的方法 */ public void addHeaderView(View view) { if (pullRefreshEnabled && !(mHeaderViews.get(0) instanceof ArrowRefreshHeader)) { ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext()); mHeaderViews.add(0, refreshHeader); mRefreshHeader = refreshHeader; mRefreshHeader.setProgressStyle(mRefreshProgressStyle); } mHeaderViews.add(view); sHeaderTypes.add(HEADER_INIT_INDEX + mHeaderViews.size());//記錄viewType }

可是這樣僅僅只是存儲了View,那麼實現的地方在哪裏呢?數據展示很顯然是在dapater中,可是在使用RecycleView時須要展現item數據,那麼header和footer如何加載?這裏就須要對傳入的數據adapter再作一層封裝。函數

@Override public void setAdapter(Adapter adapter) { mWrapAdapter = new WrapAdapter(adapter);//對傳入的adapter作封裝 super.setAdapter(mWrapAdapter); adapter.registerAdapterDataObserver(mDataObserver); mDataObserver.onChanged(); }

因爲RecycleView支持LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager,而GridLayoutManagerStaggeredGridLayoutManager在添加header時候須要注意橫跨整個屏幕寬度即: 
GridLayoutManager 是要設置SpanSize每行的佔位大小 
StaggerLayoutManager 就是要獲取StaggerLayoutManager的LayoutParams 的setFullSpan 方法來設置佔位寬度,所以在WrapAdapter中作了針對性處理
佈局

@Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); if (manager instanceof GridLayoutManager) { final GridLayoutManager gridManager = ((GridLayoutManager) manager); gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return (isHeader(position) || isFooter(position)) ? gridManager.getSpanCount() : 1; } }); } } @Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { super.onViewAttachedToWindow(holder); ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams && (isHeader(holder.getLayoutPosition()) || isFooter(holder.getLayoutPosition()))) { StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp; p.setFullSpan(true); } }

到此只是爲展現head提供了必要條件,具體展現仍是要靠WrapAdapter的 onCreateViewHolder配合getItemViewType方法,根據viewtype從對應的ArrayList中取出view來展現學習

@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == TYPE_REFRESH_HEADER) { mCurrentPosition++; return new SimpleViewHolder(mHeaderViews.get(0)); } else if (isContentHeader(mCurrentPosition)) { if (viewType == sHeaderTypes.get(mCurrentPosition - 1)) { mCurrentPosition++; return new SimpleViewHolder(mHeaderViews.get(headerPosition++)); } } else if (viewType == TYPE_FOOTER) { return new SimpleViewHolder(mFootViews.get(0)); } return adapter.onCreateViewHolder(parent, viewType); } …… @Override public int getItemViewType(int position) { if (isRefreshHeader(position)) { return TYPE_REFRESH_HEADER; } if (isHeader(position)) { position = position - 1; return sHeaderTypes.get(position); } if (isFooter(position)) { return TYPE_FOOTER; } int adjPosition = position - getHeadersCount(); int adapterCount; if (adapter != null) { adapterCount = adapter.getItemCount(); if (adjPosition < adapterCount) { return adapter.getItemViewType(adjPosition); } } return TYPE_NORMAL; }

ok,到這裏就完成了head和footer的view顯示,

  • 上拉滑動中的下拉刷新和釋放後刷新界面的緩慢消失的實現 
    上拉刷新分爲兩部分,首先是手指滑動,刷新條慢慢顯示出來(並且顯示的大小跟滑動距離有關);釋放後刷新界面慢慢隱藏,這裏刷新的動畫部分後面分析。 
    先看刷新條隨着手指滑動慢慢顯示: 
    涉及到滑動須要重寫onTouchEvent,特別是針對MotionEvent.ACTION_MOVE處理
@Override public boolean onTouchEvent(MotionEvent ev) { //經過處理onTouchEvent處理下拉刷新 if (mLastY == -1) { mLastY = ev.getRawY(); } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mLastY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: final float deltaY = ev.getRawY() - mLastY; mLastY = ev.getRawY(); if (isOnTop() && pullRefreshEnabled) { mRefreshHeader.onMove(deltaY / DRAG_RATE);//顯示刷新的關鍵代碼 if (mRefreshHeader.getVisibleHeight() > 0 && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) { // Log.i("getVisibleHeight", "getVisibleHeight = " + mRefreshHeader.getVisibleHeight()); // Log.i("getVisibleHeight", " mRefreshHeader.getState() = " + mRefreshHeader.getState()); return false; } } break; default: mLastY = -1; // reset if (isOnTop() && pullRefreshEnabled) { if (mRefreshHeader.releaseAction()) { if (mLoadingListener != null) { mLoadingListener.onRefresh(); } } } break; } return super.onTouchEvent(ev); }

onMove在ArrowRefreshHeader中實現,這裏多插一句getRawY():獲取點擊事件相對整個屏幕頂邊的y軸座標,即點擊事件距離整個屏幕頂邊的距離注意與getY()區別。

@Override public void onMove(float delta) { //因爲下拉時候區域是動態變化所以須要動態設置 if (getVisibleHeight() > 0 || delta > 0) { setVisibleHeight((int) delta + getVisibleHeight()); if (mState <= STATE_RELEASE_TO_REFRESH) { // 未處於刷新狀態,更新箭頭 if (getVisibleHeight() > mMeasuredHeight) { setState(STATE_RELEASE_TO_REFRESH); } else { setState(STATE_NORMAL); } } } }

在onMove方法輸入參數中能夠看出手指滑動距離的1/3做爲刷新顯示的高度,因爲init方法初始化時將刷新顯示高度設置爲0,一樣在ArrowRefreshHeader中

addView(mContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 0));//初始化時候高度設置爲0,經過後面setVisibleHeight設置可見高度 …… /** * 設置可見高度 * * @param height */ public void setVisibleHeight(int height) { if (height < 0) height = 0; LayoutParams lp = (LayoutParams) mContainer.getLayoutParams(); lp.height = height; mContainer.setLayoutParams(lp); }

這樣就不難理解onMove方法爲什麼能夠在下拉時慢慢出現下拉刷新. 
在看釋放是刷新界面慢慢變爲0 
一樣在在XrecycleView中的onTouch方法中: 
default分支:

default: mLastY = -1; // reset if (isOnTop() && pullRefreshEnabled) { if (mRefreshHeader.releaseAction()) {//上彈關鍵代碼 if (mLoadingListener != null) { mLoadingListener.onRefresh(); } } } break;

mRefreshHeader.releaseAction()中處理了手指釋放後即刷新慢慢向上隱藏的動做,該接口在 
ArrowRefreshHeader中實現

@Override public boolean releaseAction() { //釋放動做,此時須要處理緩慢回到頂部 boolean isOnRefresh = false; int height = getVisibleHeight(); if (height == 0) // not visible. isOnRefresh = false; if (getVisibleHeight() > mMeasuredHeight && mState < STATE_REFRESHING) { setState(STATE_REFRESHING); isOnRefresh = true; } // refreshing and header isn't shown fully. do nothing. if (mState == STATE_REFRESHING && height <= mMeasuredHeight) { //return; } int destHeight = 0; // default: scroll back to dismiss header. // is refreshing, just scroll back to show all the header. if (mState == STATE_REFRESHING) { destHeight = mMeasuredHeight; } smoothScrollTo(destHeight); return isOnRefresh; } …… private void smoothScrollTo(int destHeight) { ValueAnimator animator = ValueAnimator.ofInt(getVisibleHeight(), destHeight); animator.setDuration(300).start(); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { setVisibleHeight((int) animation.getAnimatedValue()); } }); animator.start(); }

其中主要是經過smoothScrollTo的屬性動畫+setVisibleHeight函數來實現刷新部分慢慢隱藏

  • 加載更多實現 
    一般實現該功能是在手指滑動中止後進行加載,在XRecyclerView中重寫了onScrollStateChange方法,加載更多主要是須要得到最後可見的位置即lastVisibleItem,以下所示
@Override public void onScrollStateChanged(int state) { super.onScrollStateChanged(state); //重寫該方法主要是在IDLE態即手指滑動中止後處理加載更多 if (state == RecyclerView.SCROLL_STATE_IDLE && mLoadingListener != null && !isLoadingData && loadingMoreEnabled) { LayoutManager layoutManager = getLayoutManager(); int lastVisibleItemPosition; if (layoutManager instanceof GridLayoutManager) { lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { //瀑布流佈局發現最後可見的item位置 int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()]; ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into); lastVisibleItemPosition = findMax(into); } else { lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); } if (layoutManager.getChildCount() > 0 && lastVisibleItemPosition >= layoutManager.getItemCount() - 1 && layoutManager.getItemCount() > layoutManager.getChildCount() && !isNoMore && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) { View footView = mFootViews.get(0); isLoadingData = true; if (footView instanceof LoadingMoreFooter) { ((LoadingMoreFooter) footView).setState(LoadingMoreFooter.STATE_LOADING); } else { footView.setVisibility(View.VISIBLE); } mLoadingListener.onLoadMore(); } } }
  • 空白數據處理 
    數據變化時處理佈局,這裏主要經過AapterDataObserver監聽數據變化以此來更換佈局
@Override public void onChanged() { //重寫該方法是在數據發生變化時更換佈局 Adapter<?> adapter = getAdapter(); if (adapter != null && mEmptyView != null) { int emptyCount = 0; if (pullRefreshEnabled) { emptyCount++; } if (loadingMoreEnabled) { emptyCount++; } if (adapter.getItemCount() == emptyCount) { mEmptyView.setVisibility(View.VISIBLE); XRecyclerView.this.setVisibility(View.GONE); } else { mEmptyView.setVisibility(View.GONE); XRecyclerView.this.setVisibility(View.VISIBLE); } } if (mWrapAdapter != null) { mWrapAdapter.notifyDataSetChanged(); } }
  1. ArrowRefreshHeader與LoadingMoreFooter 
    這倆個都是繼承viewgroup的自定義控件,前者要比後者稍微複雜一些,先揀軟柿子捏,看看LoadingMoreFooter: 
    主要功能就是初始化好加載更多的動畫view和家在文字,而後經過state通通暴露在setState函數中供外界調用
public void setState(int state) { switch(state) { case STATE_LOADING: progressCon.setVisibility(View.VISIBLE); mText.setText(getContext().getText(R.string.listview_loading)); this.setVisibility(View.VISIBLE); break; case STATE_COMPLETE: mText.setText(getContext().getText(R.string.listview_loading)); this.setVisibility(View.GONE); break; case STATE_NOMORE: mText.setText(getContext().getText(R.string.nomore_loading)); progressCon.setVisibility(View.GONE); this.setVisibility(View.VISIBLE); break; } }

能夠看出,經過不一樣的狀態來處理文字和加載動畫。

在看ArrowRefreshHeader,稍微複雜點,主要是要處理隨着手指滑動刷新界面慢慢顯示和釋放釋放手指刷新界面慢慢返回,刷新完成後的狀態重置,這些都實現接口BaseRefreshHeader處理。其中該自定義控件在初始化時候將高度設置爲0,經過setVisibleHeight來設置高度,這樣就能夠處理刷新高度的動態變化,在介紹XRecyclerView中已經對這幾個接口方法作了詳細介紹了,這裏就不贅述了。這裏處理方式與LoadingMoreFooter相同,根據不一樣刷新狀態來處理控件的顯示狀態。 
抽象來看,這兩個控件的核心就是使用 SimpleViewSwitcher作中轉將AVLoadingIndicatorView不一樣的加載動畫呈現的過稱。 
其中 SimpleViewSwitcher比較簡單就是一個設置view的很普通的自定義viewgroup,而AVLoadingIndicatorView則是另外一個加載動畫庫了github項目地址此次就不分析了。 
到此基本上這個輪子就大體分析完了。

尾聲

做爲一個Android彩筆,仍是應該多讀讀源碼,包括android源碼和github上的一些多星的優秀項目的源碼,經過拆這個輪子,能夠收穫到:

  • 熟悉uml拆分框架
  • recycleView針對不一樣佈局(如StaggeredGridLayoutManager)獲取findLastVisibleItemPositions和header的處理方式
  • 下拉刷新手指滑動距離與刷新高度變化、釋放後刷新頭部自動消失(onTouch)
  • 改變不一樣不通佈局的方式,根據狀態設置empytyview可見仍是 recycleView可見與否
  • recycleView增長頭部底部後使用對傳入的數據adapter來進行二次封裝
  • 自定義viewgroup的使用
  • 屬性動畫的簡單使用
  • view座標系
  • 熟悉了設計模式的里氏替換、接口隔離、依賴倒置原則
轉自:http://blog.csdn.net/xsf50717/article/details/51366922
版權聲明:本文內容由互聯網用戶自發貢獻,版權歸做者全部,本社區不擁有全部權,也不承擔相關法律責任。若是您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至:yqgroup@service.aliyun.com 進行舉報,並提供相關證據,一經查實,本社區將馬上刪除涉嫌侵權內容。
相關文章
相關標籤/搜索