TwinklingRefreshLayout 源码分析

项目地址:TwinklingRefreshLayout 本文分析版本:0a9b613

1.简介

scrollview

以下是 TwinklingRefreshLayout 的官方介绍:
TwinklingRefreshLayout 延伸了 Google 的 SwipeRefreshLayout 的思想,不在列表控件上动刀,而是使用一个 ViewGroup 来包含列表控件,以保持其较低的耦合性和较高的通用性。其主要特性有:

  • 支持 RecyclerView、ScrollView、AbsListView 系列(ListView、GridView)、WebView 以及其它可以获取到 scrollY 的控件
  • 支持加载更多
  • 默认支持 越界回弹,随手势速度有不同的效果
  • 可开启没有刷新控件的纯净越界回弹模式
  • setOnRefreshListener 中拥有大量可以回调的方法
  • 将 Header 和 Footer 抽象成了接口,并回调了滑动过程中的系数,方便实现个性化的 Header 和 Footer

2.使用方法

下面以 scrollView 为例子对下拉刷新和上拉加载来做分析

1、在 XML 文件中声明

    

    
        

            .
            .
            .

        
    


在 XML 文件中只要在滑动控件 ScrollView 外嵌套 TwinklingRefreshLayout 就可以了,TwinklingRefreshLayout 包含很多自定义的属性在下面的分析中我们会看到。

2、在 JAVA 文件中设置回调
refreshLayout.setOnRefreshListener(new RefreshListenerAdapter() {
    @Override
    public void onRefresh(final TwinklingRefreshLayout refreshLayout) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                refreshLayout.finishRefreshing();
            }
        }, 4000);
    }
});

TwinklingRefreshLayout 项目中给出了一个控件状态的回调监听,上面的代码中只监听了 onRefresh 的状态,我们来看下 RefreshListenerAdapter 的实现,看看有多少状态。

public abstract class RefreshListenerAdapter implements PullListener {
        @Override
        public void onPullingDown(TwinklingRefreshLayout refreshLayout, float fraction) {
        }

        @Override
        public void onPullingUp(TwinklingRefreshLayout refreshLayout, float fraction) {
        }

        @Override
        public void onPullDownReleasing(TwinklingRefreshLayout refreshLayout, float fraction) {
        }

        @Override
        public void onPullUpReleasing(TwinklingRefreshLayout refreshLayout, float fraction) {
        }

        @Override
        public void onRefresh(TwinklingRefreshLayout refreshLayout) {
        }

        @Override
        public void onLoadMore(TwinklingRefreshLayout refreshLayout) {
        }

        @Override
        public void onFinishRefresh() {
        }

        @Override
        public void onFinishLoadMore() {
        }
    }

RefreshListenerAdapter 是一个抽象类,实现了 PullListener 接口:

public interface PullListener {
        /**
         * 下拉中
         */
        void onPullingDown(TwinklingRefreshLayout refreshLayout, float fraction);

        /**
         * 上拉
         */
        void onPullingUp(TwinklingRefreshLayout refreshLayout, float fraction);

        /**
         * 下拉松开
         */
        void onPullDownReleasing(TwinklingRefreshLayout refreshLayout, float fraction);

        /**
         * 上拉松开
         */
        void onPullUpReleasing(TwinklingRefreshLayout refreshLayout, float fraction);

        /**
         * 刷新中。。。
         */
        void onRefresh(TwinklingRefreshLayout refreshLayout);

        /**
         * 加载更多中
         */
        void onLoadMore(TwinklingRefreshLayout refreshLayout);

        /**
         * 手动调用finishRefresh或者finishLoadmore之后的回调
         */
        void onFinishRefresh();

        void onFinishLoadMore();
}

RefreshListenerAdapter 实现了 PullListener 接口变成一个抽象类,这让使用者更加的灵活,只关注使用到的状态回调,而不需要所有的状态回调,抽象类可以做到这一点,只覆写需要的函数,而接口不一样,继承了接口就必须覆写接口内部所有函数。这一点很值得学习!

3、核心类 TwinklingRefreshLayout 的分析:

首先看一张官方的类解析图:

TwinklingRefreshLayout 源码分析_第1张图片
TwinklingRefreshLayout

通过图中可以看到 TwinklingRefreshLayout 包含了一个 HeaderView 和一个 BottomView ,这两个 View 分别在上拉和下拉的时候显示出来。其中内部还包含了一个名字叫 CoProcessor 的类, CoProcessor 又包含三个 Processor ,我们来看下 CoProcessor 的构造函数就明白了:

public class CoProcessor {
    private RefreshProcessor refreshProcessor;
    private OverScrollProcessor overScrollProcessor;
    private AnimProcessor animProcessor;

    public CoProcessor() {
        animProcessor = new AnimProcessor(this);
        overScrollProcessor = new OverScrollProcessor(this);
        refreshProcessor = new RefreshProcessor(this);
    }

    ......
}

CoProcessor相当于是一个总的调度器,把任务分配给三个不同的Processor来处理,从名字上来看AnimProcessor处理动画,OverScrollProcessor处理滑动中的越界回弹,RefreshProcessor处理刷新动作。 CoProcessor的初始化工作是在TwinklingRefreshLayout的构造函数中完成。接下来看一下CoProcessor如何调度的,首先从拦截事件开始看:

/**
 * 拦截事件
 *
 *
 @return return true时,ViewGroup的事件有效,执行onTouchEvent事件
 * return false时,事件向下传递,onTouchEvent无效 */
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercept = cp.interceptTouchEvent(ev);
    return intercept || super.onInterceptTouchEvent(ev);
  }

  @Override
  public boolean onTouchEvent(MotionEvent e) {
    boolean resume = cp.consumeTouchEvent(e);
    return resume || super.onTouchEvent(e);
  }

