这个轮子是对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上的一些多星的优秀项目的源码,经过拆这个轮子,能够收获到: