下拉刷新在Android应用开发中是一种很常见的交互方式,在实际开发中都会引用第三方的下拉刷新库来实现,第三方库通常都经过多个应用程序集成测试,有着相对较高的稳定性和可靠性,里面的代码逻辑也相对比较庞杂,对新手相对不太友好,学习起来比较费时费力,本节就通过前面学习的Android视图基本原理来实现自定义的下拉刷新库。
补白和边距
补白(Padding)指的是视图内部的内容与视图边界之间的距离,通常上下左右四个方向都可以指定补白宽度,补白就相当于视图内容的镶边,它们处于视图范围内。边距(Margin)指的是当前视图与其他视图之间的距离,其他的视图可以是它的父视图也可以是兄弟视图,边距的位置通常都属于视图的父视图,它主要负责将不同的视图分隔开防止它们相互叠加。开发中通常Padding和Margin都设置的是正数,假如把Padding和Margin的值设置为负数又会有什么样的效果呢,这里只测试常见的LinearLayout布局,在它们的内部添加子视图并且设置负数的Padding和Margin值。
上图展示了在LinearLayout中设置了负数值的子视图展示情况,可以看到负数的Padding不仅会影响子视图内容的展示还会影响父布局的尺寸大小。我们知道onMeasure()方法负责测量当前视图的宽高值,onLayout()负责将布局中的视图设置到指定的位置,查看LinearLayout竖向布局的尺寸测量代码。
在measureVertical()测量竖向布局高度时会首先计算内部可见的子视图高度总值,子布局的高度还要加上下补白的数值得到heightSize数值,heightSize还有与最小高度作比较,其实大部分情况都能确保最终setMeasureDimension()方法中使用的高度值就是heightSize的值。考虑前面的mPaddingTop设置成负值的情况,负值会减少heightSize最终的计算结果值也就导致LinearLayout的高度减小。接着查阅LinearLayout竖向布局方法的实现,看它如何排放内部的子视图位置。
// LinearLayout测量布局源代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 计算子控件的总高度mTotalLength
// 总高度会加上自己的上下补白,mPaddingTop为负值会减小布局高度
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState); // 设置LinearLayout的高度为heightSize
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop = mPaddingTop; // mPaddingTop为负值也会影响子视图展示位置
int childLeft;
for (View view : getChildren()) {
childTop += lp.topMargin; // 此处如果topMargin是负值,会影响子视图展示位置
childLeft = paddingLeft + lp.leftMargin;
// child.layout(left, top, left + width, top + height);
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += view.getMeasureHeight();
}
}
在layoutVertical()方法中会根据LinearLayout的paddingLeft和子视图的LayoutParams.leftMargin计算当前子视图左边界距离,mPaddingTop和子视图的LayoutParams.topMargin计算子视图的上边界在布局中的位置。考虑前面的marginTop边距设置为负值的情况,由于负值会使得childTop值变小,也就是说子视图距离LinearLayout顶部边界变短,子视图的位置也就更加靠上。
如果测试横向的LinearLayout会发现即使设置了负值mPaddingTop,它的高度也不会发生变化,查阅layoutHorizontal()方法会发现在测量高度的时候并不会把mPaddingTop值计算在内,自然也就不会发生最终的布局高度改变的效果。测试其他四大布局会发现有些负值mPaddingTop能够改变布局高度,有些设置负值根本不会对布局高度产生任何效果,总结来说在setMeasureDimension() 方法中设置的高度值如果计算了mPaddingTop和mPaddingBottom那么负值补白就可以改变布局高度。
刷新视图原理
在下拉刷新中控件的顶部会慢慢地出现下拉视图,下拉视图展示过程中就代表正在执行网络请求操作,等到网络请求成功返回下拉视图会慢慢消失,展示已经刷修改完数据的新界面。下拉视图的展示和消失都是有一个渐进的过程,不是setVisible()那种即刻消失或展示的样式,想要实现这种渐进展示和消失的动画效果就可以利用负值补白来改变下拉视图的高度值,当mPaddingTop为0的时候刷新视图正常展示;当mPaddingTop从0到负下拉视图高度变化时下拉视图组件高度逐渐变成0,也就是逐渐消失;当mPaddingTop从负下拉视图高度到0变化时下拉视图高度逐渐变大,也就是逐渐展示。
// PullRefreshView基础实现代码
public class PullRefreshView extends FrameLayout {
private static final String TAG = "PullRefreshView";
private View mHeaderView;
private View mContentView;
private static final int MAX_PULL_LENGTH = Utils.dp2px(200);
private static final long MAX_GOBACK_DURATION = 200;
private static final int REFRESH_IDLE = 0; // 静止状态
private static final int REFRESH_PULL = 1; // 手动下拉
private static final int REFRESH_RELEASED = 2; // 下拉松手
private static final int REFRESH_REFRESHING = 3; // 正在刷新
private int mState = REFRESH_IDLE;
private int mHeaderHeight;
private int mTouchSlop;
public interface RefreshListener {
void onRefresh();
}
private RefreshListener mRefreshListener;
private void notifyRefreshStart() {
if (mRefreshListener != null) { // 通知开始刷新操作
mRefreshListener.onRefresh();
}
}
public void notifyRefreshComplete() {
if (isRefreshing()) { // 刷新结束,头部视图弹回到不可见
headerGoBack();
}
}
// 暂时省略其他部分
}
现在开始自定义的下拉刷新控件的实现,让它继承自FrameLayout布局,内部包含两个主要的成员mHeaderView也就是下拉视图,mContentView也就是包含内容的视图对象,比如后面会提到的ScrollView、ListView和RecyclerView。下拉刷新过程是要消耗比较长的时间,对于不能即刻完成的动作为了避免错误访问可以使用状态机来保存它的内部状态,在某种状态下只能执行一些合法的操作避免出现错误。默认情况下的状态为空闲状态REFRESH_IDLE,当用户向下拉动内容控件时处于REFRESH_PULL下拉状态,如果头部视图完全展示出来等到用户松手此时控件内部处于REFRESH_RELEASED状态,用户松手后开始发起网络请求控件处于刷新状态REFRESH_REFRESHING,刷新完成后控件又进入了空闲状态,下拉刷新的状态迁移如下图。控件的有些操作只有在特定状态下才可以执行,比如onRefreshComplete()完成刷新操作必须要求之前状态是REFRESH_REFRESHING正在刷新,如果不是就说明内部状态有问题,需要开发者及时修改内部状态维护出现的异常情况。
如果用户下拉时头部视图完全可见再释放下拉刷新后需要触发网络请求,定义RefreshListener接口内部包含onRefresh()方法,需要监控下拉刷新事件的开发者可以注册刷新监听器。当网络请求完成后可以调用notifyRefreshComplete()方法通知下拉刷新控件收起下拉视图修改内部状态值。如果用户下拉时头部仅仅漏出一部分内容如下图,在用户释放刷新时仅仅将头部视图回弹到不可见,并不会触发网络请求操作。
在headerGoBack()方法中会使用ValueAnimator逐渐修改mHeaderView的mPaddingTop值使得下拉视图高度组件变小直到消失不见。在动画结束的时候同时把下拉刷新控件内部的状态更新成空闲状态,完成一次下拉刷新状态迁移。这里并没有提到下拉刷新视图是如何展示出来的,不同的内容控件有不同方式触发展示逻辑,后面刷新具体的内容控件时再详述下拉视图的展示动画实现。
// HeaderView拉伸回弹实现代码
private void setHeaderPaddingTop(int top) {
// HeaderView会随着paddingTop变化逐渐消失或逐渐展示
mHeaderView.setPadding(0, top, 0, 0);
mHeaderView.requestLayout();
}
private void headerGoBack() {
if (!isReleased() && !isRefreshing()) {
return;
}
ValueAnimator valueAnimator =
ValueAnimator.ofInt(getHeaderPaddingTop(), -mHeaderHeight);
valueAnimator.setDuration(MAX_GOBACK_DURATION);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
setHeaderPaddingTop(value); // 不断地更改头部视图paddingTop
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mState = REFRESH_IDLE; // 头部视图完全不可见时进入REFRESH_IDEL状态
}
});
valueAnimator.start();
}
ScrollView下拉刷新
PullRefreshView接收到的触摸事件一概传递给它的内容控件来处理,不过原生的ScrollView控件内部的触摸事件处理已经固定下来,需要使用ScrollView的子类覆盖dispatchTouchEvent()来修改它默认的处理方式。为此需要在加载PullRefreshView内部的InternalScrollView控件的时候替换系统提供的原生ScrollView。
// PullRefreshView替换内部的用户ScrollView
private void initViews() {
mContentView = getChildAt(0);
FrameLayout.LayoutParams contentParams = (LayoutParams)
mContentView.getLayoutParams();
contentParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
contentParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
removeView(mContentView);
// 将原生ScrollView替换成支持下拉刷新的InternalScrollView
if (mContentView instanceof ScrollView) {
mContentView = new InternalScrollView(getContext(),
(ScrollView) mContentView);
}
mContentView.setLayoutParams(contentParams);
addView(mContentView);
}
// 将PullRefreshView接收到的所有触摸事件都传递给内容控件
public boolean dispatchTouchEvent(MotionEvent event) {
return mContentView.dispatchTouchEvent(event);
}
替换后的InternalScrollView作为PullRefreshView内部的mContentView成员对象,系统派发过来的所有MotionEvent事件都由InternalScrollView负责处理。由于原生ScrollView内部只有用户内容布局存在, InternalScrollView需要先将原生的ScrollView内部用户内容对象添加到竖向LinearLayout底部,LinearLayout的上面部分则负责展示下拉视图。在dispatchTouchEvent()方法中首先判断用户是否在做滑动操作,如果是滑动操作是否满足下拉刷新的条件,满足条件就要执行下拉刷新视图展示动画,否则需要调用super.dispatchTouchEvent()实现默认的ScrollView触摸事件处理。
// InternalScrollView实现代码
private class InternalScrollView extends ScrollView {
private int mDownY;
private int mLastY;
private boolean mIsDragging = false; // 是否在做下拉刷新动作
public InternalScrollView(Context context, ScrollView origin) {
super(context);
setId(origin.getId()); // 将原生ScrollView的id设置给InternalScrollView
LinearLayout linearLayout = new LinearLayout(getContext());
linearLayout.setOrientation(LinearLayout.VERTICAL);
// 获取原生ScrollView中的用户内容布局
View content = origin.getChildAt(0);
origin.removeAllViews();
// 竖向LinearLayout内部包含下拉视图和用户内容布局
linearLayout.addView(mHeaderView);
linearLayout.addView(content);
// InternalScrollView内部的布局包含下拉刷新视图和用户内容视图
addView(linearLayout);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
int y = (int) event.getRawY();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownY = y;
break;
case MotionEvent.ACTION_MOVE:
int motionY = y - mDownY; // 代表用户滑动的方向
int diff = y - mLastY; // 用户本次滑动与上次滑动的偏差
// 如果当前没有滑动操作而且用户移动距离超出最小滑动
// 距离mTouchSlop,如果用户向下滑动且内容控件的第一
// 条数据处在内容顶部,此时需要准备开始下拉操作;如果
// 用户向上滑动而且头部视图部分可见,准备向上滑动头部视图
if (!mIsDragging && Math.abs(motionY) > mTouchSlop &&
((motionY > 0 && isFirstAtTop()) ||
isFirstAtTop() && motionY < 0 &&
getHeaderPaddingTop() > -mHeaderHeight)) {
mIsDragging = true;
}
if (mIsDragging) { // 用户正在做滑动操作
mState = REFRESH_PULL;
offsetHeader(diff); // 渐进增大或减小下拉视图
if (getHeaderPaddingTop() <= -mHeaderHeight) {
// 如果用户手动将下拉视图推到了
// 不可见位置,不再修改下拉视图的大小
mState = REFRESH_IDLE;
mIsDragging = false;
setHeaderPaddingTop(-mHeaderHeight);
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsDragging = false;
if (isPulling()) { // 如果用户正在下拉过程中松手
mState = REFRESH_RELEASED;
// 如果下拉视图已经全部展示出来需要
// 先退回展示全部,再触发刷新操作
if (shouldRefresh()) {
mState = REFRESH_REFRESHING;
goBackAndShowRefresh(); // 参考代码4-38
} else {
// 如果下拉视图没有全部展示,只下拉了一下部分,
// 直接退回去不触发刷新
headerGoBack();
}
}
break;
}
mLastY = y;
// 如果mIsDragging为true代表用户正在做下拉刷新,
// 否则就执行ScrollView内部滚动
return mIsDragging || super.dispatchTouchEvent(event);
}
// 判定当前用户内容视图的顶部在InternalScrollView的顶部,没有内容被卷起来
// 用户这时向下拉就是要做下拉刷新
public boolean isFirstAtTop() {
return mContentView.getScrollY() <= 0;
}
}
上面的代码完整展示了InternalScrollView内部处理下拉刷新的整个过程,最开始的构造函数中先要为原始用户内容控件添加下拉刷新头部视图,最终替换成下图所示,在初始情况下HeaderView是完全不展示的,仅仅展示底部原始用户内容布局。当用户在InternalScrollView上按下,首先记录下最初的按下位置mDownY,并且由super.dispatchTouchEvent(event)处理返回true代表接受后续的触摸事件,如果用户接着移动手指就会发送ACTION_MOVE事件,判定用户正在做滑动操作,除了要求用户从ACTION_DOWN到ACTION_MOVE移动的距离超出最小滑动距离外,还要求用户向上或向下滑动时内容视图没有卷起高度,也就是mScrollY的值为0,而且此时的HeaderView需要完全不可见,此时认定用户正在做下拉刷新操作,之所以存在用户向上滑动是因为下拉过程中用户是可以向上滑动的。
确定用户在做下拉滑动操作后就需要根据用户滑动偏移不断调整HeaderView的paddingTop大小,此时就能见到HeaderView不断变大或者不断减小的效果,当然如果用户一直向上移动HeaderView的paddingTop值就可能越减越小,当paddingTop减小到HeaderView的负值高度时可以忽略用户向上移动。当用户最终释放下拉拖动时在ACTION_UP中判定HeaderView是否已经完全展示,如果是就触发刷新操作,否就直接将部分展示的HeaderView弹回不可见。为了保证用户操作的平滑性用户下拉可以把HeaderView拉到比实际高度高很多的距离,这种情况下就需要先将多拉出来的高度隐藏再开始触发刷新工作。
上图中用户下拉很长距离导致HeaderView整体的高度比原始高度高了很多,此时就需要先把HeaderView被多拉出来的高度隐藏起来,等到超长高度隐藏结束后就可以通知触发刷新操作。
// HeaderView拉伸过长弹回到实际高度展示
private void goBackAndShowRefresh() {
if (!isRefreshing()) {
return;
}
int paddingTop = getHeaderPaddingTop();
// paddingTop为零的时候下拉视图完全展示,超出0时需要先回到0
if (paddingTop > 0) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(paddingTop, 0);
valueAnimator.setDuration(MAX_GOBACK_DURATION);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
setHeaderPaddingTop(value);
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 下拉视图paddingTop为零时触发刷新操作
notifyRefreshStart();
}
});
valueAnimator.start();
} else {
notifyRefreshStart();// 下拉视图paddingTop为零时触发刷新操作
}
}
代码中paddingTop大于零就代表用户将HeaderView下拉的比实际高度要高出paddingTop的长度,需要先将HeaderView缩回到paddingTop为零的正常高度再触发刷新操作。到目前为止ScrollView的下拉刷新就成功触发了网络请求,等到网络请求成功后会通知刷新操作已完成并调用headGoBack()实现下拉视图渐进消失操作,ScrollView的一次下拉刷新交互就完成了。
ListView下拉刷新
在初始化PullRefreshView内部控件时如果子控件是ListView类型,需要将它替换成InternalListView自定义控件,InternalListView需要控件会在调用addHeaderView()方法添加HeaderView为头部视图,这样当头部视图的高度发生变化的时候ListView内部的用户内容控件也会随之变化位置。
// InternalListView实现代码
private class InternalListView extends ListView {
private int mDownY;
private int mLastY;
private boolean mIsDragging = false;
public InternalListView(Context context, ListView origin) {
super(context);
ListView.LayoutParams layoutParams =
new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
FrameLayout frameLayout = new FrameLayout(getContext());
frameLayout.addView(mHeaderView);
frameLayout.setLayoutParams(layoutParams);
addHeaderView(frameLayout); // 将下拉视图添加成ListView的头部视图
setId(origin.getId());
}
// 与InternalScrollView的处理基本一样
public void dispatchTouchEvent(MotionEvent e) { .... }
public boolean isFirstAtTop() { // 判定ListView头部没有内容被卷起
if (getChildCount() < 2) {
return false;
}
View view = getChildAt(1);
// 第二个View,其实就是第一个用户内容View并且展示的是第一条用户数据
return view.getTop() < mTouchSlop && getFirstVisiblePosition() <= 1;
}
}
InternalListView和InternalScrollView在判定顶部内容没有卷起稍有不同,InternalListView要判定它内部的第二个视图处于顶部位置,用户在这种情况下向下滑动才能够被判定是在做下拉刷新操作。在InternalListView替换ListView控件时会在头部添加下拉视图,下拉视图就是它内部的第一个视图。ListView内部会使用回收复用机制防止过多创建视图对象,第二个视图并不代表它展示的是用户数据中的第一条内容,需要加上getFirstVisiblePosition() <= 1确保第二个视图展示的是用户数据列表里的第一条数据。
RecycleView下拉刷新
RecyclerView是Android Design包中提供的用于替换ListView和GridView等动态视图的控件,通过设置不同的LayoutManager对象就可以实现展示成ListView样式还是GridView样式,这里仅仅讨论ListView样式展示的RecyclerView的下拉刷新实现。RecyclerView自带了ViewHolder机制实现,但不包含添加头部视图和底部视图的功能,想要像ListView那样通过添加头部视图来实现下拉刷新就需要先实现RecyclerView的头部和底部视图添加功能。
// InternalRecyclerView实现代码
private class InternalRecyclerView extends BaseRecyclerView {
private boolean mIsDragging = false;
private View mHeaderView;
public InternalRecyclerView(Context context, RecyclerView origin) {
super(context);
setId(origin.getId());
setLayoutManager(new LinearLayoutManager(context));
mHeaderView = LayoutInflater.from(context).inflate(R.layout.layout_header, this, false);
addHeaderView(mHeaderView);
}
// 与InternalScrollView的处理基本一样
public void dispatchTouchEvent(MotionEvent e) { .... }
public boolean isFirstAtTop() {
return !canScrollVertically(-1);
}
}
总体上来说RecyclerView的实现和ListView基本类似,不过RecyclerView的下拉刷新判定还是有点特殊的,RecyclerView可以使用 !canScrollVertically(-1)判定它是否能够向下拉动,如果无法向下拉动表示用户目前正在做下拉刷新操作。
在实现了下拉操作的判定后只剩下如何实现在RecyclerView中添加头部视图的实现,参考ListView的源代码中实现添加头部和底部视图的实现,源码中会创建HeaderWrapperAdapter对象,它会包含用户添加的HeaderView,FooterView和用户设置的Adapter对象。
// HeaderWrapperAdapter实现代码
public class HeaderWrapperAdapter extends RecyclerView.Adapter<BaseRecyclerViewHolder> {
private List<View> mHeaders; // HeaderView列表
private List<View> mFooters; // FooterView列表
private BaseRecyclerAdapter<BaseRecyclerViewHolder> mAdapter; // 用户Adapter对象
private static final int HEADER_VIEW_TYPE = 0x8888; // HeaderView类型
private static final int FOOTER_VIEW_TYPE = 0x9999; // FooterView类型
HeaderWrapperAdapter(List<View> headers, List<View> footers,
BaseRecyclerAdapter adapter) {
this.mHeaders = headers;
this.mFooters = footers;
this.mAdapter = adapter;
}
@Override
public BaseRecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
int viewType) {
int realType = getType(viewType), position = getPosition(viewType);
if (realType == HEADER_VIEW_TYPE) {
return new HeaderViewHolder(mHeaders.get(position));
} else if (realType == FOOTER_VIEW_TYPE) {
return new HeaderViewHolder(mFooters.get(position –
mAdapter.getItemCount() - mHeaders.size()));
} else {
return mAdapter.onCreateViewHolder(viewGroup, realType);
}
}
@Override // 绑定ViewHolder是只需要执行用户内容Adapter的绑定操作
public void onBindViewHolder(@NonNull BaseRecyclerViewHolder viewHolder,
int position) {
if (position >= mHeaders.size() &&
position < mHeaders.size() + mAdapter.getItemCount()) {
mAdapter.onBindViewHolder(viewHolder, position - mHeaders.size());
}
}
@Override // 绑定ViewHolder是只需要执行用户内容Adapter的绑定操作
public void onBindViewHolder(@NonNull BaseRecyclerViewHolder viewHolder,
int position, @NonNull List<Object> payloads) {
if (position >= mHeaders.size() &&
position < mHeaders.size() + mAdapter.getItemCount()) {
mAdapter.onBindViewHolder(viewHolder, position - mHeaders.size());
}
}
@Override
public int getItemCount() {
// RecyclerView内的元素个数,头视图、尾视图和用户视图总个数
return mHeaders.size() + mAdapter.getItemCount() + mFooters.size();
}
@Override
public int getItemViewType(int position) { // 根据position决定视图类型
if (position < mHeaders.size()) {
return makeTypePos(HEADER_VIEW_TYPE, position);
} else if (position < mHeaders.size() + mAdapter.getItemCount()) {
return makeTypePos(mAdapter.getItemViewType(
position - mHeaders.size()), position);
} else {
return makeTypePos(FOOTER_VIEW_TYPE, position);
}
}
private int makeTypePos(int type, int pos) { // viewType和position绑定到int中
return (type << 16) + pos;
}
private int getType(int typePos) { // 从int高16为获取viewType
return typePos >>> 16;
}
private int getPosition(int typePos) { // 从int低16位获取position
return typePos & 0xffff;
}
private static final class HeaderViewHolder extends BaseRecyclerViewHolder {
HeaderViewHolder(@NonNull View itemView) {
super(itemView);
}
}
}
代码中通过继承RecyclerView.Adapter创建了HeaderWrapperAdapter类型,该类型内部增加了HeaderView列表与FooterView列表,同时包含用户自己的RecyclerView.Adapter对象 ,在计算适配器内部数量长度包含了头部列表、尾部列表和用户适配器数据长度。当RecyclerView展示内部控件时先调用getItemViewType()根据position判定是头部视图、尾部视图还是用户内容视图,然后在将视图类型viewType和position绑定到int中传递到onCreateViewHolder()创建不同类型的ViewHolder对象。onCreateViewHolder()方法中只允许传递进来viewType类型数据,不过这里使用移位运算符实现在int数字中保存viewType和当前position两个数据,本节就使用如下的处理方式使用int的前两个字节保存viewType,后两个字节保存position。
接着创建自定义的BaseRecyclerView继承自RecyclerView,它内部使用HeaderWrapperAdapter管理内容视图,当用户调用setAdapter()内容适配器的时候就把它封装到HeaderWrapperAdapter内部,同时要添加addHeaderView()/removeHeaderView()等接口实现头部视图的添加移除工作。
// 支持Header和Fooer的BaseRecyclerView实现代码
public class BaseRecyclerView extends RecyclerView {
private List<View> mHeaders = new ArrayList<>();
private List<View> mFooters = new ArrayList<>();
private HeaderWrapperAdapter mWrapperAdapter;
private Adapter mAdapter;
@Override
public void setAdapter(@Nullable Adapter adapter) {
if (adapter instanceof BaseRecyclerAdapter) {
mAdapter = adapter;
mWrapperAdapter = new HeaderWrapperAdapter(mHeaders,
mFooters, (BaseRecyclerAdapter) adapter);
super.setAdapter(mWrapperAdapter);
} else {
super.setAdapter(adapter);
}
}
public void addHeaderView(View headerView) {
if (mWrapperAdapter != null) {
mWrapperAdapter.notifyItemInserted(mHeaders.size() - 1);
}
}
public void removeHeaderView(View headerView) {
int index = mHeaders.indexOf(headerView);
if (index < 0) {
return;
}
mHeaders.remove(headerView);
if (mWrapperAdapter != null) {
mWrapperAdapter.notifyItemRemoved(index);
}
}
// 省略底部视图添加、删除代码
}
代码中BaseRecyclerView继承自RecyclerView同时覆盖了setAdapter()方法,在设置适配器时会自动将BaseRecyclerAdapter封装到HeaderWrapperAdapter中,当用户调用addHeaderView()方法时实际上会把HeaderView添加到HeaderWrapperAdapter中,此时只要调用notifyItemInserted()就能够将添加的HeaderView展示出来。BaseRecyclerView通过addHeaderView() 添加下拉刷新的HeaderView后,在下拉刷新中使用canScrollVertical()判定顶部没有卷起内容,其他的用户事件处理与ScrollView基本相同,这样RecyclerView就实现了下拉刷新功能。