当前位置: 移动技术网 > IT编程>移动开发>Android > Android下拉刷新控件SwipeRefreshLayout源码解析

Android下拉刷新控件SwipeRefreshLayout源码解析

2019年07月24日  | 移动技术网IT编程  | 我要评论

榆次消费你我他,红枣的吃法,成功学演讲

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的源码出发自定义样式高仿微信朋友圈的下拉刷新效果的文章,有兴趣可以看一下哈

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网