這個輪子是對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
,而AVLoadingIncatorView
和SimpleViewSwitcher
是用來輔助刷新或者加載時候的動畫。框架
下面分析源碼時限於篇幅緣由只展示出關鍵代碼,具體能夠參考項目源碼。ide
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,而GridLayoutManager
、StaggeredGridLayoutManager
在添加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顯示,
先看刷新條隨着手指滑動慢慢顯示
: @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函數來實現刷新部分慢慢隱藏
@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(); } } }
@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(); } }
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上的一些多星的優秀項目的源碼,經過拆這個輪子,能夠收穫到: