Android PullToRefresh 分析之四、扩展RecyclerView

前言:

    接着上一篇《Android PullToRefresh 分析之三、响应手势事件,这一篇主要分析如何扩展PullToRefreshBase,来创建各式各样的刷新加载内容区域。

一、 回顾

   我们在第二篇《 PullToRefresh 分析之二、UI结构 》中提出了四个问题,只是简单粗暴 的说了怎么解决,没有去看源码,下面先把这四个问题再拿出来:
  1. 刷新加载的方向是怎样的,通常的是竖向,万一奇葩的需求提出横向呢?
  2. "内容区域"应该显示什么?怎么设置才好扩展?
  3. 怎么判断到顶部了,要触发刷新动作?
  4. 怎么判断到底部了,要触发加载操作?

二、 源码分析

还是以简单的ScrollView为例进行分析,为毛我们老是欺负PullToRefreshScrollView,不急分析完ScrollView我们再分析PullToRefreshListView。翠花,上代码!


(一)、PullToRefreshScrollView 分析

    首先看下都有哪些方法:



 除了构造方法之外,只有四个方法,即getPullToRefreshScrollDirection()、createRefreshableView()、isReadyForPullStart()、isReadyForPullEnd()。这四个方法通过名字就可以看出来是对应的我们提出的四个问题,那么一一来分析下:

1. 刷新方向是怎样的?

    对于这个问题大家肯定知道那就是竖向的,代码里面也比较简单:

@Override
public final Orientation getPullToRefreshScrollDirection() {
	return Orientation.VERTICAL;
}

2. "内容区域"显示什么?

    因为是PullToRefreshScrollView,这里肯定是生成一个ScrollView对象:

@Override
protected ScrollView createRefreshableView(Context context, AttributeSet attrs) {
	ScrollView scrollView;
	if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
		scrollView = new InternalScrollViewSDK9(context, attrs);
	} else {
		scrollView = new ScrollView(context, attrs);
	}

	scrollView.setId(R.id.scrollview);
	return scrollView;
}

可以看到如果当前系统版本大于9创建的是InternalScrollVIewSDK9,其他情况是创建的系统的ScrollView,那么这个InternalScrollView是什么呢,来看下:

@TargetApi(9)
final class InternalScrollViewSDK9 extends ScrollView {

	public InternalScrollViewSDK9(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	@Override
	protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
			 int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {

	final boolean returnValue = super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
	scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);

	// Does all of the hard work...
	OverscrollHelper.overScrollBy(PullToRefreshScrollView.this, deltaX, scrollX, deltaY, scrollY,
	getScrollRange(), isTouchEvent);

		return returnValue;
	}

	/**
	* Taken from the AOSP ScrollView source
	*/
	private int getScrollRange() {
	int scrollRange = 0;
		if (getChildCount() > 0) {
			View child = getChildAt(0);
			scrollRange = Math.max(0, child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
	}
	return scrollRange;
	}
}

可以看到该类还是继承的系统的ScrollView,只不过是覆写了两个方法overScrollBy()和getScrillRange()。那么为什么要覆写这两个方法呢?overScrollBy()是android2.3提供的一个方法,用来实现View的过度滚动,即滚动到头部之后还可以继续滚动。默认是没有这个效果的,这里就不去关系它了。
        
3.怎么判断到顶部了,要触发刷新动作?

    判断到达了顶部,这个时候就可以触发一系列的刷新加载操作了,由于ScrollView只能包含一个子View,所以只要判断子View的头部完全显示出来就可以了。

@Override
protected boolean isReadyForPullStart() {
	return mRefreshableView.getScrollY() == 0;
}

只是简单的一行代码,那么这个getScrollY()是什么意思呢?google给我们的注释是 
Return the scrolled top position of this view. 大致意思就是获取ScrollView内的子View已经向上滚动的距离,那么如果滚动的距离为0的话就是子View的头部就完全显示啦。

4.怎么判断到底部了,要触发加载操作?

    判断到达了底部的作用其实和判断到达顶部的作用是一样的,在ScrollView中我们只需判断子View已经滚动到了底部就可以:

@Override
protected boolean isReadyForPullEnd() {
	View scrollViewChild = mRefreshableView.getChildAt(0);
	if (null != scrollViewChild) {
		return mRefreshableView.getScrollY() >= (scrollViewChild.getHeight() - getHeight());
	}
	return false;
}

通过代码可以看出,就是已经向上滚动的距离 + ScrollView的高度 = 子View的高度 就可以断定已经到了底部。

(二)、PullToRefreshListView 分析

