图解系列之SwipeRefreshLayout源码分析(一)

  发布日期:   2018-08-24
  最新修改:   2020-11-30
  阅读次数:   182 次

  • SwipeRefreshLayout是google官方提供的下拉刷新控制,其已非侵入式的方式运转,深受广大开发者的喜爱。

  • SwipeRefreshLayout的实现方式为二合一的方式,为什么是二合一呢?因为从兼容性方面考虑,google通过传统的方式即重写onTouchEvent方法来实现滑动的拦截处理操作,但CoordinatorLayout又是开发中常用的利器,而配合其使用需实现内部嵌套接口,所以SwipeRefreshLayout还实现了NestedScrollingChild和NestedScrollingParent接口

  • 同时,SwipeRefreshLayout还重写了onMesure和onLayout来做自己的测量和布局

  • 下面我们将通过源码结合图解梳理的方式去学习SwipeRefreshLayout的设计思想,将重点从以下几点进行分析

    • 构造方法;
    • onMeasure();
    • onLayout();
    • onIntercepterTouchEvent();(重点)
    • onTouchEvent();(重点)
    • 嵌套滑动机制(重点)
  • 该知识点将实行三步走的战略,分点分类将其消灭,共分三篇进行梳理

  • 第一篇为测量布局篇

  • 第二篇为传统实现篇

  • 第三篇为嵌套滚动实现篇


构造函数
  • 构造函数源码

    public SwipeRefreshLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
    
             //触发移动事件的最小距离,自定义View处理touch事件的时候,
             //有的时候需要判断用户是否真的存在movie,系统提供了这样的方法。
             //表示滑动的时候,手的移动要大于这个返回的距离值才开始移动控件。
    
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    
            //动画的执行时长
            mMediumAnimationDuration = getResources().getInteger(
                    android.R.integer.config_mediumAnimTime);
            //一般如果自定义view不需要在onDraw绘制自己的东西时,可以不理会
            setWillNotDraw(false);
            //获取动画的插值器
            mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
    
            //获取设备显示器的指标数据,后面计算需要
            final DisplayMetrics metrics = getResources().getDisplayMetrics();
            //刷新圆的直径,默认是40
            mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
    
            //创建一个圆形进度View,下拉时看到的那个就是了
            createProgressView();
    
            //设置其child的绘制是可以排序的,因为SwipeRefreshLayout总是将下拉的圆形最后再绘制,
            //这样就能一直保证绘制其在最上层可见了
            ViewCompat.setChildrenDrawingOrderEnabled(this, true);
    
            // the absolute offset has to take into account that the circle starts at an offset
            //下拉触发刷新的偏离距离,默认是64 也就是下拉一个半圆多一点点的距离松开手就可以触发下拉刷新了
            mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);
            //总共需要拖拽的距离等于触发的距离
            mTotalDragDistance = mSpinnerOffsetEnd;
    
            //嵌套滑动机制的辅助类
            mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    
            mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
    
            //开启嵌套滑动
            setNestedScrollingEnabled(true);
    
            //最开始的偏离值   当前的偏离值  
            mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
    
            moveToStart(1.0f);
    
            final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
            setEnabled(a.getBoolean(0, true));
            a.recycle();
        }
  • 重要字段:

    • mTotalDragDistance
    • mOriginalOffsetTop :刷新view最初的top值
    • mCurrentTargetOffsetTop :刷新view实时的top值
    • mSpinnerOffsetEnd :刷新view触发刷新的阈值

onMeasure
  • onMeasure源码

     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
            //mTarget 即为下拉的圆形view
            if (mTarget == null) {
                ensureTarget();
            }
            if (mTarget == null) {
                return;
            }
    
            //内容View,如SwipeRefreshLayout包裹着的RecyclerView
            //SwipeRefreshLayout 减去自己padding,剩下的都给mTarget,使其撑满
            mTarget.measure(MeasureSpec.makeMeasureSpec(
                    getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                    MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                    getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
    
            //刷新控件,大小直接根据给定的硬编码
            mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));
            //刷新控件在SwipeRefreshLayuot‘s  childs 中的索引值,后面draw时会用到
            mCircleViewIndex = -1;
    
            // Get the index of the circleview.
            for (int index = 0; index < getChildCount(); index++) {
                if (getChildAt(index) == mCircleView) {
                    mCircleViewIndex = index;
                    break;
                }
            }
        }   
  • 重要字段

    • mCircleViewIndex
    • mTarget
    • mCircleView

onLayout
  • onLayout源码

      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
          //获取测量到的宽度和高度
          final int width = getMeasuredWidth();
          final int height = getMeasuredHeight();
          //没有child  直接return
          if (getChildCount() == 0) {
              return;
          }
    
          if (mTarget == null) {
              ensureTarget();
          }
           //没有mTarget 直接 return
          if (mTarget == null) {
              return;
          }
    
          final View child = mTarget;
          final int childLeft = getPaddingLeft();
          final int childTop = getPaddingTop();
          final int childWidth = width - getPaddingLeft() - getPaddingRight();
          final int childHeight = height - getPaddingTop() - getPaddingBottom();
    
          //mTarget撑满SwipeRefreshLayout
          child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
    
          int circleWidth = mCircleView.getMeasuredWidth();
          int circleHeight = mCircleView.getMeasuredHeight();
    
          //一直水平居中
          mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
                  (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
      }

绘制
  • 通过重写getChildDrawingOrder去控制viewgroup绘制child的顺序,viewgroup默认从上至下

    protected int getChildDrawingOrder(int childCount, int i) {

      if (mCircleViewIndex < 0) {
          return i;
      } else if (i == childCount - 1) {
          // Draw the selected child last
          //绘制到最后一个时,返回
          return mCircleViewIndex;
      } else if (i >= mCircleViewIndex) {
          // Move the children after the selected child earlier one
          return i + 1;
      } else {
          // Keep the children before the selected child the same
          return i;
      }

    }


总结
  • 通过上面我们了解了SwipeRefreshLayout的测量布局和绘制部分都做了什么
  • 也了解到如何让下拉刷新view一直保持在最顶层让用户可见的一些策略
  • 下篇将梳理传统touch事件实现方式的部分代码

   转载规则

《图解系列之SwipeRefreshLayout源码分析(一)字》GajAngels 采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。