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 文件中声明
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <include layout="@layout/inc_toolbar" />

    <com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout
        android:id="@+id/refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#afbfff"
            android:overScrollMode="never">

            .
            .
            .

        </ScrollView>
    </com.lcodecore.tkrefreshlayout.TwinklingRefreshLayout>

</LinearLayout>

在 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

通过图中可以看到 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 的操作,按三个处理器分拆了,扩展性很好,可以很容易的去理解整个流程,这种分拆方式很值得学习!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容