简介
这个轮子是对RecyclerView的封装,主要完成了下拉刷新
、上拉加载更多
、RecyclerView头部
。在我的Material Design学习项目中使用到了项目地址,感觉还不错。趁着毕业答辩还有2个星期,先把这个轮子拆了看看,这个项目地址在XRecyclerView,先贴个效果图,更多效果图请进入项目中查看。
使用
使用起来也比较简单,首先向普通RecyclerView那样:
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdapter);
下拉刷新和加载更多需要实现其接口即可:
mRecyclerView.setLoadingListener(new XRecyclerView.LoadingListener() {
@Override
public void onRefresh() {
//refresh data here
}
@Override
public void onLoadMore() {
// load more data here
}
});
这里要注意的是需要人为的通知刷新和加载都已经完成,通过如下代码
mRecyclerView.refreshComplete(); //下拉刷新完成
mRecyclerView.loadMoreComplete();//加载更多完成
类关系图
首先梳理了一下框架,用UML图画了这个轮子的结构,这样有利于帮助我理解,右击-查看图像 可以查看清晰大图)
可以看出主要的类只有3个 XRecyclerView
,LoadingMoreFooter
,ArrowRefreshHeader
,而AVLoadingIncatorView
和SimpleViewSwitcher
是用来辅助刷新或者加载时候的动画。
下面分析源码时限于篇幅原因只展现出关键代码,具体可以参考项目源码。
- 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,而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显示,
- 上拉滑动中的下拉刷新和释放后刷新界面的缓慢消失的实现
上拉刷新分为两部分,首先是手指滑动,刷新条慢慢显示出来(而且显示的大小跟滑动距离有关);释放后刷新界面慢慢隐藏,这里刷新的动画部分后面分析。
先看刷新条随着手指滑动慢慢显示
:涉及到滑动需要重写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();
}
}
- 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坐标系
- 熟悉了设计模式的里氏替换、接口隔离、依赖倒置原则