打造通用的Android下拉刷新组件(适用于ListView、GridView等各类View)
前言
近期在做项目时,使用了一个开源的下拉刷新ListView组件。极其的不稳定,bug还多。稳定的组件又写得太复杂了,jar包较大。在我的一篇博客中也讲述过下拉刷新的实现,即Android打造(ListView、GridView等)通用的下拉刷新、上拉自己主动载入的组件。可是这样的通过改动Margin的形式感觉不是特别的流畅,因此在这漫长的国庆长假又花了点时间用另外的原理实现了一遍,特此分享出来。
基本原理
原理就是自己定义一个ViewGroup,将Header View, Content View, Footer View从上到下依次布局。如图1 (红色区域为屏幕的显示区域)。在初始时通过滚动,使得该组件在Y轴方向上滚动HeaderView的高度的距离,这样HeaderView就被隐藏掉了,如图2。
而Content View的宽度和高度都是match_parent的,因此此时屏幕上仅仅显示Content View, HeaderView 和 FooterView都被隐藏在屏幕外了。当组件被滚动到顶端时,假设用户继续下拉,那么拦截触摸事件,然后通过Scroller来滚动y轴的偏移量。实现逐步的显示HeaderView,从而到达下拉的效果。如图3。当用户滑动到最底部时会触发载入很多其它的操作。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYmJveWZlaXl1/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" width="200" height="290" />
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYmJveWZlaXl1/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" width="200" height="290" />
图 1 (红色区域为屏幕) 图2 (红色区域为屏幕) 图 3(红色区域为屏幕)
通过使用Scroller使得整个滚动更加的平滑。而使用Margin来实现的话须要自己来计算滚动时间和margin值。并非非常流畅,而且频繁的改动布局參数效率也不高。
使用Scroller仅仅是滚动位置,而没有改动布局參数,因此有点较为突出。
Scroller的使用
为了更好的理解下拉刷的实现。我们先要了解Scroller的作用以及怎样使用。这里我们将做一个简单的演示样例来说明。
Scroller是一个帮助View滚动的辅助类,在使用它之前用户须要通过startScroll来设置滚动的參数,即起始点坐标和x,y轴上要滚动的距离。Scroller它封装了滚动时间、要滚动的目标x轴和y轴,以及在每一个时间内view应该滚动到的x,y轴的坐标点。这样用户就能够在有效的滚动周期内通过Scroller的getCurX()和getCurY()来获取当前时刻View应该滚动的位置,然后通过调用View的scrollTo或者ScrollBy方法进行滚动。
那么怎样推断滚动是否结束呢 ? 我们仅仅须要覆写View类的computeScroll方法,该方法会在View绘制的时候被调用,在里面调用Scroller的computeScrollOffset来推断滚动是否完毕,假设返回true表明滚动未完毕,否则滚动完毕。上述说的scrollTo或者ScrollBy的调用就是在computeScrollOffset为true的情况下调用,而且最后还要调用目标view的postInvalidate()或者invalidate()以实现View的重绘。View的重绘又会导致computeScroll方法被调用,从而继续整个滚动过程,直至computeScrollOffset返回false, 即滚动结束。整个过程有点绕。我们看一个样例吧。
public class ScrollLayout extends FrameLayout { private String TAG = ScrollLayout.class.getSimpleName(); Scroller mScroller ; public ScrollLayout(Context context) {
super(context); mScroller = new Scroller(context) ;
} // 该函数会在View重绘之时被调用
@Override
public void computeScroll() {
if ( mScroller.computeScrollOffset() ) {
// 滚动到此刻View应该滚动到的x,y坐标上.
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 请求重绘该View,从而又会导致computeScroll被调用。然后继续滚动,直到computeScrollOffset返回false
this.postInvalidate();
}
} // 调用这种方法进行滚动,这里我们仅仅滚动竖直方向,
public void scrollTo(int y) {
// 參数1和參数2分别为滚动的起始点水平、竖直方向的滚动偏移量
// 參数3和參数4为在水平和竖直方向上滚动的距离
mScroller.startScroll(getScrollX(), getScrollY(), 0, y);
this.invalidate();
}
}
滚动该视图的代码 :
ScrollLayout scrollView = new ScrollLayout(getContext()) ;
scrollView.scrollTo(100);
通过上面这段代码会让scrollView在y轴上向下滚动100个像素点。我们结合代码来分析一下。
首先调用scrollTo(int y)方法,然后我们在该方法中通过mScroller.startScroll()方法来设置了滚动的參数,然后调用invalidate()方法使得该View重绘。重绘时会调用computeScroll方法,在该方法中通过mScroller.computeScrollOffset()推断滚动是否完毕。假设返回true那代表没有滚动完毕。此时把该View滚动到此刻View应该滚动到的x, y位置。这个位置通过mScroller的getCurX, getCurY获得。
然后继续调用重绘方法。继续运行滚动过程,直至滚动完毕。
了解了Scroller原理后,我们继续看通用的下拉刷新组件的实现吧。
下拉刷新实现
代码量不算多,可是也挺长的,我们这里仅仅拿出重要的点来分析。完毕的源代码在博文最后会给出。以下是重要的代码段 :
/**
* @author mrsimple
*/
public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements
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; /**
* 下拉刷新监听器
*/
protected OnRefreshListener mOnRefreshListener; /**
* header中的箭头图标
*/
private ImageView mArrowImageView;
/**
* 箭头是否向上
*/
private boolean isArrowUp;
/**
* header 中的文本标签
*/
private TextView mTipsTextView;
/**
* header中的时间标签
*/
private TextView mTimeTextView;
/**
* header中的进度条
*/
private ProgressBar mProgressBar;
/**
*
*/
private int mScreenHeight;
/**
*
*/
private int mHeaderHeight;
/**
*
*/
protected OnLoadListener mLoadListener; /**
* @param context
*/
public RefreshLayoutBase(Context context) {
this(context, null);
} /**
* @param context
* @param attrs
*/
public RefreshLayoutBase(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} /**
* @param context
* @param attrs
* @param defStyle
*/
public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs); // 初始化Scroller对象
mScroller = new Scroller(context); // 获取屏幕高度
mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
// header 的高度为屏幕高度的 1/4
mHeaderHeight = mScreenHeight / 4; // 初始化整个布局
initLayout(context);
} /**
* 初始化整个布局
*
* @param context
*/
private final void initLayout(Context context) { // header view
setupHeaderView(context); // 设置内容视图
setupContentView(context);
// 设置布局參数
setDefaultContentLayoutParams();
//
addView(mContentView); // footer view
setupFooterView(context); } /**
* 初始化 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 VIEWS
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); /**
* 与Scroller合作,实现平滑滚动。在该方法中调用Scroller的computeScrollOffset来推断滚动是否结束。 假设没有结束。
* 那么滚动到对应的位置,而且调用postInvalidate方法重绘界面,从而再次进入到这个computeScroll流程,直到滚动结束。
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
} /*
* 在适当的时候拦截触摸事件,这里指的适当的时候是当mContentView滑动到顶部,而且是下拉时拦截触摸事件,否则不拦截,交给其child
* view 来处理。 * @see
* android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) { /*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onTouchEvent will be called and we do the actual
* scrolling there.
*/
final int action = MotionEventCompat.getActionMasked(ev);
// Always handle the case of the touch gesture being complete.
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:
// int yDistance = (int) ev.getRawY() - mYDown;
mYOffset = (int) ev.getRawY() - mLastY;
// 假设拉到了顶部, 而且是下拉,则拦截触摸事件,从而转到onTouchEvent来处理下拉刷新事件
if (isTop() && mYOffset > 0) {
return true;
}
break; } // Do not intercept touch event, let the child handle it
return false;
} /**
* 是否已经到了最顶部,子类需覆写该方法,使得mContentView滑动到最顶端时返回true, 假设到达最顶端用户继续下拉则拦截事件;
*
* @return
*/
protected abstract boolean isTop(); /**
* 是否已经到了最底部,子类需覆写该方法,使得mContentView滑动到最底端时返回true;从而触发自己主动载入很多其它的操作
*
* @return
*/
protected abstract boolean isBottom(); /**
* 显示footer view
*/
private void showFooterView() {
startScroll(mFooterView.getMeasuredHeight());
mCurrentStatus = STATUS_LOADING;
} /**
* 设置滚动的參数
*
* @param yOffset
*/
private void startScroll(int yOffset) {
mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);
invalidate();
} /*
* 在这里处理触摸事件以达到下拉刷新或者上拉自己主动载入的问题
* @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_DOWN:
mLastY = (int) event.getRawY();
break; case MotionEvent.ACTION_MOVE:
int currentY = (int) event.getRawY();
mYOffset = currentY - mLastY;
if (mCurrentStatus != STATUS_LOADING) {
//
changeScrollY(mYOffset);
} rotateHeaderArrow();
changeTips();
mLastY = currentY;
break; case MotionEvent.ACTION_UP:
// 下拉刷新的详细操作
doRefresh();
break;
default:
break; } return true;
} /**
* 改动y轴上的滚动值,从而实现header被下拉的效果
* @param distance
* @return
*/
private void changeScrollY(int distance) {
// 最大值为 scrollY(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;
}
} /**
* 刷新结束,恢复状态
*/
public void refreshComplete() {
mCurrentStatus = STATUS_IDLE;
// 隐藏header view
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);
}
}, 100); } /**
* 载入结束,恢复状态
*/
public void loadCompelte() {
// 隐藏footer
startScroll(mInitScrollY - getScrollY());
mCurrentStatus = STATUS_IDLE;
} /**
* 手指抬起时,依据用户下拉的高度来推断是否是有效的下拉刷新操作。假设下拉的距离超过header view的
* 1/2那么则觉得是有效的下拉刷新操作,否则恢复原来的视图状态.
*/
private void changeHeaderViewStaus() {
int curScrollY = getScrollY();
// 超过1/2则觉得是有效的下拉刷新, 否则还原
if (curScrollY < mInitScrollY / 2) {
// 滚动到能够正常显示header的位置
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();
} /**
* 运行下拉刷新
*/
private void doRefresh() {
changeHeaderViewStaus();
// 运行刷新操作
if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {
mOnRefreshListener.onRefresh();
}
} /**
* 运行下拉(自己主动)载入很多其它的操作
*/
private void doLoadMore() {
if (mLoadListener != null) {
mLoadListener.onLoadMore();
}
} /*
* 丈量视图的宽、高。 宽度为用户设置的宽度,高度则为header, content view, footer这三个子控件的高度仅仅和。 * @see android.view.View#onMeasure(int, int)
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int childCount = getChildCount(); int finalHeight = 0; for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// measure
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 该view所须要的总高度
finalHeight += child.getMeasuredHeight();
} setMeasuredDimension(width, finalHeight);
} /*
* 布局函数,将header, content view,
* footer这三个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 top = getPaddingTop();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.layout(0, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
top += child.getMeasuredHeight();
} // 计算初始化滑动的y轴距离
mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
// 滑动到header view高度的位置, 从而达到隐藏header view的效果
scrollTo(0, mInitScrollY);
} /*
* 滚动监听,当滚动到最底部,且用户设置了载入很多其它的监听器时触发载入很多其它操作.
* @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();
}
} }
在构造函数中会调用initLayout来加入Header View, Content View, Footer View这三个区域的视图, 当中Content View就是我们的核心组件。比方ListView、GridView。这个区域的视图默认宽高都是match_parent的。
Header的高度为屏幕宽度的1/4,但它的有效显示区域仅仅有100像素,其它的都是paddingTop,这样就是的内容显示区域显示在最以下。
这样当用户一直下拉时,首先会显示内容区域。继续下拉则会显示PaddingTop区域。此时就达到header view高度被拉伸的效果。
例如以下图 :
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYmJveWZlaXl1/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" width="320" height="490" />
图 4 图5
不断下拉。y轴的偏移量不断减小,使得header越来越多的部分显示出来。仅仅有白色的内容显示区域是有效的显示区。上面的绿色都是paddingTop区,这样就形成了被拉伸的效果。
加入这三个view之后,我们在onMeasure中对这几个子view进行丈量。使得该组件的宽度为用户设置的宽度,高度为header, content view, footer的高度之和。得到各个子视图的宽高和该组件的总宽高以后,会进行布局操作,即会调用onLayout方法。我们把这个几个视图从上到下排列。
最后将该组件在y方向上滚动与header view的高度相同大小的像素值,使得header view隐藏掉,使得Content View全然显示出来。
/*
* 丈量视图的宽、高。宽度为用户设置的宽度。高度则为header, content view, footer这三个子控件的高度仅仅和。
* @see android.view.View#onMeasure(int, int)
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int childCount = getChildCount(); int finalHeight = 0; for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// measure
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 该view所须要的总高度
finalHeight += child.getMeasuredHeight();
} setMeasuredDimension(width, finalHeight);
} /*
* 布局函数,将header, content view,
* footer这三个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 top = getPaddingTop();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.layout(0, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
top += child.getMeasuredHeight();
} // 计算初始化滑动的y轴距离
mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
// 滑动到header view高度的位置, 从而达到隐藏header view的效果
scrollTo(0, mInitScrollY);
}
然后就是下拉刷新触发点了。在onInterceptTouchEvent方法中,对于ACTION_MOVE事件我们会推断。假设已经滑到了Content View的顶部,而且还继续下拉,那么拦截触摸事件。使得事件转到onTouchEvent方法中处理。事件拦截的关键点例如以下 :
case MotionEvent.ACTION_MOVE:
// int yDistance = (int) ev.getRawY() - mYDown;
mYOffset = (int) ev.getRawY() - mLastY;
// 假设拉到了顶部, 而且是下拉,则拦截触摸事件,从而转到onTouchEvent来处理下拉刷新事件
if (isTop() && mYOffset > 0) {
return true;
}
break;
假设在onTouchEvent中我们依据用户当前触摸事件的y轴位置与上一次的y轴位置的偏移量来改动该组件在y轴上的滚动值,调用的方法为changeScrollY()函数。而且会改动header中的文本内容。
当用户抬起手指时,会推断用户在y轴上滑动的距离是否大于header view的1/2, 假设大于header view的1/2那么为有效的下拉刷新。此时滚动到刚好显示header view的内容y轴位置,然后触发刷新操作,直到用户调用refreshCompete()位置,最后全然隐藏header。否则视为无效的下拉刷新操作,然后通过Scroller滚动来隐藏header view。
而载入很多其它操作为用户滑动到了最底部。而且继续上拉,那么会触发载入很多其它的操作。
在操作在onScroll方法中被触发。
基本原理就是通过一个ViewGroup来组织header view, content view, footer view, 使它们从上到下排列,而且在初始化时滚动y轴。使得header 和 footer全然隐藏,仅仅显示content view。用户下拉或者上拉时。通过推断是否显示header 或者 footer, 也是通过Scroller来滚动y轴的偏移量来实现HeaderView, Footer View的显示和隐藏,不须要改动margin值,这样效率更高。滚动也更平滑。当用户的上拉或者下拉操作满足了条件时,则会触发对应的操作,即下拉刷新、上拉载入很多其它。
如有不明确的地方,就对照參考Android打造(ListView、GridView等)通用的下拉刷新、上拉自己主动载入的组件吧,原理都差点儿相同。
下拉刷新的ListView
/**
* @author mrsimple
*/
public class RefreshListView extends RefreshLayoutBase<ListView> { /**
* @param context
*/
public RefreshListView(Context context) {
this(context, null);
} /**
* @param context
* @param attrs
*/
public RefreshListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} /**
* @param context
* @param attrs
* @param defStyle
*/
public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
} @Override
protected void setupContentView(Context context) {
mContentView = new ListView(context);
// 设置滚动监听器
mContentView.setOnScrollListener(this); } @Override
protected boolean isTop() { // Log.d(VIEW_LOG_TAG,
// "### first pos = " + mContentView.getFirstVisiblePosition()
// + ", getScrollY= " + getScrollY());
return mContentView.getFirstVisiblePosition() == 0
&& getScrollY() <= mHeaderView.getMeasuredHeight();
} @Override
protected boolean isBottom() {
// Log.d(VIEW_LOG_TAG, "### last position = " +
// contentView.getLastVisiblePosition()
// + ", count = " + contentView.getAdapter().getCount());
return mContentView != null && mContentView.getAdapter() != null
&& mContentView.getLastVisiblePosition() ==
mContentView.getAdapter().getCount() - 1;
}
}
须要下拉刷新的组件仅仅须要实现isTop来推断是否滑动到最顶端、isBottom是否滑动到最底部。已经通过setupContentView设置mContentView对象就可以。
使用演示样例
final RefreshListView refreshLayout = new RefreshListView(this);
String[] dataStrings = new String[20];
for (int i = 0; i < dataStrings.length; i++) {
dataStrings[i] = "item - " +
i;
}
// 获取ListView, 这里的listview就是Content view
refreshLayout.setAdapter(new ArrayAdapter<String>(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);
}
});
效果图 :
效果图中含有下拉刷新的ListView, GridView, TextView,能够看到即使实在模拟器中,下拉刷新的效果都挺流畅的。上拉载入很多其它在ListView中正常显示,GridView中在模拟器上没有触发,可是在真机上是正常的。
代码地址
打造通用的Android下拉刷新组件(适用于ListView、GridView等各类View)的更多相关文章
- Android内置下拉刷新组件SwipeRefreshLayout
也许下拉刷新之前,你可能会使用一些第三方的开源库,例如PullToRefresh, ActionBar-PullToRefresh等待,但现在有的正式组成部分---SwipeRefreshLayout ...
- Android下拉刷新底部操作栏的隐藏问题
最近自己编写下拉刷新的时候,发现了一个问题,就是有一个需求是这样的:要求页面中是一个Tab切换界面,一个界面有底部操作栏,不可下拉刷新,另一个界面没有底部操作栏,但可以下拉刷新. 按照平常的做法,我在 ...
- Android下拉刷新效果实现
本文主要包括以下内容 自定义实现pulltorefreshView 使用google官方SwipeRefreshLayout 下拉刷新大致原理 判断当前是否在最上面而且是向下滑的,如果是的话,则加载数 ...
- Google自己的下拉刷新组件SwipeRefreshLayout
SwipeRefreshLayout SwipeRefreshLayout字面意思就是下拉刷新的布局,继承自ViewGroup,在support v4兼容包下,但必须把你的support librar ...
- Android下拉刷新-SwipeRefreshLayout
现在市面上新闻类的App基本上都有下拉刷新,算是一个标配吧,网上关于下拉刷新的博客也有很多,实现方式可以使用开源的PullToRefresh,自定义ListView,或者可以直接使用LineLayOu ...
- Google官方下拉刷新组件---SwipeRefreshLayout
今天在Google+上看到了SwipeRefreshLayout这个名词,遂搜索了下,发现竟然是刚刚google更新sdk新增加的一个widget,于是赶紧抢先体验学习下. SwipeRefreshL ...
- SuperSwipeRefreshLayout 一个功能强大的自己定义下拉刷新组件
SuperSwipeRefreshLayout 一个功能强大的自己定义下拉刷新组件. Why? 下拉刷新这样的控件.想必大家用的太多了,比方使用非常多的XListView等. 近期.项目中非常多列表都 ...
- 【原创】窥视懒人的秘密---android下拉刷新开启手势的新纪元
小飒的成长史原创作品:窥视懒人的秘密---android下拉刷新开启手势的新纪元转载请注明出处 **************************************************** ...
- Android 下拉刷新上拉载入 多种应用场景 超级大放送(上)
转载请标明原文地址:http://blog.csdn.net/yalinfendou/article/details/47707017 关于Android下拉刷新上拉载入,网上的Demo太多太多了,这 ...
随机推荐
- 【BZOJ】4709: [Jsoi2011]柠檬
4709: [Jsoi2011]柠檬 Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 779 Solved: 310[Submit][Status][ ...
- 三、python的数据类型 列表、元组、字典
1.list 列表 列表是由一序列特定顺序排列的元素组成的.可以把字符串,数字,字典等都可以任何东西加入到列表中,列表中的元素之间没有任何关系.列表也是自带下标的,默认也还是从0开始. List常用的 ...
- Google Code Jam 2009 Qualification Round Problem A. Alien Language
https://code.google.com/codejam/contest/90101/dashboard#s=p0 Problem After years of study, scientist ...
- What is the Linux High Availabi
What is the Linux High Availabi 简介: 高可用性群集的出现是为了使群集的整体服务尽可能可用,以便考虑计算硬件和软件的易错性.如果高可用性群集中的主节点发生 ...
- wpf 分别用 xaml 和后台代码实现 色彩渐变
xaml 方法: <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.micros ...
- mysql 虚拟列
http://blog.csdn.net/yueliangdao0608/article/category/351407
- Eclipse maven构建springmvc项目
原文地址: http://www.cnblogs.com/fangjins/archive/2012/05/06/2485459.html 一.背景介绍 对于初学者,用maven构建项目并不是一件容易 ...
- PHP闭包--匿名函数
匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数.最经常用作回调函数(callback)参数的值.当然,也有其它应用的情况. ...
- android 设置屏幕方向
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);//通过程序改变屏幕显示的方向 1.landscape:横屏(风景 ...
- php 验证身份证号码
身份证号码的结构 身份证号码是特征组合码,由17位数字本体码和一位校验码组成. 排列顺序从左至右依此为:六位数字地址码,八位数字出生日期码,三位数字顺序码和一位数字校验码. 地址码(前六位数) 表示编 ...