700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 打造Android万能下拉刷新上拉加载控件

打造Android万能下拉刷新上拉加载控件

时间:2021-01-13 20:07:23

相关推荐

打造Android万能下拉刷新上拉加载控件

转载请注明出处:/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();}}

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

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

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

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

源码下载

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。