Android之PullToRefresh控件源码解析

转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/73802453
本文出自:【顾林海的博客】

个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持!
在这里插入图片描述

前言

Android-PullToRefresh是一款非常出名的上拉加载和下拉刷新控件,相信同学们都使用过这个控件,Android-PullToRefresh控件内部是如何实现的呢,我们通过阅读源码来一窥究竟。这是它的github的地址,下载下来后可以看到无论是ListView、SrollView、WebView等,都是继承自PullToRefreshBase这个抽象类,这个抽象类在整个实现过程中起着举足轻重的作用,并且该控件使用了模板方法模式,封装了上拉加载和下拉刷新的逻辑以及事件的回调,同时将触发这些操作的条件交由子类来实现(这里面很容易看到的是,我们在判断控件是否滑动到顶部或是底部时的具体逻辑,是通过子类实现父类的抽象方法来实现的),也就是说,我们可以很方便的实现一个新的刷新控件(只要实现模板定义的抽象方法或是重写相关方法)。


PullToRefreshBase的职责


布局


public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T> {}

PullToRefreshBase它是一个继承自LinearLayout的泛型抽象类,当我们继承PullToRefreshBase抽象类时,通过限定了刷新控件必须是View或是View的子类。并且Android-PullToRefresh控件实现了两种方式的刷新方式,分别是垂直的上拉加载下拉刷新和水平的右拉刷新左拉加载,也就是说我们需要识别当前控件到底是横向还是纵向,前面也说过了这个控件使用了模板方法模式,因此我们将识别横向还是纵向的判断交由子类来实现,比如ListView支持纵向滚动,而HorizontalScrollView支持横向滚动,基于这些,我们在PullToRefreshBase中定义了一个抽象方法getPullToRefreshScrollDirection方法。
public abstract Orientation getPullToRefreshScrollDirection();

下面代码是PullToRefreshBase的初始化方法,第一步是设置控件的布局方向,前面也讲过,识别刷新控件的到底是垂直滚动还是水平滚动是交由具体的子类来实现的。
private void init(Context context, AttributeSet attrs) {
    //(1)通过getPullToRefreshScrollDirection方法判断是垂直滑动还是水平滑动。
    switch (getPullToRefreshScrollDirection()) {
        case HORIZONTAL:
            setOrientation(LinearLayout.HORIZONTAL);
            break;
        case VERTICAL:
        default:
            setOrientation(LinearLayout.VERTICAL);
            break;
    }
    //(2)设置内容居中。
    setGravity(Gravity.CENTER);
    //(3)使用 ViewConfiguration 获取用户手指滑动距离,用于判定滑动依据。
    ViewConfiguration config = ViewConfiguration.get(context);
    mTouchSlop = config.getScaledTouchSlop();

    // (4)获取自定义属性
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);
    //获取刷新的模式
    if (a.hasValue(R.styleable.PullToRefresh_ptrMode)) {
        mMode = Mode.mapIntToValue(a.getInteger(R.styleable.PullToRefresh_ptrMode, 0));
    }
    //获取刷新动画样式
    if (a.hasValue(R.styleable.PullToRefresh_ptrAnimationStyle)) {
        mLoadingAnimationStyle = AnimationStyle.mapIntToValue(a.getInteger(
                R.styleable.PullToRefresh_ptrAnimationStyle, 0));
    }

    //(5)创建真正刷新的控件
    mRefreshableView = createRefreshableView(context, attrs);
    //(6)添加刷新控件
    addRefreshableView(context, mRefreshableView);

    //(7)根据刷新和加载的模式分别创建头部刷新控件和底部加载控件
    mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
    mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);

    //(8)获取自定义属性
    if (a.hasValue(R.styleable.PullToRefresh_ptrRefreshableViewBackground)) {
        //背景
        Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrRefreshableViewBackground);
        if (null != background) {
            mRefreshableView.setBackgroundDrawable(background);
        }
    } else if (a.hasValue(R.styleable.PullToRefresh_ptrAdapterViewBackground)) {
        //背景
        Utils.warnDeprecation("ptrAdapterViewBackground", "ptrRefreshableViewBackground");
        Drawable background = a.getDrawable(R.styleable.PullToRefresh_ptrAdapterViewBackground);
        if (null != background) {
            mRefreshableView.setBackgroundDrawable(background);
        }
    }

    if (a.hasValue(R.styleable.PullToRefresh_ptrOverScroll)) {
        mOverScrollEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrOverScroll, true);
    }

    if (a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {
        mScrollingWhileRefreshingEnabled = a.getBoolean(
                R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled, false);
    }

    //(9)自定义属性派生给子类
    handleStyledAttributes(a);
    a.recycle();

    //(10)根据mode刷新UI
    updateUIForMode();
}