      首先看下类的继承关系:



PullToRefreshScrollView直接继承PullToRefreshBase不同的是多了一个中间的PullToRefreshAdapterViewBase,那么按照辈分PullToRefreshScrollView应该是PullToRefreshListView 的叔叔或者大伯。那么还是去找我们关系的那四个方法:



 很不幸,我们只是找到了两个getPullToRefershScrillDirection()和createRefreshableView(),那么先看下这两个方法:

 1. 刷新方向是怎样的?

@Override
public final Orientation getPullToRefreshScrollDirection() {
	return Orientation.VERTICAL;
}

这里也是返回了竖向。

2. "内容区域"显示什么?

@Override
protected ListView createRefreshableView(Context context, AttributeSet attrs) {
	ListView lv = createListView(context, attrs);

	// Set it to this so it can be used in ListActivity/ListFragment
	lv.setId(android.R.id.list);
	return lv;
}

这里也和我们预期的一样,返回了一个ListView对象。

3.怎么判断到顶部了,要触发刷新动作?


   我们要找的 是isReadyForPullStart(),但是在PullToRefreshListView中没有发现它的踪迹,那就到他的父类中去看看,果然在PullToRefreshAdapterViewBase找到了它的身影:


@Override
protected boolean isReadyForPullStart() {
	return isFirstItemVisible();
}

调用了一个isFirstItemVisible()方法,也比较好理解,判断是否可以触发刷新动作的依据就是判断ListView的第一个条目是否完全可见:

private boolean isFirstItemVisible() {
	final Adapter adapter = mRefreshableView.getAdapter();
	......
	if (mRefreshableView.getFirstVisiblePosition() <= 1) {
		final View firstVisibleChild = mRefreshableView.getChildAt(0);
		if (firstVisibleChild != null) {
		return firstVisibleChild.getTop() >= mRefreshableView.getTop();
	}

	return false;
}

可以看出,这里获取ListView的第一个条目,然后判断该条目是否完全可见。

4.怎么判断到底部了,要触发加载操作?

这里和isReadyForPullStart()类似,isReadyForPullEnd()也是在PullToRefreshAdapterViewBase中:

protected boolean isReadyForPullEnd() {
	return isLastItemVisible();
}

相同的它也调用了一个 isLastItemVisible()的方法:

private boolean isLastItemVisible() {
	final Adapter adapter = mRefreshableView.getAdapter();
	......
	final int lastItemPosition = mRefreshableView.getCount() - 1;
	   final int lastVisiblePosition = mRefreshableView.getLastVisiblePosition();

	if (lastVisiblePosition >= lastItemPosition - 1) {
		final int childIndex = lastVisiblePosition - mRefreshableView.getFirstVisiblePosition();
			final View lastVisibleChild = mRefreshableView.getChildAt(childIndex);
			if (lastVisibleChild != null) {
				return lastVisibleChild.getBottom() <= mRefreshableView.getBottom();
		}
	}

	return false;
}

这里通过判断,可见的最后一个条目是否是最后一个并且是否完全可见来判定到达了底部。

三、 扩展RecyclerView


    通过以上分析,我们可以知道如果想自己扩展的话只需要继承PullToRefreshBase并且根据扩展View的属性来实现这四个方法就可以了,是不是还算比较简单。

1. 继承PullToRefreshBase:


   
2. 我们发现有错误,那么再实现抽象的方法:



3. 还有错误,原来是缺少构造函数:

public class PullToRefreshRecyclerView extends PullToRefreshBase{

	public PullToRefreshRecyclerView(Context context) {
		super(context);
	}

	public PullToRefreshRecyclerView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public PullToRefreshRecyclerView(Context context, Mode mode) {
		super(context, mode);
	}

	public PullToRefreshRecyclerView(Context context, Mode mode, AnimationStyle animStyle) {
		super(context, mode, animStyle);
	}

	@Override
	public Orientation getPullToRefreshScrollDirection() {
		return null;
	}

	@Override
	protected RecyclerView createRefreshableView(Context context, AttributeSet attrs) {
		return null;
	}

	@Override
	protected boolean isReadyForPullEnd() {
		return false;
	}

	@Override
	protected boolean isReadyForPullStart() {
		return false;
	}
}

OK,至此为止,继承一个类,修复下错误,5秒钟就搞定了架子,然后往里面填肉。

4. 设定刷新加载方向

@Override
public Orientation getPullToRefreshScrollDirection() {
	return Orientation.VERTICAL;
}

由于我们想要的效果是竖向的刷新加载,所以这里设置为 VERTICAL。

5. "内容区域"对象创建

@Override
protected RecyclerView createRefreshableView(Context context, AttributeSet attrs) {
	RecyclerView recyclerView = new RecyclerView(context, attrs);
    return recyclerView;
}

这里也比较简单,new 一个 RecyclerView就可以了。

6. 怎么判断到顶部了

@Override
protected boolean isReadyForPullStart() {
return isFirstItemVisible();
}

在这里我们模仿下PullToRefreshListView的设置,搞一个isFirstItemVisible(),那么重点就是这个方法的编写了:
private boolean isFirstItemVisible() {
	final Adapter adapter = getRefreshableView().getAdapter();

	// 如果未设置Adapter或者Adapter没有数据可以下拉刷新
	if (null == adapter || adapter.getItemCount() == 0) {
		if (DEBUG) {
					Log.d(LOG_TAG, "isFirstItemVisible. Empty View.");
		}
		return true;

	} else {
		// 第一个条目完全展示,可以刷新
		if (getFirstVisiblePosition() == 0) {
			return mRefreshableView.getChildAt(0).getTop() >= mRefreshableView.getTop();
		}
	}

	return false;
}

这里又分为当前RecyclerView是否设置了Adapter,如果未设置数据适配器,我们是允许进行刷新加载动作的,所以返回 true;然后就是获取第一个条目View,getTop()就是获取它距离父控件(RecyclerView)顶端的距离,如果该距离等于RecyclerView距离顶端的距离那么就是第一条木是完全可见的,有点不好理解,来画个图说明下:


    如上图所示,在《 PullToRefresh 分析之二、UI结构 》中我们知道UI的结构为LinearLayout中添加了头部、尾部以及中间的内容, 在"刷新头部"、"加载尾部"都没有显示的时候,在 LinearLayout中只有一个FrameLayout的中间内容的父控件,在上图右侧图片,第一个条目View,getTop()获取的值为负数,小于 RecyclerView的getTop()值0。所以这个时候返回false;只有当第一个条目完全显示的时候getTop()获取值为0,等于 RecyclerView的getTop()值0。
    不知道大家注意没有,在获取第一个可见Item位置下标的时候用到了getFirstVisiablePosition()方法,

/**
 * @Description: 获取第一个可见子View的位置下标
 *
 * @return int: 位置
 * @version 1.0
 * @date 2015-9-23
 * @Author zhou.wenkai
 */
private int getFirstVisiblePosition() {
    View firstVisibleChild = mRefreshableView.getChildAt(0);
    return firstVisibleChild != null ? mRefreshableView.getChildAdapterPosition(firstVisibleChild) : -1;
}

6.怎么判断到底部了

这个的思路和判断到顶部逻辑类似。

四、总结

    
下面把扩展控件的过程步骤总结下:
    1. 继承PullToRefreshBase;
    2. 实现父类中的四个抽象方法;
    3. 实现父类中所有构造方法;
    4. 在getPullToRefreshScrollDirection()设置方向;
    5. 在createRefreshableView()初始化刷新加载View;
    6. 在isReadyForPullStart()判断刷新加载View顶端是否可见;
    7. 在isReadyForPullEnd()判断刷新加载View底端是否可见。

五、源码下载

    给大家提供一个github的地址: Android-PullToRefresh 
    另外,欢迎 star or f**k me on github! 

六、结语

    在该篇中,我们搞清楚了怎么去扩展PullToRefresh框架,并且自己扩展了一个PullToRefreshRecyclerView,相信大家能在自己开发中快速扩展出想要的控件,下篇中《Android PullToRefresh 分析之五、扩展刷新加载样式》我们将要修改PullToRefresh框架,使其能够优雅地定义自己的刷新加载样式!

你可能感兴趣的:(Android,扩展RecyclerView,PullToRefresh)