Android下拉刷新控件SwipeRefreshLayout源码解析
swiperefreshlayout是android官方的下拉刷新控件,使用简单,界面美观,不熟悉的朋友可以随便搜索了解一下,这里就不废话了,直接进入正题。
首先给张流程图吧,标出了几个主要方法的作用,可以结合着看一下哈。
这种下拉刷新控件的原理不难,基本就是监听手指的运动,获取手指的坐标,通过计算判断出是哪种操作,然后就是回调相应的接口了。swiperefreshlayout是继承自viewgroup的,根据android的事件分发机制,触摸事件应该是先传递到viewgroup,根据onintercepttouchevent的返回值决定是否拦截事件的,那么就onintercepttouchevent出发:
@override public boolean onintercepttouchevent(motionevent ev) { ensuretarget(); final int action = motioneventcompat.getactionmasked(ev); if (mreturningtostart && action == motionevent.action_down) { mreturningtostart = false; } if (!isenabled() || mreturningtostart || canchildscrollup() || mrefreshing || mnestedscrollinprogress) { // fail fast if we're not in a state where a swipe is possible return false; } switch (action) { case motionevent.action_down: settargetoffsettopandbottom(moriginaloffsettop - mcircleview.gettop(), true); mactivepointerid = motioneventcompat.getpointerid(ev, 0); misbeingdragged = false; final float initialdowny = getmotioneventy(ev, mactivepointerid); if (initialdowny == -1) { return false; } minitialdowny = initialdowny; break; case motionevent.action_move: if (mactivepointerid == invalid_pointer) { log.e(log_tag, "got action_move event but don't have an active pointer id."); return false; } final float y = getmotioneventy(ev, mactivepointerid); if (y == -1) { return false; } final float ydiff = y - minitialdowny; if (ydiff > mtouchslop && !misbeingdragged) { minitialmotiony = minitialdowny + mtouchslop; misbeingdragged = true; mprogress.setalpha(starting_progress_alpha); } break; case motioneventcompat.action_pointer_up: onsecondarypointerup(ev); break; case motionevent.action_up: case motionevent.action_cancel: misbeingdragged = false; mactivepointerid = invalid_pointer; break; } return misbeingdragged; }
是否拦截的情况有很多种,这里如果满足五个条件之一就直接返回false,使用时触摸事件发生冲突的话就可以从这里出发分析,这里也不具体展开了。简单看一下,在action_down中记录下手指坐标,action_move中计算出移动的距离,并且判断是否大于阈值,是的话就将misbeingdragged标志位设为true,action_up中则将misbeingdragged设为false。最后返回的是misbeingdragged。
swiperefreshlayout一般是嵌套可滚动的view使用的,正常滚动时会满足前面的条件,这时不进行拦截,只有当滚动到顶部才会进入后面action的判断。在手指按下和抬起期间misbeingdragged为true,也就是说进行拦截,接下来就是如何处理了,看看ontouchevent:
@override public boolean ontouchevent(motionevent ev) { .... switch (action) { case motionevent.action_down: mactivepointerid = motioneventcompat.getpointerid(ev, 0); misbeingdragged = false; break; case motionevent.action_move: { pointerindex = motioneventcompat.findpointerindex(ev, mactivepointerid); if (pointerindex < 0) { log.e(log_tag, "got action_move event but have an invalid active pointer id."); return false; } final float y = motioneventcompat.gety(ev, pointerindex); final float overscrolltop = (y - minitialmotiony) * drag_rate; if (misbeingdragged) { if (overscrolltop > 0) { movespinner(overscrolltop); } else { return false; } } break; } .... case motionevent.action_up: { pointerindex = motioneventcompat.findpointerindex(ev, mactivepointerid); if (pointerindex < 0) { log.e(log_tag, "got action_up event but don't have an active pointer id."); return false; } final float y = motioneventcompat.gety(ev, pointerindex); final float overscrolltop = (y - minitialmotiony) * drag_rate; misbeingdragged = false; finishspinner(overscrolltop); mactivepointerid = invalid_pointer; return false; } case motionevent.action_cancel: return false; } return true; }
这里省略了一些代码,前面还有几行跟上面的类似,也是在满足其中一个条件时直接返回;switch中也还有几行处理多指触控的,这些都略过了。看一下action_move中计算了手指移动的距离,这时的misbeingdragged正常情况下应为true,当距离大于零就会执行movespinner。在action_up中则会执行finishspinner,到这里就可以猜出,执行刷新的逻辑主要就在这两个方法中。
看这两个方法前,要知道两个重要的成员变量:一个是mcircleview,是circleimageview的实例,继承了imageview,主要绘制进度圈的背景;另一个是mprogress,是materialprogressdrawable的实例,继承自drawable且实现animatable接口,主要绘制进度圈,swiperefreshlayout正是通过调用其方法来绘制动画。接下来就先看一下movespinner:
<span style="font-size:18px;">private void movespinner(float overscrolltop) { mprogress.showarrow(true); float originaldragpercent = overscrolltop / mtotaldragdistance; float dragpercent = math.min(1f, math.abs(originaldragpercent)); float adjustedpercent = (float) math.max(dragpercent - .4, 0) * 5 / 3; float extraos = math.abs(overscrolltop) - mtotaldragdistance; float slingshotdist = musingcustomstart ? mspinnerfinaloffset - moriginaloffsettop : mspinnerfinaloffset; float tensionslingshotpercent = math.max(0, math.min(extraos, slingshotdist * 2) / slingshotdist); float tensionpercent = (float) ((tensionslingshotpercent / 4) - math.pow( (tensionslingshotpercent / 4), 2)) * 2f; float extramove = (slingshotdist) * tensionpercent * 2; int targety = moriginaloffsettop + (int) ((slingshotdist * dragpercent) + extramove); // where 1.0f is a full circle if (mcircleview.getvisibility() != view.visible) { mcircleview.setvisibility(view.visible); } if (!mscale) { viewcompat.setscalex(mcircleview, 1f); viewcompat.setscaley(mcircleview, 1f); } if (mscale) { setanimationprogress(math.min(1f, overscrolltop / mtotaldragdistance)); } if (overscrolltop < mtotaldragdistance) { if (mprogress.getalpha() > starting_progress_alpha && !isanimationrunning(malphastartanimation)) { // animate the alpha startprogressalphastartanimation(); } } else { if (mprogress.getalpha() < max_alpha && !isanimationrunning(malphamaxanimation)) { // animate the alpha startprogressalphamaxanimation(); } } float strokestart = adjustedpercent * .8f; mprogress.setstartendtrim(0f, math.min(max_progress_angle, strokestart)); mprogress.setarrowscale(math.min(1f, adjustedpercent)); float rotation = (-0.25f + .4f * adjustedpercent + tensionpercent * 2) * .5f; mprogress.setprogressrotation(rotation); settargetoffsettopandbottom(targety - mcurrenttargetoffsettop, true /* requires update */); }</span>
showarrow是显示箭头,中间那一坨主要也是一些math和设置进度圈的样式,倒数第二行执行了setprogressrotation,传入的是经过一堆计算后的rotation,这堆计算主要是优化效果,比如在刚开始移动时增长比较快,超过刷新的距离后就增长比较慢。传入该方法后,mprogress就根据它来绘制进度圈,因此主要的动画就应该在这个方法内。最后一行执行settargetoffsettopandbottom,我们来看一下:
<span style="font-size:18px;">private void settargetoffsettopandbottom(int offset, boolean requiresupdate) { mcircleview.bringtofront(); mcircleview.offsettopandbottom(offset); mcurrenttargetoffsettop = mcircleview.gettop(); if (requiresupdate && android.os.build.version.sdk_int < 11) { invalidate(); } }</span>
比较简单,就是调整进度圈的位置并进行记录。最后来看一下finishspinner:
<span style="font-size:18px;">private void finishspinner(float overscrolltop) { if (overscrolltop > mtotaldragdistance) { setrefreshing(true, true /* notify */); } else { // cancel refresh mrefreshing = false; mprogress.setstartendtrim(0f, 0f); animation.animationlistener listener = null; if (!mscale) { listener = new animation.animationlistener() { @override public void onanimationstart(animation animation) { } @override public void onanimationend(animation animation) { if (!mscale) { startscaledownanimation(null); } } @override public void onanimationrepeat(animation animation) { } }; } animateoffsettostartposition(mcurrenttargetoffsettop, listener); mprogress.showarrow(false); } }</span>
逻辑也很简单,当移动的距离超过设定值时就执行setrefreshing(true,true),在该方法里更新一些成员变量的值后会执行animateoffsettocorrectposition,由名字就知道是执行动画将进度圈移动到正确位置的(也就是头部)。如果移动的距离没有超过设定值,就会执行animateoffsettostartposition。一起看一下animateoffsettocorrectposition和animateoffsettostartposition这两个方法:
<span style="font-size:18px;">private void animateoffsettocorrectposition(int from, animationlistener listener) { mfrom = from; manimatetocorrectposition.reset(); manimatetocorrectposition.setduration(animate_to_trigger_duration); manimatetocorrectposition.setinterpolator(mdecelerateinterpolator); if (listener != null) { mcircleview.setanimationlistener(listener); } mcircleview.clearanimation(); mcircleview.startanimation(manimatetocorrectposition); } private void animateoffsettostartposition(int from, animationlistener listener) { if (mscale) { // scale the item back down startscaledownreturntostartanimation(from, listener); } else { mfrom = from; manimatetostartposition.reset(); manimatetostartposition.setduration(animate_to_start_duration); manimatetostartposition.setinterpolator(mdecelerateinterpolator); if (listener != null) { mcircleview.setanimationlistener(listener); } mcircleview.clearanimation(); mcircleview.startanimation(manimatetostartposition); } }</span>
逻辑基本相同,进行一些设置后,最后都会执行mcircleview的startanimation,只是传入的值以及监听器不同。
如果是要执行刷新的操作,传入的值是头部高度,监听器为:
<span style="font-size:18px;">private animation.animationlistener mrefreshlistener = new animation.animationlistener() { @override public void onanimationstart(animation animation) { } @override public void onanimationrepeat(animation animation) { } @override public void onanimationend(animation animation) { if (mrefreshing) { // make sure the progress view is fully visible mprogress.setalpha(max_alpha); mprogress.start(); if (mnotify) { if (mlistener != null) { mlistener.onrefresh(); } } mcurrenttargetoffsettop = mcircleview.gettop(); } else { reset(); } } };</span>
动画完成后,也就是进度圈移动到头部后,会执行mprogress.start();这里执行的就是在刷新时进度圈转啊转的动画。接下来注意到如果mlistener不为空就会执行onrefresh方法,这个mlistener其实就是执行setonrefreshlistener所设置的监听器,因此在这里完成刷新。如果是执行回到初始位置的操作,传入的值为初始高度(也就是顶部之上),监听器为
<span style="font-size:18px;">listener = new animation.animationlistener() { @override public void onanimationstart(animation animation) { } @override public void onanimationend(animation animation) { if (!mscale) { startscaledownanimation(null); } } @override public void onanimationrepeat(animation animation) { } };</span>
移动到初始位置后会执行startscaledownanimation,也就是消失的动画了,到这里整个刷新流程就结束了。
这样就基本把swiperefreshlayout的流程过了一遍,但是要实现这样一个控件还是有很多小问题需要考虑的,这里主要是把思路理清,知道如果出现问题该怎样解决。另外从源码也可以看出swiperefreshlayout的定制性是比较差的,也不知道google是不是故意这样希望以后全都用这种统一样式的下拉刷新。。当然有一些第三方下拉刷新的定制性还是比较好的,使用上也不难。但是有些人(比如我)是比较倾向于使用官方的控件的,不到万不得已都不想用第三方工具。下次会写一篇探讨一下用swiperefreshlayout实现自定义样式的文章~
后续还有一篇从修改swiperefreshlayout的源码出发自定义样式高仿微信朋友圈的下拉刷新效果的文章,有兴趣可以看一下哈
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
Android下拉刷新控件SwipeRefreshLayout源码解析
-
Android实现支持所有View的通用的下拉刷新控件
-
Android自定义控件下拉刷新实例代码
-
Android下拉刷新SwipeRefreshLayout控件使用方法
-
Android SwipeRefreshLayout下拉刷新源码解析
-
Android实现支持所有View的通用的下拉刷新控件
-
Android控件RefreshableView实现下拉刷新
-
Android自定义下拉刷新控件RefreshableView
-
Android下拉刷新SwipeRefreshLayout控件使用方法
-
Android SwipeRefreshLayout下拉刷新源码解析