PullToRefresh控件定义了很多自定义属性,比如控件执行的模式、背景、刷新状态下是否能滚动控件等,可以通过xml来设置,当然也可以通过Java代码来设置,除了在PullToRefreshBase获取自定义属性外,作者还定义了一个handleStyledAttributes空方法,使用者可以通过重写这个方法来获取自定义属性。上面第5步创建真正刷新的控件,其实就是获取我们的ListView、SrollView、WebView等相关刷新控件View,获取这些控件的实现也是交由具体的子类来实现,因此定义了一个泛型的抽象方法createRefreshableView,返回的类型是T也就是View或者继承View的子类。第7步通过createLoadingLayout方法创建头部和底部的刷新View。
下面是createLoadingLayout方法的具体实现:
private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();
protected LoadingLayout createLoadingLayout(Context context, Mode mode, TypedArray attrs) {
    LoadingLayout layout = mLoadingAnimationStyle.createLoadingLayout(context, mode,
            getPullToRefreshScrollDirection(), attrs);
    layout.setVisibility(View.INVISIBLE);
    return layout;
}


AnimationStyle是一个内部枚举类,定义了刷新View在执行刷新时的动画方式,目前定义了两种方式,同学们可以试着设置这两种方式试试,这个枚举类内部定义了createLoadingLayout方法用于创建这两种动画方式的刷新View,根据这两种动画方式分别创建RotateLoadingLayout和FlipLoadingLayout实例,RotateLoadingLayout和FlipLoadingLayout都是继承自LoadingLayout这个抽象类(HeaderLayout和FooterLayout的布局)。既然顶部和底部的刷新控件已经创建完毕,接下来将它们设置为隐藏,毕竟在一开始进入页面时头部和底部的刷新控件应该不是处于显示状态的。
到了这里我们的头部刷新控件和底部刷新控件已经创建完毕,第10步调用方法udateUIForMode,该方法就是将它们添加到我们的整个刷新控件的容器中去:
protected void updateUIForMode() {
    final LayoutParams lp = getLoadingLayoutLayoutParams();

    if (this == mHeaderLayout.getParent()) {
        removeView(mHeaderLayout);
    }
    if (mMode.showHeaderLoadingLayout()) {
        addViewInternal(mHeaderLayout, 0, lp);
    }

    if (this == mFooterLayout.getParent()) {
        removeView(mFooterLayout);
    }
    if (mMode.showFooterLoadingLayout()) {
        addViewInternal(mFooterLayout, lp);
    }

    refreshLoadingViewsSize();

    mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START;
}

上面代码中在添加头部刷新控件和底部刷新控件时都会检查之前刷新控件是否存在,如果存在就先移除,为什么这样做呢?这是因为我们在设置PullToRefresh控件支持的刷新模式时,需要按照指定刷新模式来添加相应的头部和底部刷新控件,这里的刷新模式是通过Mode这个内部枚举类来判别(默认是PULL_FROM_START),比如说什么时候添加头部的刷新控件,在该枚举类提供了showHeaderLoadingLayout方法用于判断当前支持的模式是PULL_FROM_START(用于下拉刷新)还是BOTH(支持下拉刷新和上拉加载两种模式);又比如说什么时候添加底部的刷新控件,相应的在该枚举类也提供了showFooterLoadingLayout方法用于判断当前支持的模式是PULL_FROM_END(用于上拉加载)或是BOTH(支持下拉刷新和上拉加载两种模式)亦或是MANUAL_PEFRESH_ONLY(禁止上拉和下拉操作,但可以手动设置上拉加载操作)。

无论是下拉刷新(右拉)还是上拉加载(左拉),比如下拉刷新时,我们的手指在屏幕上能一直往下拖动一段距离,在上面的refreshLoadingViewSize方法中设置了这个拖动的一个距离:

protected final void refreshLoadingViewsSize() {
    final int maximumPullScroll = (int) (getMaximumPullScroll() * 1.2f);

    int pLeft = getPaddingLeft();
    int pTop = getPaddingTop();
    int pRight = getPaddingRight();
    int pBottom = getPaddingBottom();

    switch (getPullToRefreshScrollDirection()) {
        case HORIZONTAL:
            if (mMode.showHeaderLoadingLayout()) {
                mHeaderLayout.setWidth(maximumPullScroll);
                pLeft = -maximumPullScroll;
            } else {
                pLeft = 0;
            }

            if (mMode.showFooterLoadingLayout()) {
                mFooterLayout.setWidth(maximumPullScroll);
                pRight = -maximumPullScroll;
            } else {
                pRight = 0;
            }
            break;

        case VERTICAL:
            if (mMode.showHeaderLoadingLayout()) {
                mHeaderLayout.setHeight(maximumPullScroll);
                pTop = -maximumPullScroll;
            } else {
                pTop = 0;
            }

            if (mMode.showFooterLoadingLayout()) {
                mFooterLayout.setHeight(maximumPullScroll);
                pBottom = -maximumPullScroll;
            } else {
                pBottom = 0;
            }
            break;
    }
    setPadding(pLeft, pTop, pRight, pBottom);
}

在上面方法中分别通过垂直滚动还是水平滚动来设置顶部View(或左边)和底部View(或右边)的宽度或高度,这里面的宽度和高度值是通过getMaximumPullScroll方法中获取,该方法根据垂直和水平两种方式,分别获取当前PullToRefresh控件的高度或宽度,再将得到的宽高度进行相应的计算,这里的值只是一个经验值(高度或宽度的一半再乘以1.2)maximumPullScroll,最后将得到的这个经验值maximumPullScroll 以及PullToRefresh的padding值进行处理,比如在水平滚动下设置左边和右边的刷新控件的宽度为maximumPullScroll,同时在显示左边View的前提下设置PullToRefresh控件距离左边为-maximumPullScroll(隐藏掉左边的刷新控件),右边View也是一样,设置PullToRefresh控件距离右边-maximumPullScroll(隐藏掉右边的刷新控件),从文字说明上看,如果大家还是不怎么明白,可以看下面两幅图:

这里写图片描述


上面这幅图是我们在设置头部View的高度为maximumPullScroll ,然后将pTop设置为0时展示的,可以看到,我们的HeadView并没有隐藏掉,那我们将pTop设置为-maximumPullScroll,这时的展示图如下:

这里写图片描述


触摸事件的处理

PullToRefresh内部的布局已经讲解清楚,接下来具体分析一下触摸事件的处理,查看源码我们知道PullToRefreshBase继承自LinearLayout,也就是说PullToRefreshBase本身就是一个容器,用于存放顶部和底部的LoadingLayout以及中间的T(ListView、SrollView、WebView ),那么问题来了,当我们手指触摸PullToRefreshBase容器时,整个容器的拖动何时处理,内部View的滚动何时处理,这就需要进行相应的事件传递。PullToRefreshBase内部重写了onIterceptTouchEvent和onTouchEvent方法,下面我们先对这两个有事件相关的回调函数进行扫盲。

onIterceptTouchEvent方法,我们称它为事件的拦截,默认返回super.onInterceptTouchEvent(ev),事件默认会被拦截,并将拦截的事件交由当前View的onTouchEvent进行处理;如果返回true,事件的处理和返回super.onInterceptTouchEvent(ev)的事件处理一样,交由当前View的onTouchEvent进行处理;如果返回false,表示事件放行(不拦截),当前View上的事件会被传递到子View上,由子View的onTouchEvent进行处理。
onTouchEvent方法,我们称它为事件的响应,默认返回super.onTouchEvent(ev),同事事件会从当前View向上传递,由上层View的onTouchEvent来接收处理;如果返回false,事件处理与返回super.onTouchEvent(ev)的事件处理一样,交由上层处理;如果返回true,说明当前的事件被接收并被消费。

接下来我们从onIterceptTouchEvent方法也就是事件的拦截来说起。