cp.interceptTouchEvent(ev)调用了refreshProcessor.interceptTouchEvent(ev),来看下interceptTouchEvent做了些什么事:

public boolean interceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mTouchX = ev.getX();
            mTouchY = ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            float dx = ev.getX() - mTouchX;
            float dy = ev.getY() - mTouchY;
            if (Math.abs(dx) <= Math.abs(dy)) {//滑动允许最大角度为45度
                if (dy > 0 && !ScrollingUtil.canChildScrollUp(cp.getScrollableView()) && cp.allowPullDown()) {
                    cp.setStatePTD();
                    return true;
                } else if (dy < 0 && !ScrollingUtil.canChildScrollDown(cp.getScrollableView()) && cp.allowPullUp()) {
                    cp.setStatePBU();
                    return true;
                }
            }
            break;
    }
    return false;
}

ACTION_DOWN产生时不拦截事件只记录坐标,当ACTION_MOVE产生的时候,计算移动距离dyScrollingUtil.canChildScrollUp(cp.getScrollableView())用来判断子 View 是否还可以下拉,如果条件都满足,就调用cp.setStatePTD()改变状态,并且返回true表示拦截事件,接下去的事件就不下发给子View,直接交给onTouchEvent来处理:

public boolean consumeTouchEvent(MotionEvent e) {
    if (cp.isRefreshVisible() || cp.isLoadingVisible()) return false;

    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:
            float dy = e.getY() - mTouchY;
            if (cp.isStatePTD()) {
                dy = Math.min(cp.getMaxHeadHeight() * 2, dy);
                dy = Math.max(0, dy);
                cp.getAnimProcessor().scrollHeadByMove(dy);
            } else if (cp.isStatePBU()) {
                //加载更多的动作
                dy = Math.min(cp.getBottomHeight() * 2, Math.abs(dy));
                dy = Math.max(0, dy);
                cp.getAnimProcessor().scrollBottomByMove(dy);
            }
            return true;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            if (cp.isStatePTD()) {
                cp.getAnimProcessor().dealPullDownRelease();
            } else if (cp.isStatePBU()) {
                cp.getAnimProcessor().dealPullUpRelease();
            }
            return true;
    }
    return false;
}

根据ACTION_MOVE中的 if 条件计算出 dy 再通过 cp 来调试 AnimProcessor 做相应的动画,我们来看下scrollHeadByMove函数是如何实现头部的移动的:

public void scrollHeadByMove(float moveY) {
    float offsetY = decelerateInterpolator.getInterpolation(moveY / cp.getMaxHeadHeight() / 2) * moveY / 2;
    if (cp.getHeader().getVisibility() != VISIBLE) cp.getHeader().setVisibility(VISIBLE);
    if (cp.isPureScrollModeOn()) cp.getHeader().setVisibility(GONE);

    cp.getHeader().getLayoutParams().height = (int) Math.abs(offsetY);
    cp.getHeader().requestLayout();
    if (!cp.isOpenFloatRefresh()) {
        cp.getContent().setTranslationY(offsetY);
        translateExHead((int) offsetY);
    }
    cp.onPullingDown(offsetY);
}

根据cp.getAnimProcessor().scrollHeadByMove(dy)之前的计算可以看出dy的最大值是cp.getMaxHeadHeight()的两倍,所以scrollHeadByMove函数中offsetY的值最大应该等于cp.getMaxHeadHeight(),这就是为什么你再怎么拖动ScrollView,上面的HeadView最多也就显示这么点。接下去两个if判断是否显示HeaderView

//如果没有设置悬浮,那么需要手动对 ScrollView 做移动操作
//因为 ACTION_MOVE 已经被 TwinklingRefreshLayout 拦截,ScrollView 无法响应 ACTION_MOVE。
if (!cp.isOpenFloatRefresh()) {
    cp.getContent().setTranslationY(offsetY);
    translateExHead((int) offsetY);
}

继续回到consumeTouchEvent函数中,ACTION_MOVE动作完了以后,产生ACTION_UP调用cp.getAnimProcessor().dealPullDownRelease():

public void dealPullDownRelease() {
        if (!cp.isPureScrollModeOn() && getVisibleHeadHeight() >= cp.getHeadHeight() - cp.getTouchSlop()) {
        //下拉出的高度大于Head的高度就做刷新操作
        animHeadToRefresh();
    } else {
        //下拉的距离不够,回弹动画
        animHeadBack();
    }
}

执行animHeadToRefresh函数时对于HeadView做了,回到正常高度动画的操作。动画完成后会调用pullListener.onRefresh(TwinklingRefreshLayout.this);这就是我们在自己的 Activity 可以捕捉到的回调。
animHeadBack就是一个收起头部的动画。
至此,一次下拉刷新的操作算是完成了。上拉加载更多,原理是一样的就是方向变化了,有兴趣的可以看下实现,这边就不做重复的分析了。

目前还没提到的就是OverScrollProcessor这个处理器了,这个处理器是用来做越界回弹的,代码比较少,有兴趣的同学可以自行分析。

4、总结

整个项目,代码逻辑很清晰,亮点在于把整个 ViewGroup 的操作,按三个处理器分拆了,扩展性很好,可以很容易的去理解整个流程,这种分拆方式很值得学习!

你可能感兴趣的:(TwinklingRefreshLayout 源码分析)