关于列表刷新加载的自定义控件,网上数不胜数,但别人的用起来始终不是那么得心应手,很早以前就想自己去实现一个属于自己的刷新控件,废话不多说,看图:
怎么样,感觉还不错吧~该控件支持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(); } }上面几个变量用来控制自动刷新和滑动到底部自动加载更多。。。
至此整个的控件就讲解完了,怎么样,简单吧!其中主要的难点就是上面所说的两点:
列表和整个控件滑动的冲突处理点击长按事件和滑动的冲突处理如果还有不明白的地方,大家可以在下面留言~
