拆解轮子之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 进行举报,并提供相关证据,一经查实,本社区将马上删除涉嫌侵权内容。
相关文章
相关标签/搜索