转载请注明出处:http://blog.youkuaiyun.com/binbinqq86/article/details/70159782
关于列表刷新加载的自定义控件,网上数不胜数,但别人的用起来始终不是那么得心应手,很早以前就想自己去实现一个属于自己的刷新控件,废话不多说,看图:
怎么样,感觉还不错吧~该控件支持AbsListview,Recyclerview,并且可以自己扩展其他类型的View,包括自动刷新,滑到底部自动加载更多,header和footer均可以自定义。
下面就说说实现的主要思路和原理:首先自定义一个View继承于ViewGroup,整个布局从上到下分为header,刷新的view,footer,默认header和footer不可见,这样当下拉的时候去判断是否在列表顶部,是的话就逐渐显示header,否则列表滚动,同理footer也是一样,简单吧!
关键代码如下:
private void init(Context mContext) {
this.mContext = mContext;
mScroller = new Scroller(mContext);
screenHeight = getResources().getDisplayMetrics().heightPixels;
preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
header = LayoutInflater.from(mContext).inflate(R.layout.refresh_header, null, false);
progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);
arrow = (ImageView) header.findViewById(R.id.arrow);
description = (TextView) header.findViewById(R.id.description);
updateAt = (TextView) header.findViewById(R.id.updated_at);
footer = LayoutInflater.from(mContext).inflate(R.layout.loadmore_footer, null, false);
pbFooter = (ProgressBar) footer.findViewById(R.id.pb);
tvLoadMore = (TextView) footer.findViewById(R.id.tv_load_more);
touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
refreshUpdatedAtValue();
addView(header, 0);
}
主要是初始化一些变量,可以看到有header,footer等~
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
if(childView.getVisibility()!=View.GONE){
//获取每个子view的自己高度宽度,取最大的就是viewGroup的大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
maxWidth = Math.max(maxWidth,childView.getMeasuredWidth());
maxHeight = Math.max(maxHeight,childView.getMeasuredHeight());
}
}
//为ViewGroup设置宽高
setMeasuredDimension(maxWidth+getPaddingLeft()+getPaddingRight(), maxHeight+getPaddingTop()+getPaddingBottom());
// Log.e(TAG, "onMeasure: ");
//处理数据不满一屏的情况下禁止上拉
if(mView!=null){
LayoutParams vlp=mView.getLayoutParams();
if(vlp.height==LayoutParams.WRAP_CONTENT){
vlp.height= LayoutParams.MATCH_PARENT;
}
if(vlp.width==LayoutParams.WRAP_CONTENT){
vlp.width= LayoutParams.MATCH_PARENT;
}
mView.setLayoutParams(vlp);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// Log.e(TAG, "onLayout: ");
if(!hasFinishedLayout){
mView=getChildAt(1);
addView(footer);
hasFinishedLayout=true;
if(canLoadMore&&canAutoLoadMore){
setAutoLoadMore();
}
}
if(hideHeaderHeight==0){
hideHeaderHeight = -header.getHeight();
}
if(hideFooterHeight==0){
hideFooterHeight=footer.getHeight();
// Log.e(TAG, "onLayout: "+hideFooterHeight+"@"+hideHeaderHeight);
}
int top=hideHeaderHeight+getPaddingTop();
// header.layout(0,top,maxWidth,top+header.getMeasuredHeight());
// top+=header.getMeasuredHeight();
// mView.layout(0,top,maxWidth,top+mView.getMeasuredHeight());
// top+=mView.getMeasuredHeight();
// footer.layout(0,top,maxWidth,top+footer.getMeasuredHeight());
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
childView.layout(getPaddingLeft(), top, maxWidth+getPaddingLeft(), top+childView.getMeasuredHeight());
top+=childView.getMeasuredHeight();
}
}
}
上面主要就是自定义view必须的两个步骤,onMeasure和onLayout,代码很简单,也没有什么好说的,主要就是测量每个子view的宽高,然后从上到下依次摆放header,刷新的view,footer。
下面来看关键代码:
/**
* 根据当前View的滚动状态来设定 {@link #isTop}
* 的值,每次都需要在触摸事件中第一个执行,这样可以判断出当前应该是滚动View,还是应该进行下拉。
*/
private void judgeIsTop() {
if (mView instanceof AbsListView) {
AbsListView absListView = (AbsListView) mView;
View firstChild = absListView.getChildAt(0);//返回的是当前屏幕中的第一个子view,非整个列表
if (firstChild != null) {
int firstVisiblePos = absListView.getFirstVisiblePosition();//不必完全可见,当前屏幕中第一个可见的子view在整个列表的位置
if (firstVisiblePos == 0 && firstChild.getTop()-mView.getPaddingTop() == 0) {
// 如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新
isTop = true;
} else {
isTop = false;
}
} else {
// 如果ListView中没有元素,也应该允许下拉刷新
isTop = true;
}
} else if (mView instanceof RecyclerView) {
RecyclerView recyclerView = (RecyclerView) mView;
View firstChild = recyclerView.getLayoutManager().findViewByPosition(0);//firstChild不必须完全可见
View firstVisibleChild = recyclerView.getChildAt(0);//返回的是当前屏幕中的第一个子view,非整个列表
// if(firstChild!=null){
// Log.e("tianbin",firstChild.getTop()+"==="+recyclerView.getChildAt(0).getTop());
// }else{
// Log.e("tianbin","+++++++++");
// }
if (firstVisibleChild != null) {
if (firstChild != null && recyclerView.getLayoutManager().getDecoratedTop(firstChild)-mView.getPaddingTop() == 0) {
isTop = true;
} else {
isTop = false;
}
} else {
//没有元素也允许刷新
isTop = true;
}
} else {
isTop = true;
}
}
这里主要是用来判断当前是否处在列表的顶部,这是一个关键点,就像前面所说的,如果处于顶部,往上滑则列表进行滚动,往下拉则显示header,里面我处理了AbsListview和RecyclerView,而其他情况则可以自己去扩展,同理判断底部也是一样,这里就不贴出代码了,最后我会给出源码下载地址。。。
@Override
public boolean dispatchTouchEvent(final MotionEvent event) {
//每次首先进行判断是否在列表顶部或者底部
judgeIsTop();
judgeIsBottom();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
isUserSwiped=false;
startPress=System.currentTimeMillis();
if(event.getPointerId(event.getActionIndex())==0){
mLastY = event.getY(0);
mFirstY = event.getY();
isTouching=true;
canDrag=true;
}else{
return false;
}
break;
case MotionEvent.ACTION_MOVE:
if(!canDrag){
return false;//false交给父控件处理
}
// int pointerIndex=event.findPointerIndex(0);
// float totalDistance = event.getY() - mFirstY;
// float deltaY = event.getY(pointerIndex) - mLastY;
// mLastY = event.getY(pointerIndex);
// Log.e(TAG,touchSlop+"$$$"+Math.abs(event.getY() - mFirstY) );
// Class<?> clazz=View.class;
// try {
// Field field=clazz.getDeclaredField("mHasPerformedLongPress");
// field.setAccessible(true);
// Log.e(TAG, "dispatchTouchEvent: "+field.get(this));
// } catch (NoSuchFieldException e) {
// e.printStackTrace();
// } catch (IllegalAccessException e) {
// e.printStackTrace();
// }
break;
case MotionEvent.ACTION_POINTER_UP:
default:
if (Math.abs(event.getY() - mFirstY) > touchSlop) {//判断是否滑动还是长按
//滑动事件
// Log.e(TAG,"===dispatchTouchEvent===ACTION_POINTER_UP==yyyyyyyy");
isUserSwiped=true;
}else{
//点击或长按事件
// Log.e(TAG,"===dispatchTouchEvent===ACTION_POINTER_UP==zzzzzzzz");
}
//重置==============================================
if(event.getPointerId(event.getActionIndex())==0){
canDrag=false;
}
ratio = DEFAULT_RATIO;
isTouching=false;
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_MOVE:
float deltaY = ev.getY() - mLastY;
if (Math.abs(ev.getY() - mFirstY) > touchSlop) {//只要有滑动,就进行处理,屏蔽一切点击长按事件
if(getScrollY()<0&¤tStatus==STATUS_REFRESHING){//正在刷新并且header没有完全隐藏时,把事件交给自己处理
return true;
}
if(getScrollY()>0&¤tFooterStatus==STATUS_LOADING){//正在刷新并且footer没有完全隐藏时,把事件交给自己处理
return true;
}
if(getScrollY()==0&&((isTop&&deltaY>0)||(isBottom&&deltaY<0))){//header footer都隐藏时,顶部下拉或者底部上拉都把事件交给自己处理
return true;
}
}else{
if(System.currentTimeMillis()-startPress>=ViewConfiguration.getLongPressTimeout()){
//说明长按事件发生,禁止任何滑动操作
// Log.e(TAG, "onInterceptTouchEvent: "+"======longclick happened======" );
canDrag=false;
}
}
break;
case MotionEvent.ACTION_UP:
if (isUserSwiped) {//点击事件发生在onTouchEvent的ACTION_UP中,所以此处进行处理:如果属于滑动则拦截一切事件,禁止传递给子view
return true;
}
if(isRefreshing||isLoading){//正在刷新或者加载的时候,禁止点击事件
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_MOVE:
float deltaY = ev.getY() - mLastY;
mLastY = ev.getY();
boolean showTop=deltaY>=0 && isTop;
boolean hideTop=deltaY<=0 && getScrollY()<0;
// boolean noMove=deltaY==0;//当不动的时候屏蔽一切事件,防止列表滚动
boolean showBottom=deltaY<=0 && isBottom;
boolean hideBottom=deltaY>=0 && getScrollY()>0;
// Log.e(TAG, "dispatchTouchEvent: "+ratio+"+++"+isTop+"###"+getScrollY()+"$$$"+deltaY);
if((showBottom&&canLoadMore)||hideBottom){
if(deltaY<0){
if(getScrollY()>=hideFooterHeight){
ratio += 0.05f;
}
}else{
ratio=1;
}
int dy=(int) (deltaY / ratio);
if(deltaY>0 && Math.abs(dy)>Math.abs(getScrollY())){
//当滑动距离大于可滚动距离时,进行调整
dy=Math.abs(getScrollY());
}
scrollBy(0, -dy);
return true;
}else if ((showTop&&canRefresh)||hideTop) {
//说明头部显示,自己处理滑动,无论上滑下滑均同步移动(==0代表滑动到顶部可以继续下拉)
if (deltaY < 0) {//来回按住上下移动:下拉逐渐增加难度,上拉不变
ratio = 1;//此处如果系数不是1,则会出现列表跳动的现象。。。暂未解决!!!
} else {
if(Math.abs(getScrollY())>=-hideHeaderHeight){
ratio += 0.05f;//当头部露出以后逐步增加下拉难度
}
}
int dy=(int) (deltaY / ratio);
if(deltaY<0 && Math.abs(dy)>Math.abs(getScrollY())){
//当滑动距离大于可滚动距离时,进行调整
dy=-Math.abs(getScrollY());
}
// Log.e(TAG, "dispatchTouchEvent: "+"###"+getScrollY()+"%%%"+dy);
scrollBy(0, -dy);
// Log.e(TAG, "dispatchTouchEvent: "+"###"+getScrollY()+"&&&"+dy);
if (currentStatus != STATUS_REFRESHING){
if (getScrollY() <= hideHeaderHeight) {
currentStatus = STATUS_RELEASE_TO_REFRESH;
} else {
currentStatus = STATUS_PULL_TO_REFRESH;
}
// 时刻记得更新下拉头中的信息
updateHeaderView();
lastStatus = currentStatus;
}
return true;
}else{
return super.onTouchEvent(ev);
}
case MotionEvent.ACTION_UP:
//处理顶部==========================================
if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
// 松手时如果是释放立即刷新状态,就去调用正在刷新的任务
backToTop();
} else if (currentStatus == STATUS_PULL_TO_REFRESH) {
// 松手时如果是下拉状态,就去调用隐藏下拉头的任务
hideHeader(false);
} else if (currentStatus == STATUS_REFRESHING) {
if (getScrollY() <= hideHeaderHeight) {
//回弹
backToTop();
}
}
//处理底部===========================================
if(getScrollY()>0 && getScrollY()<hideFooterHeight && !isLoading){
//松手时隐藏底部
hideFooter();
}else if(getScrollY()>=hideFooterHeight){
//显示底部,开始加载更多
showFooter();
}
return true;
}
return super.onTouchEvent(ev);
}
以上代码就是处理整个触摸事件的核心,也是老生常谈的触摸事件三部曲:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent。
第四行可以看到,每次触摸事件发生时,首先进行顶部和底部的判断,这样便于后面在move发生的时候去判断到底该如何滑动。
isUserSwiped:这个变量主要用来区分用户的滑动和点击,在44行可以看到,如果用户滑动距离超过了最小识别距离,就认为用户是滑动了,这样就屏蔽点击事件,可以看到在onInterceptTouchEvent中拦截了触摸事件,这样就屏蔽子view发生点击事件,为什么isUserSwiped的判断要在ACTION_POINTER_UP中判断呢,这是因为源码中的点击事件发生在这里,这样就解决了滑动和点击事件的冲突。
canDrag:这个变量主要用来判断控件本身及列表是否可以滑动。当长按事件发生后,整个界面应该不允许操作,可以看第79-82行代码,长按事件主要就是在ACTION_DOWN的时候发送一个延迟消息,我就利用这一点去判断长按事件的发生,然后就很好的解决了这个冲突问题。
另外在onTouchEvent中主要就是做了一些滑动的操作,以及头部底部松手后的处理,这里我加入了一个ratio变量用来控制下拉的难度系数。
/**
* 是否支持下拉刷新
*/
private boolean canRefresh=true;
/**
* 是否支持上拉加载
*/
private boolean canLoadMore=true;
/**
* 是否支持滑动到底部自动加载更多
*/
private boolean canAutoLoadMore=false;
private void autoLoadMore(){
if (mListener != null && !isLoading) {
currentFooterStatus=STATUS_LOADING;
updateFooterView();
mScroller.startScroll(0, 0, 0, hideFooterHeight);
invalidate();
isLoading = true;
mListener.onLoadMore();
}
}
/**
* 自动刷新
*/
public void autoRefresh(){
if (mListener != null && !isRefreshing) {
currentStatus = STATUS_REFRESHING;
updateHeaderView();
mScroller.startScroll(0, 0, 0, hideHeaderHeight);
invalidate();
isRefreshing = true;
autoRefresh=true;//放在updateHeaderView后面
mListener.onRefresh();
}
}
上面几个变量用来控制自动刷新和滑动到底部自动加载更多。。。
至此整个的控件就讲解完了,怎么样,简单吧!其中主要的难点就是上面所说的两点:
- 列表和整个控件滑动的冲突处理
- 点击长按事件和滑动的冲突处理
如果还有不明白的地方,大家可以在下面留言~