@Override
public final boolean onInterceptTouchEvent(MotionEvent event) {

    /*
    T1:当不支持上拉加载和下拉刷新操作时,不进行事件的拦截,交由子View(我们的刷新控件,如:ListView、ScrollView等)来处理。
     */
    if (!isPullToRefreshEnabled()) {
        return false;
    }

    final int action = event.getAction();

    /*
    T2:当手指取消触摸或是离开屏幕时,说明一次刷新的操作结束(无论是否真正进行了刷新和加载的操作),
    最后将事件的处理交由子View(T)来处理。
     */
    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        mIsBeingDragged = false;
        return false;
    }

    /*
    T3:当我们手指在屏幕上拖动时(mIsBeingDragged标志位为true,说明当前满足了刷新和加载操作),进行
    事件的拦截,交由自身onTouchEvent来处理。
     */
    if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
        return true;//由自身onTouchEvent消费
    }

    /*
    T4:mIsBeingDragged标志位什么时候为true?需要手指触摸该控件时进行判断:当前的子View(T)是否到达了顶部以满足下拉刷新的条件,或是
    当前的子View(T)是否滑动到了底部以满足上拉加载的条件。
     */

    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            /*
            T5:mScrollingWhileRefreshingEnabled标志位用于说明在刷新过程中,手指再次滑动控件,这时控件是否能滑动,当mScrollingWhileRefreshingEnabled为
            false时,说明刷新状态下控件不能被滑动,这时就需要事件的拦截。这里的isRefreshing用于判断当前是否处于刷新状态。
             */
            if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
                return true;
            }

            /*
            T6:isReadyForPull方法的作用是检查当前控件是否满足刷新和加载操作条件,可以看的isReadyForPull方法中调用了isReadyForPullStart和isReadyForPullEnd
            抽象方法,这两个方法交由实现类来实现。
             */
            if (isReadyForPull()) {
                final float y = event.getY(), x = event.getX();
                final float diff, oppositeDiff, absDiff;

                switch (getPullToRefreshScrollDirection()) {
                    case HORIZONTAL:
                        diff = x - mLastMotionX;
                        oppositeDiff = y - mLastMotionY;
                        break;
                    case VERTICAL:
                    default:
                        diff = y - mLastMotionY;
                        oppositeDiff = x - mLastMotionX;
                        break;
                }
                absDiff = Math.abs(diff);

                if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {
                    if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {
                        //T7:下拉刷新
                        mLastMotionY = y;
                        mLastMotionX = x;
                        mIsBeingDragged = true;
                        if (mMode == Mode.BOTH) {
                            mCurrentMode = Mode.PULL_FROM_START;
                        }
                    } else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {
                        //T8:上拉加载
                        mLastMotionY = y;
                        mLastMotionX = x;
                        mIsBeingDragged = true;
                        if (mMode == Mode.BOTH) {
                            mCurrentMode = Mode.PULL_FROM_END;
                        }
                    }
                }
            }
            break;
        }
        case MotionEvent.ACTION_DOWN: {
            if (isReadyForPull()) {
                /*
                T9:当我们手指按下屏幕时,试想下如果当前子View(T)位于顶部或是底部时,我们拦截事件会导致T无法上拉或下拉操作,因此
                通过isReadyForPull方法检查当前T是否位于顶部或是滑动到了底部位置,这时将mIsBeingDragged设置为false,不进行事件的拦截,交由
                T来处理。
                 */
                mLastMotionY = mInitialMotionY = event.getY();
                mLastMotionX = mInitialMotionX = event.getX();
                mIsBeingDragged = false;
            }
            break;
        }
    }

    return mIsBeingDragged;
}


在事件不需要拦截的时候,事件的一系列响应交由子View来处理,比如ListView的滑动和点击事件,因此,总结在以下几种情况事件不需要拦截:
  1. 当前的PullToRefresh控件设置的执行模式为DISABLED(禁止上拉加载和下拉刷新)或MANUAL_PEFRESH_ONLY(禁止上拉加载和下拉刷新,但允许手动设置刷新状态)。
  2. 手指离开屏幕或是取消触摸。
  3. T(刷新控件)没有滑动到顶部或底部时。

