支持上拉加载更多
先讲下原理: ScrollView的子View 主要分为3部分:head头部,滚动内容,fooder底部 我们实现惯性滑动,以及回弹,都是靠超过head或者fooder 就重新滚动到 ,内容的顶部或者底部。 之前看了Pulltorefresh 他是通过不断改变 head或者 fooder的 pading 值来实现 上拉或者 下拉的效果。感觉有点不流畅,而且层次嵌套得比较多。当然他的好处是扩展性好。 因工作需求,需要层次嵌套少,对性能要求非常高。因此重新自定义了ViewGroup实现。 直接上代码: [java] view plain copy packagecom.example.administrator.customscrollview; importandroid.content.Context; importandroid.content.res.TypedArray; importandroid.util.AttributeSet; importandroid.util.Log; importandroid.view.Gravity; importandroid.view.MotionEvent; importandroid.view.VelocityTracker; importandroid.view.View; importandroid.view.ViewConfiguration; importandroid.view.ViewGroup; importandroid.widget.OverScroller; /** *自定义pulltorefreshLayout *TODO:ferris2015年9月11日18:52:40 */ publicclassPullTorefreshScrollViewextendsViewGroup{ privateFoodeLayoutfooder_layout;//topandbuttom privateViewtop_layout; privateintdesireWidth,desireHeight; privateVelocityTrackervelocityTracker; privateintmPointerId; privatefloatx,y; privateOverScrollermScroller; privateintmaxFlingVelocity,minFlingVelocity; privateintmTouchSlop; protectedBooleanisMove=false; protectedfloatdownX=0,downY=0; privateinttop_hight=0; privateintscrollYButtom=0; privateintnScrollYButtom=0; privateintpullDownMin=0; privateBooleanisEnablePullDown=true; privateBooleanisFirst=true; publicvoidsetEnablePullDown(BooleanisEnablePullDown){ this.isEnablePullDown=isEnablePullDown; } publicPullTorefreshScrollView(Contextcontext){ super(context); init(null,0); } publicPullTorefreshScrollView(Contextcontext,AttributeSetattrs){ super(context,attrs); init(attrs,0); } publicPullTorefreshScrollView(Contextcontext,AttributeSetattrs,intdefStyle){ super(context,attrs,defStyle); init(attrs,defStyle); } privatevoidinit(AttributeSetattrs,intdefStyle){ //Loadattributes //finalTypedArraya=getContext().obtainStyledAttributes( //attrs,R.styleable.PullTorefreshScrollView,defStyle,0); // // //a.recycle(); mScroller=newOverScroller(getContext()); maxFlingVelocity=ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity(); minFlingVelocity=ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity(); mTouchSlop=ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @Override protectedvoidonFinishInflate(){ super.onFinishInflate(); fooder_layout=(FoodeLayout)findViewById(R.id.fooder_layout); top_layout=findViewById(R.id.top_layout); if(isEnablePullDown){ fooder_layout.showFooderPull(); }else{ fooder_layout.hideFooder(); } } publicintgetScrollYTop(){ returntop_hight; } publicintgetScrollYButtom(){ returnscrollYButtom; } publicintgetNScrollYTop(){ return0; } publicintgetNScrollYButtom(){ returnnScrollYButtom; } publicintmeasureWidth(intwidthMeasureSpec){ intresult=0; intmeasureMode=MeasureSpec.getMode(widthMeasureSpec); intwidth=MeasureSpec.getSize(widthMeasureSpec); switch(measureMode){ caseMeasureSpec.AT_MOST: caseMeasureSpec.EXACTLY: result=width; break; default: break; } returnresult; } publicintmeasureHeight(intheightMeasureSpec){ intresult=0; intmeasureMode=MeasureSpec.getMode(heightMeasureSpec); intheight=MeasureSpec.getSize(heightMeasureSpec); switch(measureMode){ caseMeasureSpec.AT_MOST: caseMeasureSpec.EXACTLY: result=height; break; default: break; } returnresult; } @Override protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){ //计算所有childview要占用的空间 intwidth=measureWidth(widthMeasureSpec); intheight=measureHeight(heightMeasureSpec); desireWidth=0; desireHeight=0; intcount=getChildCount(); for(inti=0;i<count;++i){ Viewv=getChildAt(i); if(v.getVisibility()!=View.GONE){ LayoutParamslp=(LayoutParams)v.getLayoutParams(); measureChildWithMargins(v,widthMeasureSpec,0, heightMeasureSpec,0); //只是在这里增加了垂直或者水平方向的判断 if(v.getId()==R.id.top_layout){ top_hight=v.getMeasuredHeight(); } desireWidth=Math.max(desireWidth,v.getMeasuredWidth() +lp.leftMargin+lp.rightMargin); desireHeight+=v.getMeasuredHeight()+lp.topMargin +lp.bottomMargin; } } //countwithpadding desireWidth+=getPaddingLeft()+getPaddingRight(); desireHeight+=getPaddingTop()+getPaddingBottom(); //seeifthesizeisbigenough desireWidth=Math.max(desireWidth,getSuggestedMinimumWidth()); desireHeight=Math.max(desireHeight,getSuggestedMinimumHeight()); //处理内容比较少的时候,就添加一定的高度 intscrollHight=height+top_hight*2; if(scrollHight>desireWidth){ intoffset=scrollHight-desireHeight; Viewview=newView(getContext()); view.setBackgroundResource(R.color.top_layout_color); LayoutParamslp=newLayoutParams(LayoutParams.MATCH_PARENT,offset); addView(view,getChildCount()-1,lp); desireWidth=scrollHight; } setMeasuredDimension(resolveSize(desireWidth,widthMeasureSpec), resolveSize(desireHeight,heightMeasureSpec)); scrollYButtom=desireHeight-getMeasuredHeight()-top_hight; nScrollYButtom=desireHeight-getMeasuredHeight(); //如果上啦拖出一半的高度,就代表将要执行上啦 pullDownMin=nScrollYButtom-top_hight/2; if(isFirst){ scrollTo(0,top_hight); isFirst=false; } } @Override protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){ finalintparentLeft=getPaddingLeft(); finalintparentRight=r-l-getPaddingRight(); finalintparentTop=getPaddingTop(); finalintparentBottom=b-t-getPaddingBottom(); if(BuildConfig.DEBUG) Log.d("onlayout","parentleft:"+parentLeft+"parenttop:" +parentTop+"parentright:"+parentRight +"parentbottom:"+parentBottom); intleft=parentLeft; inttop=parentTop; intcount=getChildCount(); for(inti=0;i<count;++i){ Viewv=getChildAt(i); if(v.getVisibility()!=View.GONE){ LayoutParamslp=(LayoutParams)v.getLayoutParams(); finalintchildWidth=v.getMeasuredWidth(); finalintchildHeight=v.getMeasuredHeight(); finalintgravity=lp.gravity; finalinthorizontalGravity=gravity &Gravity.HORIZONTAL_GRAVITY_MASK; finalintverticalGravity=gravity &Gravity.VERTICAL_GRAVITY_MASK; //layoutvertical,andonlyconsiderhorizontalgravity left=parentLeft; top+=lp.topMargin; switch(horizontalGravity){ caseGravity.LEFT: break; caseGravity.CENTER_HORIZONTAL: left=parentLeft +(parentRight-parentLeft-childWidth)/2 +lp.leftMargin-lp.rightMargin; break; caseGravity.RIGHT: left=parentRight-childWidth-lp.rightMargin; break; } v.layout(left,top,left+childWidth,top+childHeight); top+=childHeight+lp.bottomMargin; } } } @Override protectedandroid.view.ViewGroup.LayoutParamsgenerateDefaultLayoutParams(){ returnnewLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } @Override publicandroid.view.ViewGroup.LayoutParamsgenerateLayoutParams( AttributeSetattrs){ returnnewLayoutParams(getContext(),attrs); } @Override protectedandroid.view.ViewGroup.LayoutParamsgenerateLayoutParams( android.view.ViewGroup.LayoutParamsp){ returnnewLayoutParams(p); } publicstaticclassLayoutParamsextendsMarginLayoutParams{ publicintgravity=-1; publicLayoutParams(Contextc,AttributeSetattrs){ super(c,attrs); TypedArrayta=c.obtainStyledAttributes(attrs, R.styleable.SlideGroup); gravity=ta.getInt(R.styleable.SlideGroup_layout_gravity,-1); ta.recycle(); } publicLayoutParams(intwidth,intheight){ this(width,height,-1); } publicLayoutParams(intwidth,intheight,intgravity){ super(width,height); this.gravity=gravity; } publicLayoutParams(android.view.ViewGroup.LayoutParamssource){ super(source); } publicLayoutParams(MarginLayoutParamssource){ super(source); } } /** *onInterceptTouchEvent()用来询问是否要拦截处理。onTouchEvent()是用来进行处理。 *<p/> *例如:parentLayout----childLayout----childView事件的分发流程: *parentLayout::onInterceptTouchEvent()---false?---> *childLayout::onInterceptTouchEvent()---false?---> *childView::onTouchEvent()---false?---> *childLayout::onTouchEvent()---false?--->parentLayout::onTouchEvent() *<p/> *<p/> *<p/> *如果onInterceptTouchEvent()返回false,且分发的子View的onTouchEvent()中返回true, *那么onInterceptTouchEvent()将收到所有的后续事件。 *<p/> *如果onInterceptTouchEvent()返回true,原本的target将收到ACTION_CANCEL,该事件 *将会发送给我们自己的onTouchEvent()。 */ @Override publicbooleanonInterceptTouchEvent(MotionEventev){ finalintaction=ev.getActionMasked(); if(BuildConfig.DEBUG) Log.d("onInterceptTouchEvent","action:"+action); if(action==MotionEvent.ACTION_DOWN&&ev.getEdgeFlags()!=0){ //该事件可能不是我们的 returnfalse; } booleanisIntercept=false; switch(action){ caseMotionEvent.ACTION_DOWN: //如果动画还未结束,则将此事件交给onTouchEvet()处理, //否则,先分发给子View isIntercept=!mScroller.isFinished(); //如果此时不拦截ACTION_DOWN时间,应该记录下触摸地址及手指id,当我们决定拦截ACTION_MOVE的event时, //将会需要这些初始信息(因为我们的onTouchEvent将可能接收不到ACTION_DOWN事件) mPointerId=ev.getPointerId(0); //if(!isIntercept){ downX=x=ev.getX(); downY=y=ev.getY(); //} break; caseMotionEvent.ACTION_MOVE: intpointerIndex=ev.findPointerIndex(mPointerId); if(BuildConfig.DEBUG) Log.d("onInterceptTouchEvent","pointerIndex:"+pointerIndex +",pointerId:"+mPointerId); floatmx=ev.getX(pointerIndex); floatmy=ev.getY(pointerIndex); if(BuildConfig.DEBUG) Log.d("onInterceptTouchEvent","action_move[touchSlop:" +mTouchSlop+",deltaX:"+(x-mx)+",deltaY:" +(y-my)+"]"); //根据方向进行拦截,(其实这样,如果我们的方向是水平的,里面有一个ScrollView,那么我们是支持嵌套的) if(Math.abs(y-my)>=mTouchSlop){ isIntercept=true; } //如果不拦截的话,我们不会更新位置,这样可以通过累积小的移动距离来判断是否达到可以认为是Move的阈值。 //这里当产生拦截的话,会更新位置(这样相当于损失了mTouchSlop的移动距离,如果不更新,可能会有一点点跳的感觉) if(isIntercept){ x=mx; y=my; } break; caseMotionEvent.ACTION_CANCEL: caseMotionEvent.ACTION_UP: //这是触摸的最后一个事件,无论如何都不会拦截 if(velocityTracker!=null){ velocityTracker.recycle(); velocityTracker=null; } break; caseMotionEvent.ACTION_POINTER_UP: solvePointerUp(ev); break; } returnisIntercept; } privatevoidsolvePointerUp(MotionEventevent){ //获取离开屏幕的手指的索引 intpointerIndexLeave=event.getActionIndex(); intpointerIdLeave=event.getPointerId(pointerIndexLeave); if(mPointerId==pointerIdLeave){ //离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置VelocityTracker intreIndex=pointerIndexLeave==0?1:0; mPointerId=event.getPointerId(reIndex); //调整触摸位置,防止出现跳动 x=event.getX(reIndex); y=event.getY(reIndex); if(velocityTracker!=null) velocityTracker.clear(); } } @Override publicbooleanonTouchEvent(MotionEventevent){ finalintaction=event.getActionMasked(); if(velocityTracker==null){ velocityTracker=VelocityTracker.obtain(); } velocityTracker.addMovement(event); switch(action){ caseMotionEvent.ACTION_DOWN: //获取索引为0的手指id isMove=false; mPointerId=event.getPointerId(0); x=event.getX(); y=event.getY(); if(!mScroller.isFinished()) mScroller.abortAnimation(); break; caseMotionEvent.ACTION_MOVE: isMove=true; //获取当前手指id所对应的索引,虽然在ACTION_DOWN的时候,我们默认选取索引为0 //的手指,但当有第二个手指触摸,并且先前有效的手指up之后,我们会调整有效手指 //屏幕上可能有多个手指,我们需要保证使用的是同一个手指的移动轨迹, //因此此处不能使用event.getActionIndex()来获得索引 finalintpointerIndex=event.findPointerIndex(mPointerId); floatmx=event.getX(pointerIndex); floatmy=event.getY(pointerIndex); moveBy((int)(x-mx),(int)(y-my)); x=mx; y=my; break; caseMotionEvent.ACTION_UP: isMove=false; velocityTracker.computeCurrentVelocity(1000,maxFlingVelocity); floatvelocityX=velocityTracker.getXVelocity(mPointerId); floatvelocityY=velocityTracker.getYVelocity(mPointerId); completeMove(-velocityX,-velocityY); if(velocityTracker!=null){ velocityTracker.recycle(); velocityTracker=null; } break; caseMotionEvent.ACTION_POINTER_UP: //获取离开屏幕的手指的索引 isMove=false; intpointerIndexLeave=event.getActionIndex(); intpointerIdLeave=event.getPointerId(pointerIndexLeave); if(mPointerId==pointerIdLeave){ //离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置VelocityTracker intreIndex=pointerIndexLeave==0?1:0; mPointerId=event.getPointerId(reIndex); //调整触摸位置,防止出现跳动 x=event.getX(reIndex); y=event.getY(reIndex); if(velocityTracker!=null) velocityTracker.clear(); } break; caseMotionEvent.ACTION_CANCEL: isMove=false; break; } returntrue; } privateBooleanisPull=false; //此处的moveBy是根据水平或是垂直排放的方向, //来选择是水平移动还是垂直移动 publicvoidmoveBy(intdeltaX,intdeltaY){ if(BuildConfig.DEBUG) Log.d("moveBy","deltaX:"+deltaX+"deltaY:"+deltaY); if(Math.abs(deltaY)>=Math.abs(deltaX)){ intmScrollY=getScrollY(); if(mScrollY<=0){ scrollTo(0,0); }elseif(mScrollY>=getNScrollYButtom()){ scrollTo(0,getNScrollYButtom()); }else{ scrollBy(0,deltaY); } if(isEnablePullDown&&deltaY>0&&mScrollY>=pullDownMin){ isPull=true; Log.d("onlayout","isPull:true"); } } } privatevoidcompleteMove(floatvelocityX,floatvelocityY){ intmScrollY=getScrollY(); intmaxY=getScrollYButtom(); intminY=getScrollYTop(); if(mScrollY>=maxY){//如果滚动,超过了下边界,就回弹到下边界 if(isPull){//滚动到最底部 mScroller.startScroll(0,mScrollY,0,getNScrollYButtom()-mScrollY,300); invalidate(); //显示雷达 fooder_layout.showFooderRadar(); if(pullDownListem!=null){ pullDownListem.onPullDown(); } Log.d("onlayout","isPull:true滚动到最底部,显示出雷达"); }else{ mScroller.startScroll(0,mScrollY,0,maxY-mScrollY); invalidate(); Log.d("onlayout","isPull:true"); } }elseif(mScrollY<=minY){//如果滚动,超过了上边界,就回弹到上边界 //超出了上边界,弹回 mScroller.startScroll(0,mScrollY,0,minY-mScrollY); invalidate(); }elseif(Math.abs(velocityY)>=minFlingVelocity&&maxY>0){//大于1页的时候 //mScroller.fling(0,mScrollY,0,(int)(velocityY*1.2f),0,0,minY,maxY); mScroller.fling(0,mScrollY,0,(int)(velocityY*2f),0,0,getNScrollYTop(),getNScrollYButtom()); invalidate(); } } publicvoidifNeedScrollBack(){ intmScrollY=getScrollY(); intmaxY=getScrollYButtom(); intminY=getScrollYTop(); if(mScrollY>maxY){ //超出了下边界,弹回 mScroller.startScroll(0,mScrollY,0,maxY-mScrollY); invalidate(); }elseif(mScrollY<minY){ //超出了上边界,弹回 mScroller.startScroll(0,mScrollY,0,minY-mScrollY); invalidate(); } } @Override protectedvoidonScrollChanged(intl,intt,intoldl,intoldt){ super.onScrollChanged(l,t,oldl,oldt); } @Override publicvoidcomputeScroll(){ if(mScroller.computeScrollOffset()){ scrollTo(0,mScroller.getCurrY()); postInvalidate(); }else{ Log.d("onlayout","computeScroll,isMove:"+isMove+",isPull:"+isPull); if(!isMove&&!isPull){ ifNeedScrollBack(); } } } publicvoidonPullSuccess(){ soomToBack(); } publicvoidsoomToBack(){ intmScrollY=getScrollY(); intmaxY=getScrollYButtom(); Log.d("onlayout","soomToBack:(maxY-mScrollY)="+(maxY-mScrollY)+",maxY="+maxY+",mScrollY="+mScrollY); //超出了下边界,弹回 mScroller.startScroll(0,mScrollY,0,maxY-mScrollY,300); invalidate(); postDelayed(newRunnable(){ @Override publicvoidrun(){ fooder_layout.showFooderPull(); isPull=false; } },310); } privatePullDownListempullDownListem; publicvoidsetPullDownListem(PullDownListempullDownListem){ this.pullDownListem=pullDownListem; } publicinterfacePullDownListem{ publicvoidonPullDown(); } } 本文转自莫水千流博客园博客,原文链接:http://www.cnblogs.com/zhoug2020/p/6076194.html,如需转载请自行联系原作者