欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-03-06 12:23:43
swiperefreshlayout是android官方的下拉刷新控件,使用简单,界面美观,不熟悉的朋友可以随便搜索了解一下,这里就不废话了,直接进入正题。 ...

swiperefreshlayout是android官方的下拉刷新控件,使用简单,界面美观,不熟悉的朋友可以随便搜索了解一下,这里就不废话了,直接进入正题。 

首先给张流程图吧,标出了几个主要方法的作用,可以结合着看一下哈。

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

这种下拉刷新控件的原理不难,基本就是监听手指的运动,获取手指的坐标,通过计算判断出是哪种操作,然后就是回调相应的接口了。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的源码出发自定义样式高仿微信朋友圈的下拉刷新效果的文章,有兴趣可以看一下哈

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