针对第一条:我们在上面的事件拦截方法的第一部分T1中可以看到一个条件判断语句,isPullToRefreshEnabled方法用于判断当前控件是否支持手动拖动来触发上拉加载和下拉刷新操作,也就是说当控件不支持上拉加载和下拉刷新时,需要将事件传递给它的子View(T)来处理,这样我们的T(ListView、SrollView…)才能处理自身的触摸事件。针对第二条:T2手指离开屏幕或是取消触摸事件交由T来处理。针对第三条:在手指触摸按下或是触摸时(前提在设置Mode为FULL_FROM_START或是FULL_FROM_END亦或是BOTH),依据Mode判断T是否需要刷新或加载,具体看以下代码:

/**
 * 何时下拉刷新和上拉加载,交由子类来实现
 */
private boolean isReadyForPull() {
    switch (mMode) {
        case PULL_FROM_START:
            return isReadyForPullStart();
        case PULL_FROM_END:
            return isReadyForPullEnd();
        case BOTH:
            return isReadyForPullEnd() || isReadyForPullStart();
        default:
            return false;
    }
}

调用的抽象方法交由子类来实现,isReadyForPullStart方法用于说明当前控件是否做好下拉刷新的准备,isReadyForPullEnd方法用于说明当前控件是否做好上拉加载的准备。最后我们看在执行到MotionEvent.ACTION_DOWN,也就是手指按下屏幕时,如果加载或刷新都已经准备好了,这里面的mIsBeingDraggd标志位设置为false,最后return mIsBeingDraggd,既然加载或刷新都准备好了,为什么还要进行事件的拦截呢?在这里举个例子:如果我们进入页面,此时ListView已经滑动到了顶部,这时我们想往下滑,如果在onIterceptTouchEvent方法中返回了true,What? 页面怎么滑动不了了,这是因为我们将事件拦截了,所以在手指按下屏幕是并且满足刷新或加载条件时,事件还是交由T来处理。


以上的做法可以保证我们的T能接受到事件并得到处理(也就是满足了最基本的T的滑动点击等事件的处理),何时执行我们的刷新和加载操作,也就是在设置Mode为FULL_FROM_START或是FULL_FROM_END亦或是BOTH的条件下,并满足刷新或加载的条件成立。查看onIterceptTouchEvent方法中的T6段代码,手指在屏幕滑动时,并满足刷新和加载条件的成立,判断滑动的距离是否满足下拉或是上拉操作,只有当这些条件成立,接下来才会去处理刷新和加载的相关逻辑;在T7段代码处,当Mode为PULL_FROM_START或BOTH时,并且下拉刷新的条件成立,这时将mCurrentMode设置为PULL_FROM_START,将标志位是否开始拖动mIsBeingDragged设置为true;在T8段代码处,当Mode为PULL_FROM_END或BOTH时,并且上拉加载的条件成立,这时将mCurrentMode设置为PULL_FROM_END ,将标志位是否开始拖动mIsBeingDragged设置为true。当mIsBeingDragged为ture时,自身的onTouchEvent开始处理LoadingLayout显示以及相关操作。

事件的拦截已经讲解完毕,接下来聊聊事件的响应onTouchEvent方法的实现:

@Override
public final boolean onTouchEvent(MotionEvent event) {

    if (!isPullToRefreshEnabled()) {
        return false;
    }

    if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
        return true;
    }

    if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {
        return false;
    }

    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE: {
            if (mIsBeingDragged) {
                mLastMotionY = event.getY();
                mLastMotionX = event.getX();
                pullEvent();
                return true;
            }
            break;
        }

        case MotionEvent.ACTION_DOWN: {
            if (isReadyForPull()) {
                mLastMotionY = mInitialMotionY = event.getY();
                mLastMotionX = mInitialMotionX = event.getX();
                return true;
            }
            break;
        }

        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP: {
            if (mIsBeingDragged) {
                mIsBeingDragged = false;

                if (mState == State.RELEASE_TO_REFRESH
                        && (null != mOnRefreshListener || null != mOnRefreshListener2)) {
                    /*
                    当前已经达到了刷新和加载的条件,当手指离开屏幕时执行刷新或加载。
                     */
                    setState(State.REFRESHING, true);
                    return true;
                }

                if (isRefreshing()) {
                    smoothScrollTo(0);
                    return true;
                }

                setState(State.RESET);

                return true;
            }
            break;
        }
    }

    return false;
}


