轻量刷新库EasyPullLayout源码解读

项目由于历史原因,很多同事可能基于当时的需求引入了众多不同的下拉刷新库。这就造成项目中下拉刷新控件的繁杂混乱,而且后期维护也变得复杂困难。那么,统一控件自然成了亟待解决的问题。而我期望的下拉刷新库是小巧轻量,只提供基本的功能。但又开放接口,易于扩展。偶然逛Gay网,看到了一个刷新库,本篇文章是对该库的源码解析。

介绍

一个支持横向纵向,侵入非侵入,自定义刷新头、刷新尾以及包裹任意刷新内容(ListViewRecyclerViewViewPager等等)的类库。最关键的是它是一个非常轻量级的类库!整个类库只有一个文件,代码不到500行。

项目地址

用法

  1. 布局

    • 在需要刷新的地方用EasyPullLayout包裹起来(例如根布局),并为EasyPullLayout下的子View声明layout_type属性,使得子View可以被EasyPullLayout识别,分别可以为content(必选)、edge_topedge_bottomedge_leftedge_right:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      <com.hzn.lib.EasyPullLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      android:id="@+id/epl"
      android:layout_width="match_parent"
      android:layout_height="match_parent">

      <!--刷新头 由edge_top指定-->
      <com.hzn.easypulllayout.TransformerView
      android:id="@+id/topView"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      app:layout_type="edge_top" />

      <android.support.v7.widget.RecyclerView
      android:id="@+id/rv"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:layout_type="content"
      tools:listitem="@layout/item"/>

      </com.hzn.lib.EasyPullLayout>
    • EasyPullLayout提供的布局属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      <declare-styleable name="EasyPullLayoutJ">
      <!--左侧边缘视图触发刷新的拖拽距离 默认为左侧边缘试图的宽度的一半-->
      <attr name="trigger_offset_left" format="dimension" />
      <attr name="trigger_offset_top" format="dimension" />
      <attr name="trigger_offset_right" format="dimension" />
      <attr name="trigger_offset_bottom" format="dimension" />
      <!--左侧边缘试图能够拖拽的最大距离 默认为左侧边缘试图的宽度-->
      <attr name="max_offset_left" format="dimension" />
      <attr name="max_offset_top" format="dimension" />
      <attr name="max_offset_right" format="dimension" />
      <attr name="max_offset_bottom" format="dimension" />
      <!--左侧边缘视图是否固定 刷新时类似于swipeRefreshLayout的样式 侵入式效果 默认false-->
      <attr name="fixed_content_left" format="boolean" />
      <attr name="fixed_content_top" format="boolean" />
      <attr name="fixed_content_right" format="boolean" />
      <attr name="fixed_content_bottom" format="boolean" />
      <!--刷新试图恢复原位动画的时间 默认300ms-->
      <attr name="roll_back_duration" format="integer" />
      <!--自动刷新拖拽动画的时间 默认300ms-->
      <attr name="auto_refresh_rolling_duration" format="integer" />
      <!--拖拽的阻尼系数? 系数越大,拖拽的难度越大 默认0.66-->
      <attr name="sticky_factor" format="float" />
      </declare-styleable>
    • EasyPullLayout提供的布局参数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <declare-styleable name="EasyPullLayout_LayoutParams">
      <attr name="layout_type" format="enum">
      <!--声明是左侧的刷新头布局-->
      <enum name="edge_left" value="0" />
      <enum name="edge_top" value="1" />
      <enum name="edge_right" value="2" />
      <enum name="edge_bottom" value="3" />
      <!--声明要包裹的刷新布局 这个必须提供 以上可以不提供-->
      <enum name="content" value="4" />
      </attr>
      <attr name="layout_content_fixed" format="boolean" />
      </declare-styleable>
  2. 监听

    EasyPullLayout的设计遵从单一职责原则,只负责处理拉拽相关的操作,其他的均交给外部进行处理,因此其子View可以是任何一种View。

    EasyPullLayout有2个监听可以设置,分别为:

    • OnPullListenerAdapter:用于EasyPullLatout对外通知当前拉拽的一些参数(例如拉拽进度),我们可以利用这些参数来改变我们的边缘视图的行为。一般用这个就足够了

      1
      2
      3
      4
      5
      6
      7
      8
      public abstract static class OnPullListenerAdapter {
      //拖拽进度回调
      public void onPull(int type, float fraction, boolean changed) {
      }
      //刷新边缘视图恢复初始位置的回调
      public void onRollBack(int rollBackType) {
      }
      }
    • OnEdgeListener: 用于通知EasyPullLayout当前是否到达边缘,到达边缘后EasyPullLayout会拦截触摸事件,开始拖拽行为,默认会自动监听layout_typecontent的子View是否到达边缘。一般不用自定义

      1
      2
      3
      public interface OnEdgeListener {
      int onEdge();
      }
    • 具体使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      easyPullLayout.addOnPullListenerAdapter(new EasyPullLayoutJ.OnPullListenerAdapter() {
      @Override
      public void onPull(int type, float fraction, boolean changed) {
      //是否经过触发位置 使用该参数可以只在经过触发位置时更新
      if (!changed)
      return;
      //只有刷新头 表明是下拉刷新
      if (type == EasyPullLayoutJ.TYPE_EDGE_TOP) {
      //进度为1,说明需要刷新头要准备刷新否则就是空闲状态
      if (fraction == 1f)
      topRefreshView.ready();
      else
      topRefreshView.idle();
      }
      }
      @Override
      public void onTriggered(int type) {
      //下拉刷新 此时表明已经触发了刷新,刷新头要显示为刷新状态
      if (type == EasyPullLayoutJ.TYPE_EDGE_TOP) {
      topRefreshView.triggered();
      doRefresh();
      }
      });

      private void doRefresh() {
      //耗时操作 如加载网络数据
      easyPullLayout.postDelayed(new Runnable() {
      @Override
      public void run() {
      // 加载完成后必须要调用该方法,让easyPullLayout重置状态
      easyPullLayout.stop();
      }
      }, 2000);
      }

      //如果有强制自动刷新的需求,调用这个
      easyPullLayout.autoRefresh(EasyPullLayoutJ.TYPE_EDGE_TOP);

源码解析

  • 大致原理:类似于SwipeRefreshLayout

    1. 首先为子View提供layout_type属性,用以区分刷新内容View及刷新头View,刷新尾View
    2. 确定如何摆放这些子view
    3. 处理触摸事件
  • 源码

    1. 构造方法

      初始化上面的布局属性。

    2. 确定布局参数

      自定义ViewGroup如果有一些自定义控制布局的属性设置(包括margin),就会通过继承View.MarginParams来扩展布局设置。然后重写generateLayoutParams方法,这样系统框架就会自动使用该布局读取在xml中配置的布局属性来控制我们的VIew的位置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      //生成布局参数时会调用该方法
      @Override
      protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
      return null != p && p instanceof LayoutParams;
      }

      //通过addView(View)添加会调用这个方法
      @Override
      protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
      return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
      }

      //布局文件中设置了属性,在初始化子控件时,会调用该方法来为子控件生成对应的布局属性,
      @Override
      public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
      return new LayoutParams(getContext(), attrs);
      }
      //
      @Override
      protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
      return new LayoutParams(p);
      }

      //默认只是生成layout_width和layout_height所以对应的布局参数,
      public class LayoutParams extends MarginLayoutParams {
      int type = TYPE_NONE;

      public LayoutParams(Context c, AttributeSet attrs) {
      super(c, attrs);
      TypedArray a = c.getTheme().obtainStyledAttributes(attrs, R.styleable.EasyPullLayout_LayoutParams, 0, 0);
      //在内部获取type值并保存起来
      type = a.getInt(R.styleable.EasyPullLayout_LayoutParams_layout_type, TYPE_NONE);
      a.recycle();
      }

      public LayoutParams(int width, int height) {
      super(width, height);
      }

      public LayoutParams(MarginLayoutParams source) {
      super(source);
      }

      public LayoutParams(ViewGroup.LayoutParams source) {
      super(source);
      }
      }
    3. 摆放子View

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      //完成xml解析后调用
      @Override
      protected void onFinishInflate() {
      super.onFinishInflate();
      int i = 0;
      final int childCount = getChildCount();
      //遍历子View
      while (i < childCount) {
      View child = getChildAt(i++);
      LayoutParams lp = (LayoutParams) child.getLayoutParams();
      //获取到子View后用一个HashMap来存储,key对应View,value对应View的一些参数,
      //类型不能重复!!
      if (null != getByType(childViews, lp.type))
      throw new IllegalArgumentException("Each child type can only be defined once!");
      else
      childViews.put(child, new ChildViewAttr());
      }
      //确保主内容View必须存在
      final View contentView = getByType(childViews, TYPE_CONTENT);
      if (null == contentView)
      throw new IllegalArgumentException("Child type \"content\" must be defined!");
      //设置默认的触发临界的监听
      setOnEdgeListener(new OnEdgeListener() {
      @Override
      public int onEdge() {
      //左侧边缘视图并且手指不能向右滑动(即内容布局不能向左滑动) 表明左侧视图到边缘了
      //下面几个同理
      if (null != getByType(childViews, TYPE_EDGE_LEFT) && !contentView.canScrollHorizontally(-1))
      return TYPE_EDGE_LEFT;
      else if (null != getByType(childViews, TYPE_EDGE_RIGHT) && !contentView.canScrollHorizontally(1))
      return TYPE_EDGE_RIGHT;
      //顶部边缘视图不能向上滑动了,意思就是类似listview已经滚动到顶部了
      else if (null != getByType(childViews, TYPE_EDGE_TOP) && !contentView.canScrollVertically(-1))
      return TYPE_EDGE_TOP;
      else if (null != getByType(childViews, TYPE_EDGE_BOTTOM) && !contentView.canScrollVertically(1))
      return TYPE_EDGE_BOTTOM;
      else
      return TYPE_NONE;
      }
      });
      }

      接着就是自定义ViewGroup的常见的流程 测量 摆放

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      //测量
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      //从保存的子View集合中遍历子View,
      for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
      View childView = entry.getKey();
      ChildViewAttr childViewAttr = entry.getValue();
      //对每个子View进行测量 这里因为继承了MarginLayoutParams,把margin及padding 也作为子视图大小的一部分
      easureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
      //记录下边缘视图的一些参数,以及根据这些参数初始化EasyPullLayout自身的一些参数:
      //把子View的size值记录下来,在摆放子View时会用到
      LayoutParams lp = (LayoutParams) childView.getLayoutParams();
      switch (lp.type) {
      case TYPE_EDGE_LEFT:
      case TYPE_EDGE_RIGHT:
      //横向size就是宽度加左右margin
      childViewAttr.size = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
      //初始化EasyPullLayout的属性
      //如果trigger_offset_left未在xml指定,在这里初始化 设为左侧边缘试图宽度的一半
      trigger_offset_left = trigger_offset_left < 0 ? childViewAttr.size / 2 : trigger_offset_left;
      trigger_offset_right = trigger_offset_right < 0 ? childViewAttr.size / 2 : trigger_offset_right;
      //未指定则为左侧边缘试图的宽度
      max_offset_left = max_offset_left < 0 ? childViewAttr.size : max_offset_left;
      max_offset_right = max_offset_right < 0 ? childViewAttr.size : max_offset_right;
      break;
      case TYPE_EDGE_TOP:
      case TYPE_EDGE_BOTTOM:
      // 纵向size就是高度加上下margin
      childViewAttr.size = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
      trigger_offset_top = trigger_offset_top < 0 ? childViewAttr.size / 2 : trigger_offset_top;
      trigger_offset_bottom = trigger_offset_bottom < 0 ? childViewAttr.size / 2 : trigger_offset_bottom;
      max_offset_top = max_offset_top < 0 ? childViewAttr.size : max_offset_top;
      max_offset_bottom = max_offset_bottom < 0 ? childViewAttr.size : max_offset_bottom;
      break;
      }
      }
      }

      //摆放
      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
      View contentView = getByType(childViews, TYPE_CONTENT);
      //contentView不能为空!!!
      if (null == contentView)
      throw new IllegalArgumentException("EasyPullLayout must have and only have one layout_type \"content\"!");
      //获取内容布局的宽高
      int contentWidth = contentView.getMeasuredWidth();
      int contentHeight = contentView.getMeasuredHeight();
      //遍历子View
      for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
      //首先计算出子View的位置,此时都还未偏移,左上角位于(0,0) left = 0, top = 0
      View childView = entry.getKey();
      ChildViewAttr childViewAttr = entry.getValue();
      LayoutParams lp = (LayoutParams) childView.getLayoutParams();
      int left = getPaddingLeft() + lp.leftMargin;
      int top = getPaddingTop() + lp.topMargin;
      int right = left + childView.getMeasuredWidth();
      int bottom = top + childView.getMeasuredHeight();
      //将view摆放在合适的位置
      switch (lp.type) {
      case TYPE_EDGE_LEFT:
      //左侧刷新view向左移动-childViewAttr.size,刚好隐藏view
      //以下同理
      left -= childViewAttr.size;
      right -= childViewAttr.size;
      break;
      case TYPE_EDGE_TOP:
      top -= childViewAttr.size;
      bottom -= childViewAttr.size;
      break;
      case TYPE_EDGE_RIGHT:
      left += contentWidth;
      right += contentWidth;
      break;
      case TYPE_EDGE_BOTTOM:
      top += contentHeight;
      bottom += contentHeight;
      break;
      }
      //将iew的当前摆放位置记录下来,因为EasyPullLayout是通过改变View的x和y属性来达到位移效果的, 因此需要参考子View的初始位置
      //并且可以不通过onLayout来重置位置,避免回调onLayout。
      childViewAttr.setBounds(left, top, right, bottom);
      childView.layout(left, top, right, bottom);

      }
    4. 处理触摸事件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      //触摸事件拦截处理
      @Override
      public boolean onInterceptTouchEvent(MotionEvent ev) {
      //闲置状态不做任何处理
      if (currentState != STATE_IDLE)
      return false;

      switch (ev.getAction()) {
      //手指按下的时候记录当前位置
      case MotionEvent.ACTION_DOWN:
      downX = ev.getX();
      downY = ev.getY();
      break;
      case MotionEvent.ACTION_MOVE:
      //回掉onEdge,判断当前触发边缘的位置
      //把是否进行拦截的判断操作交给了外部进行处理,只要返回正确的类型,则开始对触摸事件进行拦截
      int type = onEdgeListener.onEdge();
      //计算x y偏移量 deltaX deltaY
      float dx = ev.getX() - downX;
      float dy = ev.getY() - downY;
      currentType = type;
      //如果当前是左侧边缘View到了边缘,并且允许向左滑动,横向滑动距离大于纵向滑动距离开始拦截
      if (type == TYPE_EDGE_LEFT && (pullTypeMask & PULL_TYPE_LEFT) != 0)
      return ev.getX() > downX && Math.abs(dx) > Math.abs(dy);
      else if (type == TYPE_EDGE_RIGHT && (pullTypeMask & TYPE_EDGE_RIGHT) != 0)
      return ev.getX() < downX && Math.abs(dx) > Math.abs(dy);
      else if (type == TYPE_EDGE_TOP && (pullTypeMask & TYPE_EDGE_TOP) != 0)
      return ev.getY() > downY && Math.abs(dy) > Math.abs(dx);
      else if (type == TYPE_EDGE_BOTTOM && (pullTypeMask & TYPE_EDGE_BOTTOM) != 0)
      return ev.getY() < downY && Math.abs(dy) > Math.abs(dx);
      else
      return false;
      }
      return false;
      }

      //拦截了触摸事件后,由EasyPullLayout自身进行消费处理
      @Override
      public boolean onTouchEvent(MotionEvent event) {
      //闲置状态不做任何处理
      if (currentState != STATE_IDLE)
      return false;
      // 检查滑动到边缘视图类型
      if (currentType == TYPE_EDGE_LEFT && (pullTypeMask & PULL_TYPE_LEFT) == 0 ||
      currentType == TYPE_EDGE_TOP && (pullTypeMask & PULL_TYPE_TOP) == 0 ||
      currentType == TYPE_EDGE_RIGHT && (pullTypeMask & PULL_TYPE_RIGHT) == 0 ||
      currentType == TYPE_EDGE_BOTTOM && (pullTypeMask & PULL_TYPE_BOTTOM) == 0)
      return false;
      //因为自己要处理,所以一定要请求父view不要拦截
      getParent().requestDisallowInterceptTouchEvent(true);

      switch (event.getAction()) {
      case MotionEvent.ACTION_MOVE:
      float x = event.getX();
      float y = event.getY();
      //计算x,y 偏移量 = 滑动的距离 * (1- 阻尼系数)
      //从这里可以得出阻尼系数与滑动的距离有反比例关系
      //阻尼系数可以使拖拽时有黏着效果
      offsetX = (x - downX) * (1 - sticky_factor * 0.75f);
      offsetY = (y - downY) * (1 - sticky_factor * 0.75f);
      //对必要的子View进行偏移(设置了对应的fixed选项后,content不会进行偏移,达到侵入式效果)
      move();
      break;
      //手指释放时,通过ValueAnimator,在一段时间内将子View还原 达到缓缓还原的效果
      //还原后的位置分2种情况
      //1还没超过触发偏移量,则还原回到初始位置
      //2已经超过了触发偏移量,则回到触发偏移量的位置(即刷新位置) 再经由外部调用Stop方法,再次将位置还原的初始位置
      case MotionEvent.ACTION_CANCEL:
      case MotionEvent.ACTION_UP:
      //更改状态为滑动中
      currentState = STATE_ROLLING;
      switch (currentType) {
      case TYPE_EDGE_LEFT:
      case TYPE_EDGE_RIGHT:
      rollBackHorizontal();
      break;
      case TYPE_EDGE_TOP:
      case TYPE_EDGE_BOTTOM:
      rollBackVertical();
      break;
      }
      break;
      }
      return true;
      }

      private void move() {
      //定义当前拖拽的进度
      float pullFraction = 0f;

      // 根据max_offset_left 限制拖拽距离 计算当前拖拽完成的百分比
      switch (currentType) {
      case TYPE_EDGE_LEFT:
      //限制x偏移量的边界值
      offsetX = offsetX < 0 ? 0f : offsetX > max_offset_left ? max_offset_left : offsetX;
      //计算当前拖拽的进度 同时也限制了边界 进度 = 当前的x偏移量 / 触发刷新位置的x值
      pullFraction = offsetX == 0f ? 0f : trigger_offset_left > offsetX ? offsetX / trigger_offset_left : 1f;
      break;
      case TYPE_EDGE_RIGHT:
      offsetX = offsetX > 0 ? 0f : offsetX < -max_offset_right ? -max_offset_right : offsetX;
      pullFraction = offsetX == 0f ? 0f : -trigger_offset_right < offsetX ? offsetX / -trigger_offset_right : 1f;
      break;
      case TYPE_EDGE_TOP:
      offsetY = offsetY < 0 ? 0f : offsetY > max_offset_top ? max_offset_top : offsetY;
      pullFraction = offsetY == 0f ? 0f : trigger_offset_top > offsetY ? offsetY / trigger_offset_top : 1f;
      break;
      case TYPE_EDGE_BOTTOM:
      offsetY = offsetY > 0 ? 0f : offsetY < -max_offset_bottom ? -max_offset_bottom : offsetY;
      pullFraction = offsetY == 0f ? 0f : -trigger_offset_bottom < offsetY ? offsetY / -trigger_offset_bottom : 1f;
      break;
      }
      //是否经过触发位置
      //changed为true时,两者不能同时为1 也不能同时小于1
      //其实就是为了保证上下滑动只在经过触发位置时更新
      boolean changed = !(lastPullFraction < 1f && pullFraction < 1f || lastPullFraction == 1f && pullFraction == 1f);
      //将当前的滑动边缘类型,及滑动进度等相关信息回调
      onPullListenerAdapter.onPull(currentType, pullFraction, changed);
      //记录上次的滑动进度
      lastPullFraction = pullFraction;

      //遍历子View,根据拖拽的距离摆放子View
      //横向的就是设置X坐标值
      //纵向的就是设置Y坐标值
      // 如果设置了对应的fixed,且为content,则不偏移
      for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
      View childView = entry.getKey();
      ChildViewAttr childViewAttr = entry.getValue();
      if (currentType == TYPE_EDGE_LEFT &&
      (((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_left)) {
      childView.setX(childViewAttr.left + offsetX);
      } else if (currentType == TYPE_EDGE_RIGHT &&
      (((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_right)) {
      childView.setX(childViewAttr.left + offsetX);
      } else if (currentType == TYPE_EDGE_TOP &&
      ((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_top) {
      childView.setY(childViewAttr.top + offsetY);
      } else if (currentType == TYPE_EDGE_BOTTOM &&
      (((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_bottom)) {
      childView.setY(childViewAttr.top + offsetY);
      }
      }
      }

      //这里只看下纵向的 横向的原理一样
      private void rollBackVertical() {
      //要还原的偏移量
      //下拉 offset是正值
      //1 trigger_offset_bottom < offsetY <= trigger_offset_top即未拉到触发位置的,还原的偏移量就是下拉的偏移量offsetY
      //2 offsetY > trigger_offset_top即超过触发位置的,还原偏移量是超过触发位置的偏移量 offsetY - trigger_offset_top
      //上拉 offset是负值
      //1 offsetY >= -trigger_offset_bottom 即上拉未拉到触发位置的,还原的偏移量就是上拉的偏移量offsetY
      //2 offsetY < -trigger_offset_bottom 即上拉超过触发位置的,还原偏移量是超过触发位置的偏移量
      //offsetY(负值) + trigger_offset_bottom(正值)
      final float rollBackOffset =
      offsetY > trigger_offset_top ? offsetY - trigger_offset_top
      : offsetY < -trigger_offset_bottom ? offsetY + trigger_offset_bottom
      : offsetY;
      Log.e("roll", "offsetY: " + offsetY + "-->trigger_offset_top: " + trigger_offset_top +
      "--> -trigger_offset_bottom: " + (-trigger_offset_bottom) + "--rollBackOffset: " + rollBackOffset);
      //触发刷新位置的偏移量 如果尚未拉到触发位置的,则为0
      //超过触发位置的, 判断当前视图边缘位置,是下拉刷新,则未trigger_offset_top
      //上拉加载的则未-trigger_offset_bottom,否则未0
      final float triggerOffset =
      rollBackOffset != offsetY ?
      currentType == TYPE_EDGE_TOP ? trigger_offset_top :
      currentType == TYPE_EDGE_BOTTOM ? -trigger_offset_bottom :
      0
      : 0;
      Log.e("roll", "triggerOffset: " + triggerOffset);
      // 通过ValueAnimator,在一段时间内将子View还原 达到缓缓还原 动画,值从1->0
      verticalAnimator = ValueAnimator.ofFloat(1f, 0f);
      //设置动画时间
      verticalAnimator.setDuration(roll_back_duration)
      .setInterpolator(new DecelerateInterpolator());
      //动画更新监听
      verticalAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
      switch (currentType) {
      //当前是下拉刷新 遍历子View
      case TYPE_EDGE_TOP:
      for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
      View childView = entry.getKey();
      ChildViewAttr childViewAttr = entry.getValue();
      //为content且设置了fixed 不偏移 否则通过rollBackOffset和triggerOffset,以及animatedValue计算得出y
      if (((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_top)
      childView.setY(childViewAttr.top + triggerOffset + rollBackOffset *
      (float) animation.getAnimatedValue());
      }
      break;

      case TYPE_EDGE_BOTTOM:
      for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
      View childView = entry.getKey();
      ChildViewAttr childViewAttr = entry.getValue();
      if (((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_bottom)
      childView.setY(childViewAttr.top + triggerOffset + rollBackOffset *
      (float) animation.getAnimatedValue());
      }
      break;
      }
      }
      });
      // 动画结束后,还原一些参数,回调监听
      verticalAnimator.addListener(new AnimatorListenerAdapter() {
      @Override
      public void onAnimationEnd(Animator animation) {
      //此时是超过触发位置的,动画将之还原到触发位置
      if (triggerOffset != 0 && currentState == STATE_ROLLING) {
      //更改状态为触发状态
      currentState = STATE_TRIGGERING;
      //记录此时的偏移量= triggerOffset 因为在外部刷新后调用stop()方法还需要再次还原到初始位置
      offsetY = triggerOffset;
      //回调触发刷新的监听
      onPullListenerAdapter.onTriggered(currentType);
      } else {
      //更改状态为空闲状态
      currentState = STATE_IDLE;
      offsetY = 0f;
      //回调通知
      rollBackEnd();
      }
      }
      });
      verticalAnimator.start();
      }

      //在刷新完由外部调用,再次将子View位置由触发位置还原到初始位置
      public void stop() {
      switch (currentType) {
      case TYPE_EDGE_LEFT:
      case TYPE_EDGE_RIGHT:
      rollBackHorizontal();
      break;
      case TYPE_EDGE_TOP:
      case TYPE_EDGE_BOTTOM:
      rollBackVertical();
      break;
      }
      }
    5. 自动刷新

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      //自动更新
      //根据要更新的刷新类型计算出要自动滑动的距离,通过ValueAnimator不断更新子View的X或者Y属性,达到缓缓平移的效果
      //动画结束后,再次通过ValueAnimator还原到刷新位置
      public void autoRefresh(int typeEdge) {
      //检查状态
      if (currentState != STATE_IDLE) return;
      //检查类型
      if (typeEdge != TYPE_EDGE_LEFT &&
      typeEdge != TYPE_EDGE_TOP &&
      typeEdge != TYPE_EDGE_RIGHT &&
      typeEdge != TYPE_EDGE_BOTTOM) return;
      //更改状态为滑动
      currentState = STATE_ROLLING;
      //记录当前要刷新的类型
      currentType = typeEdge;
      //要自动滑动的距离
      float end;
      switch (currentType) {
      case TYPE_EDGE_LEFT:
      end = max_offset_left;
      break;
      case TYPE_EDGE_TOP:
      end = max_offset_top;
      break;
      case TYPE_EDGE_RIGHT:
      end = -max_offset_right;
      break;
      case TYPE_EDGE_BOTTOM:
      end = -max_offset_bottom;
      break;
      default:
      end = 0;
      break;
      }
      verticalAnimator = ValueAnimator.ofFloat(0f, end);
      //设置动画时间
      verticalAnimator.setDuration(auto_refresh_rolling_duration);
      verticalAnimator.setInterpolator(new DecelerateInterpolator());
      verticalAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
      //计算每次更新拖拽的偏移量
      if (currentType == TYPE_EDGE_LEFT || currentState == TYPE_EDGE_RIGHT) {
      offsetX = (float) animation.getAnimatedValue();
      } else if (currentType == TYPE_EDGE_BOTTOM || currentType == TYPE_EDGE_TOP) {
      offsetY = (float) animation.getAnimatedValue();
      }
      move();
      }
      });
      verticalAnimator.addListener(new AnimatorListenerAdapter() {
      @Override
      public void onAnimationEnd(Animator animation) {
      //动画结束时 还原位置到触发刷新位置
      if (currentType == TYPE_EDGE_LEFT || currentState == TYPE_EDGE_RIGHT) {
      rollBackHorizontal();
      } else if (currentType == TYPE_EDGE_BOTTOM || currentType == TYPE_EDGE_TOP) {
      rollBackVertical();
      }
      }
      });
      verticalAnimator.start();
      }

    至此,源码已分析完毕。