转载请注明出处:http://blog.csdn.net/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等~

@Overrideprotected 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);}}@Overrideprotected 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,而其他情况则可以自己去扩展,同理判断底部也是一样,这里就不贴出代码了,最后我会给出源码下载地址。。。

    @Overridepublic 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);}@Overridepublic 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&&currentStatus==STATUS_REFRESHING){//正在刷新并且header没有完全隐藏时,把事件交给自己处理return true;}if(getScrollY()>0&&currentFooterStatus==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中,所以此处进行处理:如果属于滑动则拦截一切事件,禁止传递给子viewreturn true;}if(isRefreshing||isLoading){//正在刷新或者加载的时候,禁止点击事件return true;}break;}return super.onInterceptTouchEvent(ev);}@Overridepublic 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();}}

上面几个变量用来控制自动刷新和滑动到底部自动加载更多。。。

至此整个的控件就讲解完了,怎么样,简单吧!其中主要的难点就是上面所说的两点:

  • 列表和整个控件滑动的冲突处理
  • 点击长按事件和滑动的冲突处理

如果还有不明白的地方,大家可以在下面留言~

源码下载

打造Android万能下拉刷新上拉加载控件相关推荐

  1. android listview下拉刷新动画,android 安卓 listview 支持下拉刷新 上拉加载更多

    [1]重写listViewimport java.text.SimpleDateFormat; import java.util.Date; import com.example.testdddlea ...

  2. Android 下拉刷新上拉载入 多种应用场景 超级大放送(上)

    转载请标明原文地址:http://blog.csdn.net/yalinfendou/article/details/47707017 关于Android下拉刷新上拉载入,网上的Demo太多太多了,这 ...

  3. Android ListView 实现下拉刷新上拉加载

    转载请注明出处:http://blog.csdn.net/allen315410/article/details/39965327 1.简介 无疑,在Android开发中,ListView是使用非常频 ...

  4. android 列表上拉加载更多,Android 下拉刷新,上拉加载更多控件–支持ListView,GridView和ScrollView...

    麦洛遇到这样一个需求,实现类似于IOS下拉刷新,上拉加载更多的控件.麦洛google,baidu了一番,网上有不少实现,比较常见的是国外牛人的实现,不过国外的实现基本上都是扩展于ListView,所以 ...

  5. Android 下拉刷新上拉加载可以左右滑动

    下面是下拉刷新上拉加载可以左右滑动的实例,下面是效果图: GitHub 下载地址:https://github.com/wuqingsen/MySlidingNested CSDN 下载地址:http ...

  6. Android自定义控件实战——实现仿IOS下拉刷新上拉加载 PullToRefreshLayout

    下拉刷新控件,网上有很多版本,有自定义Layout布局的,也有封装控件的,各种实现方式的都有.但是很少有人告诉你具体如何实现的,今天我们就来一步步实现自己封装的 PullToRefreshLayout ...

  7. Android 下拉刷新上拉加载 多种应用场景 超级大放送(上)

    转载请标明原文地址:http://blog.csdn.net/yalinfendou/article/details/47707017 关于Android下拉刷新上拉加载,网上的Demo太多太多了,这 ...

  8. Android滑动冲突解决方式(下拉刷新上拉加载更多,适配RecyclerView/ListView/ScrollView)

    一.Android事件的分发机制 这里需要了解下Andorid事件的分发机制.事件分发一般是针对一组事件,即ACTION_DOWN > ACTION_UP 或 ACTION_DOWN > ...

  9. Flutter开发之ListView下拉刷新上拉加载更多(35)

    在Flutter开发之ListView组件(21) 文章中,我们了解了ListView组件的基本使用.但是数据比较少,没有涉及分页加载.而实际开发中,下拉刷新和分页加载几乎是所有APP的标配.在iOS ...

  10. 分享轮子-flutter下拉刷新上拉加载

    flutter下拉上拉组件轮子 什么是flutter? 首先说下flutter,估计这个应该挺多人没听过flutter这个框架,它是一个google推出的跨平台的移动应用UI框架,和React Nat ...

最新文章

  1. 织梦html引入html代码,织梦标签引入共html.doc
  2. 理解java中的两种接口
  3. 在VMware ESXI 6.5创建虚拟机
  4. 06 ORA系列:ORA-01741 非法的零长度标识符
  5. english writing sample for professional
  6. C#连接sql server
  7. 27 行代码开发一个最简单的 SAP ALV 报表
  8. oracle .ctl 是什么文件_Oracle误删dual表怎么办?这里教你怎么恢复
  9. junit规则_jUnit:规则
  10. Java-idea-生成for循环
  11. 博客园php教程,PHP仿博客园,个人博客(1)_PHP教程
  12. iOS自动布局高级用法 纯代码约束写法
  13. fork的写时复制1
  14. Python 爬取小程序接口图片
  15. 华为路由器怎么看是不是公网_如何查看华为路由器默认ip地址
  16. IP被封检测和端口被封检测方法分享
  17. 企业支付宝转账到银行卡(免费率 无限额)PHP 演示示例
  18. afc系统线路中心计算机系统,青岛地铁线网AFC系统建设探讨
  19. 实现人rou搜索的10个经典方法
  20. Open3d 获取渲染和固定视角json文件及读入

热门文章

  1. 读书笔记:《招聘面试新法》
  2. 五金与机械行业的WMS系统方案
  3. 2020成考C语言答案,2020年成人高考语文题库(含历年真题练习题模拟题)
  4. 【抖音小程序】抖音小程序避免onClose重复回调 解决广告重复回调
  5. 关于征集参与团体标准起草单位的通知的各地奖励政策汇总
  6. 2018程序员拜年的奇思妙想,涨姿势了
  7. 两招让你成为牛X的T型人才
  8. 网络编程+go+java,Go语言中的TCP/IP网络编程
  9. linux打印文件名称唯美,程序员的情人节应该这么优雅度过(附源码)
  10. 产品经理的职责 产品规划 产品设计 推导研发 职责误区