onTouchEvent方法中,手指按下去的操作没有执行,这是因为在事件拦截方法中按下的动作并没有进行拦截,这里我们重点分析手指拖动以及手指抬起时的相关操作逻辑。在拖动时(ACTION_MOVE),通过不停的调用pullEvent方法来显示或隐藏LoadingLayout,pullEvent方法如下:
private void pullEvent() {
    final int newScrollValue;
    final int itemDimension;
    final float initialMotionValue, lastMotionValue;

    switch (getPullToRefreshScrollDirection()) {
        case HORIZONTAL:
            initialMotionValue = mInitialMotionX;
            lastMotionValue = mLastMotionX;
            break;
        case VERTICAL:
        default:
            initialMotionValue = mInitialMotionY;
            lastMotionValue = mLastMotionY;
            break;
    }

    //获取滚动的值
    switch (mCurrentMode) {
        case PULL_FROM_END:
            newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION);
            itemDimension = getFooterSize();
            break;
        case PULL_FROM_START:
        default:
            newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION);
            itemDimension = getHeaderSize();
            break;
    }

    setHeaderScroll(newScrollValue);

    if (newScrollValue != 0 && !isRefreshing()) {
        /*
        计算出下拉或上拉时 刷新View显示的占比
         */
        float scale = Math.abs(newScrollValue) / (float) itemDimension;
        switch (mCurrentMode) {
            case PULL_FROM_END:
                mFooterLayout.onPull(scale);
                break;
            case PULL_FROM_START:
            default:
                mHeaderLayout.onPull(scale);
                break;
        }

        if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) {
            /*
            头部和底部刷新View显示的高度低于刷新View的高度,说明还没有到达刷新状态
             */
            setState(State.PULL_TO_REFRESH);
        } else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) {
            /*
             头部和底部刷新View显示的高度高于刷新View的高度,说明手指松开执行刷新
             */
            setState(State.RELEASE_TO_REFRESH);
        }
    }
}

该方法主要负责HeaderLayout和FooterLayout的显示与隐藏、HeaderLayout和FooterLayout显示的百分比以及刷新状态的更新,其中通过setHeaderScroll方法来实现HeaderLayout和FooterLayout的显示与隐藏:
protected final void setHeaderScroll(int value) {
    final int maximumPullScroll = getMaximumPullScroll();
    value = Math.min(maximumPullScroll, Math.max(-maximumPullScroll, value));

    if (mLayoutVisibilityChangesEnabled) {
        if (value < 0) {
            mHeaderLayout.setVisibility(View.VISIBLE);
        } else if (value > 0) {
            mFooterLayout.setVisibility(View.VISIBLE);
        } else {
            mHeaderLayout.setVisibility(View.INVISIBLE);
            mFooterLayout.setVisibility(View.INVISIBLE);
        }
    }

    if (USE_HW_LAYERS) {
        ViewCompat.setLayerType(mRefreshableViewWrapper, value != 0 ? View.LAYER_TYPE_HARDWARE
                : View.LAYER_TYPE_NONE);
    }

    switch (getPullToRefreshScrollDirection()) {
        case VERTICAL:
            scrollTo(0, value);
            break;
        case HORIZONTAL:
            scrollTo(value, 0);
            break;
    }
}

通过scrollTo方法来实现HeaderLayout和FooterLayout 的移动,scrollTo 通过拖动的距离移动整个控件的位置从而达到拖动HeaderLayout和FooterLayout 的效果。在pullEvent方法中,通过HeaderLayout和FooterLayout显示的区域来判断当前是否处于刷新状态(也就是HeaderLayout和FooterLayout完全被显示出来),当State状态为PULL_TO_REFRESH时还没有到达刷新状态,当State状态为RELEASE_TO_REFRESH时说明已经处于刷新状态。手指离开屏幕时可以根据该状态做出相应的操作。

case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
    if (mIsBeingDragged) {
        mIsBeingDragged = false;

        if (mState == State.RELEASE_TO_REFRESH
                && (null != mOnRefreshListener || null != mOnRefreshListener2)) {
            /*
            当前已经达到了刷新和加载的条件,当手指离开屏幕时执行刷新或加载。
             */
            setState(State.REFRESHING, true);
            return true;
        }

        if (isRefreshing()) {
            smoothScrollTo(0);
            return true;
        }

        setState(State.RESET);

        return true;
    }
    break;
}

