一、前言
自定义 ViewGroup 是另一种重要的自定义 View 形式,当我们需要自定义子视图的排列方式时,通常需要通过这种形式实现。例如:最常用的下拉刷新组件,实现下拉刷新、上拉加载更多的原理就是自定义一个 ViewGroup,将 Header View、Content View、Footer View 从上到下依次布局。然后在初始时通过 Scroller 滚动使得该组件在 y 轴方向上滚动 Header View 的高度,这样当依赖该 ViewGroup 显示在用户眼前时 Header View 就被隐藏掉了。而 Content View 的宽度和高度都是 match_parent 的,因此,此时屏幕上只显示 Content View,Header View 和 Footer View 都被隐藏在屏幕之外。
当 Content View 被滚动到顶部,此时如果用户继续下拉,那么该下拉刷新组件将拦截触摸事件,然后根据用户的触摸事件获取到手指滑动的 y 轴距离,并通过 Scroller 将该下拉刷新组件在 y 轴上滚动手指滑动的距离,实现 Header View 显示与隐藏,从而达到下拉的效果。
当用户滑动到最底部时会触发加载更多的操作,此时会通过 Scroller 滚动该下拉刷新组件,将 Footer View 显示出来,实现加载更多的效果。
通过使用 Scroller 使得整个滚动效果更加平滑,而使用 Margin 来实现则需要自己来计算滚动时间和 margin 值,滚动效果不是很流畅,且频繁地修改布局参数效率也不高。使用 Scroller 只是滚动位置,并没有修改布局参数,因此,使用 Scroller 是最好的选择。
为了更好地理解下拉刷新的实现,可以先看看以下这篇博文了解 Scroller 的作用以及如何使用。
Android Scroller 介绍与使用说明
二、下拉刷新实现
了解了 Scroller 原理后,我们来看看通用的下拉刷新组件的实现吧。
以下是重要的代码段:
// 下拉刷新组件抽象基类,泛型参数T为中间内容视图的类型
public abstract class RefreshLayoutBase
extends ViewGroup implements AbsListView.OnScrollListener {
/**
* 滚动控制器
*/
protected Scroller mScroller;
/**
* 下拉刷新时显示的header view
*/
protected View mHeaderView;
/**
* 上拉加载更多时显示的footer view
*/
protected View mFooterView;
/**
* 本次触摸滑动y坐标上的偏移量
*/
protected int mYOffset;
/**
* 内容视图, 即用户触摸导致下拉刷新、上拉加载的主视图. 比如ListView, GridView等.
*/
protected T mContentView;
/**
* 最初的滚动位置.第一次布局时滚动header的高度的距离
*/
protected int mInitScrollY = 0;
/**
* 最后一次触摸事件的y轴坐标
*/
protected int mLastY = 0;
/**
* 空闲状态
*/
public static final int STATUS_IDLE = 0;
/**
* 下拉或者上拉状态, 还没有到达可刷新的状态
*/
public static final int STATUS_PULL_TO_REFRESH = 1;
/**
* 下拉或者上拉状态
*/
public static final int STATUS_RELEASE_TO_REFRESH = 2;
/**
* 刷新中
*/
public static final int STATUS_REFRESHING = 3;
/**
* Loading中
*/
public static final int STATUS_LOADING = 4;
/**
* 当前状态
*/
protected int mCurrentStatus = STATUS_IDLE;
/**
* header中的箭头图标
*/
private ImageView mArrowImageView;
/**
* 箭头是否向上
*/
private boolean isArrowUp;
/**
* header 中的文本标签
*/
private TextView mTipsTextView;
/**
* header中的时间标签
*/
private TextView mTimeTextView;
/**
* header中的进度条
*/
private ProgressBar mProgressBar;
/**
* 屏幕高度
*/
private int mScreenHeight;
/**
* Header 高度
*/
private int mHeaderHeight;
/**
* 下拉刷新监听器
*/
protected OnRefreshListener mOnRefreshListener;
/**
* 加载更多回调
*/
protected OnLoadListener mLoadListener;
public RefreshLayoutBase(Context context) {
this(context, null);
}
public RefreshLayoutBase(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化Scroller对象
mScroller = new Scroller(context);
// 获取屏幕高度
mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
// header 的高度为屏幕高度的 1/4
mHeaderHeight = mScreenHeight / 4;
// 初始化整个布局
initLayout(context);
}
/**
* 初始化整个布局,从上到下分别为 header、内容视图、footer
*
* @param context
*/
private final void initLayout(Context context) {
// 设置header view
setupHeaderView(context);
// 设置内容视图
setupContentView(context);
// 设置布局参数
setDefaultContentLayoutParams();
// 添加内容视图,如ListView、GridView等
addView(mContentView);
// 设置footer view
setupFooterView(context);
}
//代码省略
}
在构造函数中首先调用 initLayout()
函数初始化整个布局,从上到下分别为 Header View、内容视图、Footer View,我们先看看这 3 部分的相关函数:
/**
* 初始化 header view
*/
protected void setupHeaderView(Context context) {
mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this,
false);
mHeaderView
.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
mHeaderHeight));
mHeaderView.setBackgroundColor(Color.RED);
// header 的高度为1/4的屏幕高度,但是,它只有100px是有效的显示区域
// 其余为paddingTop,这样是为了达到下拉的效果
mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);
addView(mHeaderView);
// 初始化 header view 中的子视图
mArrowImageView = (ImageView) mHeaderView.findViewById(R.id.pull_to_arrow_image);
mTipsTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_text);
mTimeTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);
mProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);
}
/**
* 初始化 Content View, 子类覆写.
*/
protected abstract void setupContentView(Context context);
/**
* 初始化 footer view
*/
protected void setupFooterView(Context context) {
mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_footer,
this, false);
addView(mFooterView);
}
其中 Header View 和 Footer View 都是从默认的布局中加载,因此它们是固定的。但是,最中间的内容视图是可变的,例如,我们显示内容的控件可能是 ListView、GridView、TextView 等,因此 setContentView()
留给子类去具体化。还有另外两个抽象函数,分别为判断是否下拉到顶部以及上拉到底部的函数,因为不同内容视图判断是否滚动到顶部、底部的实现代码也是不一样的,因此也需要抽象化。函数定义如下:
/**
* 是否已经到了最顶部,子类需覆写该方法,使得mContentView滑动到最顶端时返回true,
* 如果到达最顶端用户继续下拉则拦截事件;
*
* @return
*/
protected abstract boolean isTop();
/**
* 是否已经到了最底部,子类需覆写该方法,使得mContentView滑动到最底端时返回true;
* 从而触发自动加载更多的操作
*
* @return
*/
protected abstract boolean isBottom();
初始化这 3 部分视图之后,接下来的第一个关键步骤就是视图测量与布局,也就是我们自定义 ViewGroup 中必备的两个步骤。Header View 、内容视图、Footer View 是纵向布局,因此,需要将它们从上到下布局。在布局之前还需要测量各个子视图的尺寸以及该下拉刷新组件自身的尺寸,代码如下:
/*
* 丈量视图的宽、高。宽度为用户设置的宽度,
* 高度则为header, content view, footer这三个子控件的高度之和。
* @see android.view.View#onMeasure(int, int)
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// MeasureSpec 中的宽度值
int width = MeasureSpec.getSize(widthMeasureSpec);
// 子视图的个数
int childCount = getChildCount();
// 最终的高度
int finalHeight = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 测量每个子视图的尺寸
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 所有子视图的高度和就是该下拉刷新组件的总高度
finalHeight += child.getMeasuredHeight();
}
// 设置该下拉刷新组件的尺寸
setMeasuredDimension(width, finalHeight);
}
/*
* 布局函数,将header, content view,
* footer view 这三个view从上到下布局。
* 布局完成后通过Scroller滚动到header的底部,即滚动距离为header的高度 +
* 本视图的paddingTop,从而达到隐藏header的效果.
* @see android.view.ViewGroup#onLayout(boolean, int, int, int, int)
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int left = getPaddingLeft();
int top = getPaddingTop();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.layout(left, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
top += child.getMeasuredHeight();
}
// 计算初始化滑动的y轴距离
mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
// 滑动到header view高度的位置, 从而达到隐藏header view的效果
scrollTo(0, mInitScrollY);
}
在 onMeasure()
中我们测量了该组件自身的大小以及所有子视图的大小,并且将该控件的高度设置为所有子视图的高度之和,这样在布局时我们才有足够的空间竖向放置子视图。
在 onLayout()
时,会将 Header View,内容视图、Footer View 从上到下布局。而在 onLayout()
的最后,我们通过 Scroller 将该 ViewGroup 向上滚动了 Header View 的高度,使得 Header View 变得不可见。当用户向下拉时,该组件判断内容视图是否滑到了顶部,此时又通过 Scroller 将该组件向下滚动,使得 Header View 慢慢显示出来。实现这些功能就需要我们处理该控件的触摸事件,通过内容视图滚动到了顶部或者底部来判断是否需要拦截触摸事件。代码如下:
/*
* 在适当的时候拦截触摸事件,这里指的适当的时候是当mContentView滑动到顶部,
* 并且是下拉时拦截触摸事件,否则不拦截,交给其child view 来处理。
* @see
* android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//获取触摸事件的类型
final int action = MotionEventCompat.getActionMasked(ev);
// 取消事件和抬起事件则直接返回 false
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Do not intercept touch event, let the child handle it
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastY = (int) ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
mYOffset = (int) ev.getRawY() - mLastY;
// 如果拉到了顶部, 并且是下拉,则拦截触摸事件
// 从而转到onTouchEvent来处理下拉刷新事件
if (isTop() && mYOffset > 0) {
return true;
}
break;
}
// 默认不拦截触摸事件,使得该控件的子视图能够得到处理机会
return false;
}
onInterceptTouchEvent()
是 ViewGroup 中对触摸事件进行拦截的函数,当返回 true 时后续的触摸事件就会被该 ViewGroup 拦截,此时子视图将不会再获得触摸事件。相应地,返回 false 则表示不进行拦截。例如在上述 onInterceptTouchEvent()
函数中,我们在 ACTION_DOWN 事件(手指第一次按下)时记录了 y 轴的坐标,当用户的手指在屏幕上滑动时就会产生 ACTION_MOVE 事件,此时我们获取了 y 轴坐标,并且与最初的 ACTION_DOWN 事件的 y 轴坐标相减。如果 mYOffset 大于 0,那么表示用户的手指是从上到下滑动,如果此时内容视图已经是到了顶部,例如:ListView 的第一次可见元素就是第一项,那么则返回 true,也就是将后续的触摸事件拦截。此时,后续的 ACTION_MOVE、ACTION_UP 等事件就会由该组件进行处理,处理函数为 onTouchEvent()
函数,代码如下:
/*
* 在这里处理触摸事件以达到下拉刷新或者上拉自动加载的问题
* @see android.view.View#onTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(VIEW_LOG_TAG, "@@@ onTouchEvent : action = " + event.getAction());
switch (event.getAction()) {
// 滑动事件
case MotionEvent.ACTION_MOVE:
// 获取手指触摸的当前 y 坐标
int currentY = (int) event.getRawY();
// 当前坐标减去按下时的 y 坐标得到 y 轴上的偏移量
mYOffset = currentY - mLastY;
if (mCurrentStatus != STATUS_LOADING) {
// 在 y 轴方向上滚动该控件
changeScrollY(mYOffset);
}
// 旋转 Header View 中的箭头图标
rotateHeaderArrow();
// 修改 Header View 中的文本信息
changeTips();
// mLastY 设置为这次的 y 轴坐标
mLastY = currentY;
break;
case MotionEvent.ACTION_UP:
// 下拉刷新的具体操作
doRefresh();
break;
default:
break;
}
// 返回 true 表示消耗该事件,不再传递
return true;
}
在 onTouchEvent()
函数中,我们会判断触摸事件的类型,如果还是 ACTION_MOVE 事件,那么计算当前触摸事件的 y 坐标与 ACTION_DOWN 时的 y 坐标的差值,然后调用 changeScrollY()
函数在 y 轴上滚动该控件。如果用户一直向下滑动手指,那么 mYOffset 值将不断增大,那么此时该控件将不断地往上滚动,Header View 的可见高度也就越来越大。我们看看 changeScrollY()
函数的实现:
/**
* 修改 y 轴上的滚动值,从而实现 Header View 被下拉的效果
* @param distance 这次触摸事件的 y 轴与上一次的 y 轴的差值
* @return
*/
protected void changeScrollY(int distance) {
// 最大值为 mInitScrollY(header 隐藏), 最小值为0 ( header 完全显示).
int curY = getScrollY();
// 下拉
if (distance > 0 && curY - distance > getPaddingTop()) {
scrollBy(0, -distance);
} else if (distance < 0 && curY - distance <= mInitScrollY) {
// 上拉过程
scrollBy(0, -distance);
}
curY = getScrollY();
int slop = mInitScrollY / 2;
if (curY > 0 && curY < slop) {
mCurrentStatus = STATUS_RELEASE_TO_REFRESH;
} else if (curY > 0 && curY > slop) {
mCurrentStatus = STATUS_PULL_TO_REFRESH;
}
}
从上述代码中可以看到,changeScrollY()
函数实际上就是根据这一次与上一次 y 轴的差值来滚动当前控件,由于两次触摸事件的差值较小,因此,滚动起来相对比较流畅。当 distance 小于 0 时,则是向上滚动,此时 Header View 的可见范围越来越小,最后完全隐蔽;当 distance 大于 0 时则是向下滚动,此时 Header View 的可见范围越来越大,这样一来也就实现了下拉时显示 Header View 的效果。当然在下拉过程中,我们也会修改 Header View 布局中的一些控件状态,例如箭头、文本信息等。
Header View 显示之后,当我们的手指离开屏幕时,如果在 y 轴上的滚动高度大于 Header View 有效区域高度的 1/2,那么就会触发刷新操作,否则就会通过 Scroller 将 Header View 再次隐藏起来。相关代码为 ACTION_UP 触摸事件中调用的 doRefresh()
函数:
/**
* 执行下拉刷新
*/
protected void doRefresh() {
changeHeaderViewStaus();
// 执行刷新操作
if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {
mOnRefreshListener.onRefresh();
}
}
/**
* 手指抬起时,根据用户下拉的高度来判断是否是有效的下拉刷新操作。
* 如果下拉的距离超过header view 的 1/2
* 那么则认为是有效的下拉刷新操作,否则恢复原来的视图状态.
*/
private void changeHeaderViewStaus() {
int curScrollY = getScrollY();
// 超过1/2则认为是有效的下拉刷新, 否则还原
if (curScrollY < mInitScrollY / 2) {
mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop()
- curScrollY);
mCurrentStatus = STATUS_REFRESHING;
mTipsTextView.setText(R.string.pull_to_refresh_refreshing_label);
mArrowImageView.clearAnimation();
mArrowImageView.setVisibility(View.GONE);
mProgressBar.setVisibility(View.VISIBLE);
} else {
mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);
mCurrentStatus = STATUS_IDLE;
}
invalidate();
}
在 changeHeaderViewStaus()
函数中,当判断为满足下拉刷新的条件时,就会设置当前组件的状态为 STATUS_REFRESHING 状态,并且设置正好显示 Header View 区域,最后调用 OnRefreshListener 实现用户设定的下拉刷新操作。刷新操作执行完成之后,用户需要调用 refreshComplete()
函数告知当前控件刷新完毕,此时当前控件会将 Header View 隐藏,代码如下:
/**
* 刷新结束,恢复状态
*/
public void refreshComplete() {
mCurrentStatus = STATUS_IDLE;
mScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());
invalidate();
updateHeaderTimeStamp();
// 200毫秒后处理arrow和progressbar,免得太突兀
this.postDelayed(new Runnable() {
@Override
public void run() {
mArrowImageView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.GONE);
}
}, 200);
}
在 refreshComplete()
中将重置控件的状态,并且将 Header View 滚动到屏幕之外。此时,整个下拉刷新操作就完成了。滚动到底部时加载更多比下拉刷新要简单一些,只需要判断是否滚动到底部,如果已经到底部那么直接触发加载更多,因此,当前控件需要监听内容视图的滚动事件:
/*
* 滚动监听,当滚动到最底部,且用户设置了加载更多的监听器时触发加载更多操作.
* @see android.widget.AbsListView.OnScrollListener#onScroll(android.widget.
* AbsListView, int, int, int)
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
// 用户设置了加载更多监听器,且到了最底部,并且是上拉操作,那么执行加载更多.
if (mLoadListener != null && isBottom() && mScroller.getCurrY() <= mInitScrollY
&& mYOffset <= 0
&& mCurrentStatus == STATUS_IDLE) {
showFooterView();
// 调用加载更多
doLoadMore();
}
}
/**
* 显示footer view
*/
private void showFooterView() {
startScroll(mFooterView.getMeasuredHeight());
mCurrentStatus = STATUS_LOADING;
}
/**
* 执行下拉(自动)加载更多的操作
*/
protected void doLoadMore() {
if (mLoadListener != null) {
mLoadListener.onLoadMore();
}
}
在 onScroll()
中监听内容视图的滚动事件,当内容视图滚动到底部时显示 Footer View,并且调用 OnLoadListener 回调执行加载更多的操作。当操作执行完毕后用户需要调用 loadComplete()
函数告知当前控件加载完毕,下拉刷新组件此时隐藏 Footer View 并且设置为 STATUS_IDLE 状态。
这就是整个 RefreshLayoutBase 类的核心逻辑。构成完整的 RefreshLayoutBase 类还需要添加以下代码:
/**
* 设置Content View的默认布局参数
*/
protected void setDefaultContentLayoutParams() {
ViewGroup.LayoutParams params =
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
mContentView.setLayoutParams(params);
}
/**
* 旋转箭头图标
*/
protected void rotateHeaderArrow() {
if (mCurrentStatus == STATUS_REFRESHING) {
return;
} else if (mCurrentStatus == STATUS_PULL_TO_REFRESH && !isArrowUp) {
return;
} else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH && isArrowUp) {
return;
}
mProgressBar.setVisibility(View.GONE);
mArrowImageView.setVisibility(View.VISIBLE);
float pivotX = mArrowImageView.getWidth() / 2f;
float pivotY = mArrowImageView.getHeight() / 2f;
float fromDegrees = 0f;
float toDegrees = 0f;
if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
fromDegrees = 180f;
toDegrees = 360f;
} else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
fromDegrees = 0f;
toDegrees = 180f;
}
RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
animation.setDuration(100);
animation.setFillAfter(true);
mArrowImageView.startAnimation(animation);
if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
isArrowUp = true;
} else {
isArrowUp = false;
}
}
/**
* 根据当前状态修改header view中的文本标签
*/
protected void changeTips() {
if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
mTipsTextView.setText(R.string.pull_to_refresh_pull_label);
} else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
mTipsTextView.setText(R.string.pull_to_refresh_release_label);
}
}
/**
* 修改header上的最近更新时间
*/
private void updateHeaderTimeStamp() {
// 设置更新时间
mTimeTextView.setText(R.string.pull_to_refresh_update_time_label);
SimpleDateFormat sdf = (SimpleDateFormat) SimpleDateFormat.getInstance();
sdf.applyPattern("yyyy-MM-dd HH:mm:ss");
mTimeTextView.append(sdf.format(new Date()));
}
/**
* 设置滚动的参数
*
* @param yOffset
*/
private void startScroll(int yOffset) {
mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);
invalidate();
}
/**
* 设置下拉刷新监听器
*
* @param listener
*/
public void setOnRefreshListener(OnRefreshListener listener) {
mOnRefreshListener = listener;
}
/**
* 设置滑动到底部时自动加载更多的监听器
*
* @param listener
*/
public void setOnLoadListener(OnLoadListener listener) {
mLoadListener = listener;
}
/**
* 加载结束,恢复状态
*/
public void loadCompelte() {
// 隐藏footer
startScroll(mInitScrollY - getScrollY());
mCurrentStatus = STATUS_IDLE;
}
下面我们来看看具体实现类,例如内容视图是 ListView 的实现:
public abstract class RefreshAdapterView extends RefreshLayoutBase {
public RefreshAdapterView(Context context) {
this(context, null);
}
public RefreshAdapterView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RefreshAdapterView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setAdapter(ListAdapter adapter) {
mContentView.setAdapter(adapter);
}
public ListAdapter getAdapter() {
return mContentView.getAdapter();
}
}
public class RefreshListView extends RefreshAdapterView {
public RefreshListView(Context context) {
this(context, null);
}
public RefreshListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
// 设置内容视图为 ListView,并且设置 mContentView 的滚动监听器为当前对象
@Override
protected void setupContentView(Context context) {
mContentView = new ListView(context);
// 设置滚动监听器
mContentView.setOnScrollListener(this);
}
@Override
protected boolean isTop() {
// 当第一个可见项是第一项表示到了顶部
return mContentView.getFirstVisiblePosition() == 0
&& getScrollY() <= mHeaderView.getMeasuredHeight();
}
@Override
protected boolean isBottom() {
// 最后一个可见项是最后一项时表示滚动到了底部
return mContentView != null && mContentView.getAdapter() != null
&& mContentView.getLastVisiblePosition() == mContentView.getAdapter().getCount() - 1;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
}
RefreshListView 覆写了 RefreshLayoutBase 的 3 个函数,分别为设置内容视图、判断是否滚动到顶部以及判断是否滚动到底部。需要注意的是,在 setContentView()
函数中,我们将 mContentView(在这里也就是 ListView)的 onScrollListener 设置为 this,这是因为需要监听 ListView 的滚动状态,当滚动到最后一项时触发加载更多操作。因为 RefreshLayoutBase 实现了 onScrollListener()
接口,而判断是否调用加载更多的代码被封装在了 RefreshLayoutBase 中,因此,在这里直接调用 mContentView 对象的 setOnScrollListener(this)
即可。使用示例代码如下:
private void setListView() {
final RefreshListView refreshLayout = new RefreshListView(this);
List dataStrings = new ArrayList();
// 准备数据
for (int i = 0; i < 20; i++) {
dataStrings.add("item - " + i);
}
// 获取ListView, 这里的listview就是Content View
refreshLayout.setAdapter(new ArrayAdapter(this,
android.R.layout.simple_list_item_1, dataStrings));
// 设置下拉刷新监听器
refreshLayout.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh() {
Toast.makeText(getApplicationContext(), "refreshing", Toast.LENGTH_SHORT)
.show();
refreshLayout.postDelayed(new Runnable() {
@Override
public void run() {
refreshLayout.refreshComplete();
}
}, 1500);
}
});
// 不设置的话到底部不会自动加载
refreshLayout.setOnLoadListener(new OnLoadListener() {
@Override
public void onLoadMore() {
Toast.makeText(getApplicationContext(), "loading", Toast.LENGTH_SHORT)
.show();
refreshLayout.postDelayed(new Runnable() {
@Override
public void run() {
refreshLayout.loadCompelte();
}
}, 1500);
}
});
//
setContentView(refreshLayout);
}
扩展一个支持下拉刷新的控件也很简单,只需要继承自 RefreshLayoutBase 类并且覆写 setContentView()
、isTop()
、isBottom()
函数即可。通过这种形式,使得下拉刷新组件具有良好的可扩展性。