从上面的代码中可以看到当State状态为RELEASE_TO_REFRESH时说明已经处于刷新状态,这时调用setState(State.REFRESHING ,true)方法将State设置成REFRESHING,说明当前处于刷新状态:

final void setState(State state, final boolean... params) {
    mState = state;
    switch (mState) {
        case RESET:
            onReset();
            break;
        case PULL_TO_REFRESH:
            onPullToRefresh();
            break;
        case RELEASE_TO_REFRESH:
            onReleaseToRefresh();
            break;
        case REFRESHING:
        case MANUAL_REFRESHING:
            onRefreshing(params[0]);
            break;
        case OVERSCROLLING:
            // NO-OP
            break;
    }

    if (null != mOnPullEventListener) {
        mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
    }
}


这时的State状态为REFRESHING,我直接查看REFRESHING这条分支,调用了onRefreshing方法,继续往下查看:
protected void onRefreshing(final boolean doScroll) {
    /*
    通知给相应的Layout当前处于刷新状态。
     */
    if (mMode.showHeaderLoadingLayout()) {
        mHeaderLayout.refreshing();
    }
    if (mMode.showFooterLoadingLayout()) {
        mFooterLayout.refreshing();
    }

    if (doScroll) {
        if (mShowViewWhileRefreshing) {

            OnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener() {
                @Override
                public void onSmoothScrollFinished() {
                    /*
                    刷新和加载事件的回调
                     */
                    callRefreshListener();
                }
            };

            switch (mCurrentMode) {
                case MANUAL_REFRESH_ONLY:
                case PULL_FROM_END:
                    smoothScrollTo(getFooterSize(), listener);
                    break;
                default:
                case PULL_FROM_START:
                    smoothScrollTo(-getHeaderSize(), listener);
                    break;
            }
        } else {
            smoothScrollTo(0);
        }
    } else {
        callRefreshListener();
    }
}

在onRefreshing方法中,前两个条件判断语句用于通知HeaderLayout和FooterLayout当前处于刷新状态(可以进行更新文案、时间之类的操作),OnSmoothScrollFinishedListener用于刷新和加载操作的监听,接着通过smoothScrollTo方法实现OnSmoothScrollFinishedListener接口中onSmoothScrollFinished方法的回调。
protected final void smoothScrollTo(int scrollValue, OnSmoothScrollFinishedListener listener) {
    smoothScrollTo(scrollValue, getPullToRefreshScrollDuration(), 0, listener);
}
private final void smoothScrollTo(int newScrollValue, long duration, long delayMillis,
                                  OnSmoothScrollFinishedListener listener) {
    if (null != mCurrentSmoothScrollRunnable) {
        mCurrentSmoothScrollRunnable.stop();
    }

    final int oldScrollValue;
    switch (getPullToRefreshScrollDirection()) {
        case HORIZONTAL:
            oldScrollValue = getScrollX();
            break;
        case VERTICAL:
        default:
            oldScrollValue = getScrollY();
            break;
    }

    if (oldScrollValue != newScrollValue) {
        if (null == mScrollAnimationInterpolator) {
            // Default interpolator is a Decelerate Interpolator
            mScrollAnimationInterpolator = new DecelerateInterpolator();
        }
        mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(oldScrollValue, newScrollValue, duration, listener);

        if (delayMillis > 0) {
            postDelayed(mCurrentSmoothScrollRunnable, delayMillis);
        } else {
            post(mCurrentSmoothScrollRunnable);
        }
    }
}


HeaderLayout和FooterLayout的回弹实现和刷新的监听通过View的postDelayed和post方法来实现,通过post方法,可以获取当前线程(UI线程)的Handler。
final class SmoothScrollRunnable implements Runnable {
    private final Interpolator mInterpolator;
    private final int mScrollToY;
    private final int mScrollFromY;
    private final long mDuration;
    private OnSmoothScrollFinishedListener mListener;

    private boolean mContinueRunning = true;
    private long mStartTime = -1;
    private int mCurrentY = -1;

    public SmoothScrollRunnable(int fromY, int toY, long duration, OnSmoothScrollFinishedListener listener) {
        mScrollFromY = fromY;
        mScrollToY = toY;
        mInterpolator = mScrollAnimationInterpolator;
        mDuration = duration;
        mListener = listener;
    }

    @Override
    public void run() {

        if (mStartTime == -1) {
            mStartTime = System.currentTimeMillis();
        } else {

            long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration;
            normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);

            final int deltaY = Math.round((mScrollFromY - mScrollToY)
                    * mInterpolator.getInterpolation(normalizedTime / 1000f));
            mCurrentY = mScrollFromY - deltaY;
            setHeaderScroll(mCurrentY);
        }

        if (mContinueRunning && mScrollToY != mCurrentY) {
            ViewCompat.postOnAnimation(PullToRefreshBase.this, this);
        } else {
            if (null != mListener) {
                mListener.onSmoothScrollFinished();
            }
        }
    }

    public void stop() {
        mContinueRunning = false;
        removeCallbacks(this);
    }
}

从上面代码中可以看到,当HeaderLayout和FooterLayout回弹效果结束后就开始回调OnSmoothScrollFinishedListener接口中onSmoothScrollFinished方法。在onSmoothScrollFinished方法中调用了callRefreshListener方法。
private void callRefreshListener() {
    if (null != mOnRefreshListener) {
        mOnRefreshListener.onRefresh(this);
    } else if (null != mOnRefreshListener2) {
        if (mCurrentMode == Mode.PULL_FROM_START) {
            mOnRefreshListener2.onPullDownToRefresh(this);
        } else if (mCurrentMode == Mode.PULL_FROM_END) {
            mOnRefreshListener2.onPullUpToRefresh(this);
        }
    }
}

将刷新事件回调给UI线程,交由UI线程处理相关操作。最后调用onRefreshComplete方法进行重置。

到这里事件的处理已经讲解完毕,总结来说,PullToRefreshBase主要负责以下几件事:

  • HeaderLayout和FooterLayout的创建。
  • 添加HeaderLayout和FooterLayout到PullToRefreshBase容器中。
  • 添加T(ListView、WebView…)到PullToRefreshBase容器中。(交由具体的子类来创建)
  • 处理刷新逻辑(事件的拦截和响应),并将刷新事件回调给UI线程。(刷新和加载条件成立交由具体的子类来实现)

自定义控件PullToRefreshScrollView实例

PullToRefreshBase这个抽象类的职责已经讲解了差不多了,现在我们就来自定义一个ScrollView的上拉加载和下拉刷新的控件,首先实现PullToRefreshBase抽线类定以的几个方法

@Override
public Orientation getPullToRefreshScrollDirection() {
   return null;
}

@Override
protected ScrollView createRefreshableView(Context context, AttributeSet attrs) {
   return null;
}

@Override
protected boolean isReadyForPullEnd() {
   return false;
}

@Override
protected boolean isReadyForPullStart() {
   return false;
}


这些方法的作用在上面都讲解过,这里我们只要实现这些方法就可以:
public class PullToRefreshScrollView extends PullToRefreshBase<ScrollView> {

    public PullToRefreshScrollView(Context context) {
        super(context);
    }

    public PullToRefreshScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public PullToRefreshScrollView(Context context, Mode mode) {
        super(context, mode);
    }

    public PullToRefreshScrollView(Context context, Mode mode, AnimationStyle style) {
        super(context, mode, style);
    }


    @Override
    public final Orientation getPullToRefreshScrollDirection() {
        return Orientation.VERTICAL;
    }

    @Override
    protected ScrollView createRefreshableView(Context context, AttributeSet attrs) {
        ScrollView scrollView;
        scrollView = new ScrollView(context, attrs);
        scrollView.setId(R.id.scrollview);
        return scrollView;
    }

    @Override
    protected boolean isReadyForPullStart() {
        return mRefreshableView.getScrollY() == 0;
    }

    @Override
    protected boolean isReadyForPullEnd() {
        View scrollViewChild = mRefreshableView.getChildAt(0);
        if (null != scrollViewChild) {
            return mRefreshableView.getScrollY() >= (scrollViewChild.getHeight() - getHeight());
        }
        return false;
    }

}

到此整个PullToRefresh控件的大体实现思路已经讲解完毕。
©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:上身试试 返回首页