Android-PullToRefresh 之二:详细设计(一、PullToRefresh)

系列文章


Android-PullToRefresh 之一:概要设计
Android-PullToRefresh 之二:详细设计(一、PullToRefresh)
Android-PullToRefresh 之二:详细设计(二、LoadingLayout)
Android-PullToRefresh 之三:扩展的PullToRefreshRecyclerView

详细设计的思路


两种设计思路:基于具体功能的设计和基于抽象功能的设计。

基于具体功能的设计

基于具体功能的设计需要结合面相过程思想和面相对象思想。
具体过程:
1、面相过程(从上往下分解):

  1. 流程化:将每个具体功能(编程无关,看Android-PullToRefresh 之一:概要设计)用工作流程来表示。
  2. 分解:工作流程的每个步骤就是一个更小的功能(编程相关,即涉及到编程知识)。循环执行1、2步骤,直到不能再分解。
  3. 代码实现:每个小的功能用具体代码实现。

总之,对功能的分解,就是对功能的工作流程的分解,工作流程的每个步骤就是一个小的功能,而小的功能又可以用一个小的工作流程表示。
功能可以用是一个流程表示,流程就是过程,这就是面相过程思想

2、面相对象(从下往上封装、抽象):当功能用具体代码表示时,其实就是用具体的对象表示,只不过这时的对象是“最低层的”(未经过程序员抽象、封装的)。实际上整个软件的功能已经能通过这些代码实现了,但是,这时的代码重用性和可扩展性都不高。为优化代码,必须对代码封装、抽象:

  • 封装:封装流程为类。
    封装一个小的流程为一个类(一个流程就是一个功能,一个类只有单一功能,可提高内聚性),封装后的类就是更高一层的,在这更高一层又可以继续封装里面的流程,如此循环,从下往上封装。
  • 抽象:抽取出多个类中的公共方法或公共流程,独立成为一个工具类或抽象类。
    将多个类中公共的方法抽取出来,独立成为一个工具类或抽象类。

从最低层的对象开始,一层一层的往上封装、抽象,这就是面相对象思想。

上面的设计思路是基于具体功能的,流程如下:
具体功能->工作流程->分解->小功能->代码(最底层的对象)实现->封装、抽象->父类

其实真正设计时,如果这么细致,会特别麻烦,也很少有人这样做。那么是否还有别的方法呢?在此之前要先思考,这样设计的目的是什么呢?
只是为了抽象,从而提高重用性、可扩展性。只是为了抽象,那么当我们的功能是抽象的不就行了,请看下面:

基于抽象功能的设计

步骤:

  1. 流程化:将抽象功能用一个工作流程表示。
  2. 分解:工作流程的每个步骤就是一个更小的功能(编程相关,即涉及到编程知识)。循环执行1、2步骤,直到不能再分。
  3. 代码实现:用代码实现这个工作流程(涉及到具体功能的可用通过区分具体功能的类型,不同类型执行各自的操作,也可以直接用abstract方法代替),这样就形成了父类。
    流程如下:
    抽象功能->工作流程->分解->小功能->代码实现->父类
    没有了封装抽象的步骤。因为抽象功能本身就是从具体功能抽象出来的。

看的云里雾里吧?我是不会告诉你我也一样的。
如果你看懂了,那我只能说你是小母牛蹲火炉——牛b哄哄!!
不是很明白的,看下面对Android-PullToRefresh的分析就清楚多了,下面的分析是基于抽象功能:

PullToRefresh的设计


在分析之前,先上类图。

类图

PullToRefresh的类图:
ptr类图

header、footer的类图:
LoadingLayout类图

本文只分析IPullToRefresh、PullToRefreshBase、PullToRefreshScrollView这三个类的设计,其它的思路类似。

设计过程

基于抽象功能设计的流程:抽象功能->工作流程->代码实现->父类
既然是基于抽象功能设计的,那么流程的每个步骤具体是什么呢?下面逐个分析。

抽象功能

通过Android-PullToRefresh 之一:概要设计可知,Android-PullToRefresh的抽象功能是基于抽象的UI结构的,包括下拉刷新和上拉加载,可以把这两个功能统称为一个pull功能

抽象的UI结构图(抽象的PullToRefresh):头部、内容区域、尾部
PullToRefresh 的UI结构

设计思路:从上图可知,header、内容区域、footer是可变的。根据“独立变化”这一设计原则,把 header、内容区域、footer 独立出来成为单独的类,PullToRefreshBase 类分别持有 header、内容区域、footer 这三个类的变量,并未每个变量设置 setter 方法。而header、footer 需提供设置状态等方法,内容区域对应的类需提供方法判断是否已到达内容区域的顶部或底部。这其实就是策略模式。
注:在该开源库中并没有把“内容区域”独立成为一个单独的类。

工作流程

UI:
抽象的UI结构是线性的,故PullToRefreshBase继承LinearLayout比较好,它按顺序添加了header、content、footer。
由于每个具体PullToRefresh的header和footer都是可以是任何类型的LoadingLayout,因此,header、footer的实现是在PullToRefreshBase中。=>AnimationStyle用于区分LoadingLayout类型(2种:rotate、flip)
content则是用一个FrameLayout代替,具体PullToRefresh需要实现自己的mRefreshableView并添加到该FrameLayout中。=>abstract方法createRefreshableView()

pull功能的工作流程:事件拦截->消耗(滑动、更改状态、load data)
- 事件拦截的流程
1. enabled:header或footer被允许pull。=>Mode标志
2. filter action:过滤掉ACTION_CANCEL、ACTION_UP等action。
3. ready for pull:内容区域中添加的View(如ScrollView)的内容到达顶部或底部。=>(abstract 方法)
4. 滑动距离正确:orientation 方向上滑动距离大于其垂直方向上的距离。=>Orientation标志
5. 滑动方向正确:(pull_from_start && 滑动方向向下/右) || (pull_from_end && 滑动方向向上/左)。
6. 拦截

  • 消耗的流程
    1. 滑动的流程
      1.1 移动ptr:显示或隐藏相应LoadingLayout并据滑动距离移动整个ptr的内容。
      1.2 动画:改变LoadingLayout中图片的旋转角度。=>(LoadingLayout的abstract 方法)
    2. 更改状态的流程=>State标志
      2.1 改变PullToRefresh当前状态
      2.2 改变相应LoadingLayout的状态。=>(LoadingLayout的abstract 方法)
    3. load
      3.1 调用监听器方法。=>(监听器的abstract 方法)

abstract表示该步骤是抽象的,需要提供一个abstract分发,因为涉及到具体PullToRefresh,UI不同,其具体实现不同,符合“依赖倒置原则”。
流程中的步骤若涉及到具体PullToRefresh时,依据“依赖倒置原则”应该提供abstract方法,但是,若可以通过一个标志而且该标志的值或者说类型是固定不变的,那么就可以通过这个标志来区分具体PullToRefresh,不用提供abstract方法。由于标志的值或者说类型是固定不变的,因此当增加新的具体PullToRefresh类型时不用去改变这一步骤的实现代码,符合“开放封闭原则”。Orientation就是这种标志位。

根据详细的工作流程可知,需要的Field:
Orientation:水平、垂直。
Mode:只允许上拉、只允许下拉、允许上拉和下拉、都不允许等。
State:当前处于pull过程中的哪个状态。

代码实现

IPullToRefresh的实现

下面内容可以跳过,直接看PullToRefreshBase的实现,不影响下面的分析,因为它设计的不好。

上面的工作流程实现后得到的父类其实是 PullToRefreshBase,而这个 IPullToRefresh 是 PullToRefreshBase 的接口,既然是接口了,那么应该适用于多种类型的刷新库(无论header和footer是否存在,样式如何,如:ActionBar-PullToRefresh开源库就没有header和footer、BeautifulRefreshLayout开源库等)

基于适用于各种类型刷新库的目的(无论header和footer是否存在,样式如何)那么就需要设计IPullToRefresh,而该IPullToRefresh接口中的方法就是将上面工作流程中的一些步骤用一个抽象方法代替,因为无论是什么类型的刷新库,工作流程基本都是“事件拦截->消耗”,而这个两个步骤中又有一些步骤是公有的,比如更改状态、load等。除此之外,还应该在IPullToRefresh和PullToRefreshBase直接插入一个IPullToRefresh的子类AbstractClass,同时这个类又是PullToRefreshBase的父类,那么AbstractClass需要重写onInterceptTouchEvent()和onTouchEvent()分别在里面调用IPullToRefresh中的abstract方法实现“事件拦截”的工作流程、“事件消耗”的工作流程。
以后有机会可能会改一改Android-PullToRefresh,实现上面这种目的。
上面这段话看不懂没关系,跟下面没什么关系。

由于Android-PullToRefresh设计时可能并没有考虑这个目的,因此它设计出来的接口并不适用于各种类型刷新库,因此接口其实可以不用的,我们直接忽略它。

PullToRefreshBase的实现

无论是类还是方法,都是按照“输入(合法性、创建、初始化、设置)->处理(逻辑操作)->输出(释放、返回)”来设计的。
上面的工作流程只是PullToRefreshBase的逻辑操作,那么PullToRefreshBase的输入、输出是怎样设计的呢?

1、输入
  • 合法性
    通过xml和构造方法提供入参,这里不需要判断,因为创建Field时有为它们提供默认值。

  • 创建Field
    PullToRefreshBase所需的Field。

    源码:

public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T> {
    //用于区分具体PullToRefresh的标志位。
    //没有创建Orientation的Field,都是通过抽象方法getPullToRefreshScrollDirection()直接获取。
    private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();//有默认值
    private Mode mMode = Mode.getDefault();//有默认值
    private Mode mCurrentMode;

    //UI
    private LoadingLayout mHeaderLayout;//因为AnimationStyle有默认值,因此它也有默认值,因为它是根据前者初始化的。
    private LoadingLayout mFooterLayout;    
    private FrameLayout mRefreshableViewWrapper;    
    T mRefreshableView;

    //当前状态
    private State mState = State.RESET;//有默认值
    //load的监听器
    private OnRefreshListener<T> mOnRefreshListener;//只监听上拉
    private OnRefreshListener2<T> mOnRefreshListener2;//监听上拉、下拉
}

实际上,Android-PullToRefresh将Orientation、AnimationStyle 、Mode、State写成PullToRefreshBase的枚举类型的内部类。

public static enum Orientation {
    //具体PullToRefresh的方向,如PullToRefreshHorizontalScrollView就是HORIZONTAL类型。
    VERTICAL, HORIZONTAL;
}
public static enum AnimationStyle {
    //两种LoadingLayout类型:rotate、flip。
    ROTATE,
    FLIP;

    static AnimationStyle getDefault() {
        return ROTATE;
    }

    //用于将通过xml中的属性获得的int值转换为相应类型,动态设置header、footer类型,使得PullToRefreshListView等的header、footer既可以是rotate类型也可以是flip类型。
    static AnimationStyle mapIntToValue(int modeInt) {
        switch (modeInt) {
            case 0x0:
            default:
                return ROTATE;
            case 0x1:
                return FLIP;
        }
    }

    //header、footer的创建。
    LoadingLayout createLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) {
        switch (this) {
            case ROTATE:
            default:
                return new RotateLoadingLayout(context, mode, scrollDirection, attrs);
            case FLIP:
                return new FlipLoadingLayout(context, mode, scrollDirection, attrs);
        }
    }
}
public static enum Mode {
    //enabled类型:即允许刷新加载的类型,如PULL_FROM_START就是只允许下拉刷新,不允许上拉加载。   
    DISABLED(0x0),
    PULL_FROM_START(0x1),
    PULL_FROM_END(0x2),
    BOTH(0x3),
    MANUAL_REFRESH_ONLY(0x4);//不允许通过手势刷新,只能通过代码调用刷新。
}

用于区分PullToRefresh当前状态的枚举类:State
Android-PullToRefresh 之一:概要设计可知,pull功能有4个基本状态:reset 、pull(包括pull to refresh、release to refresh) 、refreshing。State中除了这几个基本状态外还添加了其他一些状态。

public static enum State {
    RESET(0x0),
    PULL_TO_REFRESH(0x1),
    RELEASE_TO_REFRESH(0x2),
    REFRESHING(0x8),
    MANUAL_REFRESHING(0x9), //正在刷新,只不过是通过代码调用导致的刷新。 
    OVERSCROLLING(0x10);
}
  • 初始化
private void init(Context context, AttributeSet attrs) {
    switch (getPullToRefreshScrollDirection()) {//抽象方法获取Orientation
        case HORIZONTAL:
            setOrientation(LinearLayout.HORIZONTAL);
            break;
        case VERTICAL:
        default:
            setOrientation(LinearLayout.VERTICAL);
            break;
    }

    setGravity(Gravity.CENTER);

    ViewConfiguration config = ViewConfiguration.get(context);
    mTouchSlop = config.getScaledTouchSlop();

    // Styleables from XML
    //xml中动态设置AnimationStyle、Mode
    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));
    }

    // Refreshable View
    // By passing the attrs, we can add ListView/GridView params via XML
    mRefreshableView = createRefreshableView(context, attrs);//抽象方法创建RefreshableView
    addRefreshableView(context, mRefreshableView);

    // We need to create now layouts now
    //根据AnimationStyle创建header、footer
    mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
    mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);

    /**
     * Styleables from XML
     */
    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);
    }

    // Let the derivative classes have a go at handling attributes, then
    // recycle them...
    handleStyledAttributes(a);
    a.recycle();

    // Finally update the UI for the modes
    updateUIForMode();//根据LoadingLayout数量的变化,添加和删除UI中对应的LoadingLayout
}
  • 设置
    为Mode提供了setter方法。
    分别为两个监听器提供了setOnRefreshListener()方法。
2、处理

由于pull功能的工作流程:事件拦截->消耗,因此该类的关键方法在onInterceptTouchEvent()、onTouchEvent()这两个方法。
PullToRefreshBase类的源码解析:

  • 事件拦截:onInterceptTouchEvent()
@Override
public final boolean onInterceptTouchEvent(MotionEvent event) {
    //事件拦截的流程:
    //1. enabled:header或footer被允许pull。
    //2. filter action:过滤掉ACTION_CANCEL、ACTION_UP等action。
    //3. ready for pull(abstract):内容区域自身的内容处于顶部或底部。
    //4. 滑动距离正确:orientation 方向上滑动距离大于其垂直方向上的距离。
    //5. 滑动方向正确:(pull_from_start && 滑动方向向下/右) || (pull_from_end && 滑动方向向上/左)。
    //6. 拦截

    //1、enabled
    if (!isPullToRefreshEnabled()) {
        return false;
    }

    final int action = event.getAction();
    //2、filter action
    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        mIsBeingDragged = false;
        return false;
    }

    if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
        return true;
    }

    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            // If we're refreshing, and the flag is set. Eat all MOVE events
            //处于refreshing状态&&不允许滑动时,拦截。
            if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
                return true;
            }

            //3、ready for pull
            if (isReadyForPull()) {
                final float y = event.getY(), x = event.getX();
                final float diff, oppositeDiff, absDiff;

                // We need to use the correct values, based on scroll
                // direction
                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);

                //4、滑动距离正确:大于系统默认的最小距离 
                //&& (!mFilterTouchEvents || orientation方向上滑动距离大于其垂直方向上滑动距离)
                //mFilterTouchEvents 不用管,没怎么用到,默认值false。
                if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {
                    //5、滑动方向正确
                    if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {
                        mLastMotionY = y;
                        mLastMotionX = x;
                        mIsBeingDragged = true;//6、拦截
                        if (mMode == Mode.BOTH) {
                            mCurrentMode = Mode.PULL_FROM_START;
                        }
                    } else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {
                        mLastMotionY = y;
                        mLastMotionX = x;
                        mIsBeingDragged = true;//6、拦截
                        if (mMode == Mode.BOTH) {
                            mCurrentMode = Mode.PULL_FROM_END;
                        }
                    }
                }
            }
            break;
        }
        case MotionEvent.ACTION_DOWN: {
            if (isReadyForPull()) {
                mLastMotionY = mInitialMotionY = event.getY();
                mLastMotionX = mInitialMotionX = event.getX();
                mIsBeingDragged = false;
            }
            break;
        }
    }

    return mIsBeingDragged;
}
  • 消耗:onTouchEvent()
// - 消耗的流程
//  1. 滑动的流程
//    1.1 移动ptr:显示或隐藏相应LoadingLayout并据滑动距离移动整个ptr的内容。
//    1.2 动画:改变LoadingLayout中图片的旋转角度。=>(abstract 方法)
//  2. 更改状态的流程=>State标志
//    2.1 改变PullToRefresh当前状态
//    2.2 改变相应LoadingLayout的状态。=>(abstract 方法)
//  3. load
//  3.1 调用监听器方法。=>(abstract 方法)

@Override
public final boolean onTouchEvent(MotionEvent event) {  
    if (!isPullToRefreshEnabled()) {
        return false;
    }

    // If we're refreshing, and the flag is set. Eat the event
    //(不允许refreshing时滑动 && ptr处于refreshing)   
    //消耗事件,但是注意,onTouchEvent()对它没有进行任何处理,直接返回true。
    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();//1. 滑动的流程
                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);// - 2. 更改状态的流程=>State标志
                    return true;
                }

                // If we're already refreshing, just scroll back to the top
                //当允许处于refreshing状态时响应滑动,在滑动释放后,复原到原先refreshing时位置。
                if (isRefreshing()) {
                    smoothScrollTo(0);
                    return true;
                }

                // If we haven't returned by here, then we're not in a state
                // to pull, so just reset
                setState(State.RESET);//2. 更改状态的流程=>State标志

                return true;
            }
            break;
        }
    }

    return false;
}

1、滑动的流程:pullEvent()

// - 消耗的流程
//  1. 滑动的流程
//    1.1 移动ptr:显示或隐藏相应LoadingLayout并据滑动距离移动整个ptr的内容。
//    1.2 动画:改变LoadingLayout中图片的旋转角度。=>(abstract 方法)
//  2. 更改状态的流程=>State标志
//    2.1 改变PullToRefresh当前状态
//    2.2 改变相应LoadingLayout的状态。=>(abstract 方法)
//  3. load
//  3.1 调用监听器方法。=>(abstract 方法)

private void pullEvent() {
    final int newScrollValue;
    final int itemDimension;
    final float initialMotionValue, lastMotionValue;

    switch (getPullToRefreshScrollDirection()) {
        case HORIZONTAL:
            initialMotionValue = mInitialMotionX;//ACTION_DOWN时的x坐标。
            lastMotionValue = mLastMotionX;//最近一个ACTION_UP时的x坐标。
            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;
    }

    //1.1 移动ptr:显示或隐藏相应LoadingLayout并据滑动距离移动整个ptr的内容。
    setHeaderScroll(newScrollValue);

    if (newScrollValue != 0 && !isRefreshing()) {
        float scale = Math.abs(newScrollValue) / (float) itemDimension;
        switch (mCurrentMode) {
            case PULL_FROM_END:
                //1.2 动画:改变LoadingLayout中图片的旋转角度。=>(abstract 方法)
                mFooterLayout.onPull(scale);
                break;
            case PULL_FROM_START:
            default:
                mHeaderLayout.onPull(scale);
                break;
        }

        if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) {
            //2. 更改状态的流程
            setState(State.PULL_TO_REFRESH);
        } else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) {
            //2. 更改状态的流程
            setState(State.RELEASE_TO_REFRESH);
        }
    }
}

滑动冲突的解决可参考:
不清楚View的滑动可参考:Android 3种坐标系、View在各坐标系下获取自身坐标的方法、View的滑动和scroll方法

2、更改状态的流程:setState(State state)

// - 消耗的流程
//  1. 滑动的流程
//    1.1 移动ptr:显示或隐藏相应LoadingLayout并据滑动距离移动整个ptr的内容。
//    1.2 动画:改变LoadingLayout中图片的旋转角度。=>(LoadingLayout的abstract 方法)
//  2. 更改状态的流程=>State标志
//    2.1 改变PullToRefresh当前状态
//    2.2 改变相应LoadingLayout的状态。=>(LoadingLayout的abstract 方法)
//  3. load
//  3.1 调用监听器方法。=>(监听器的abstract 方法)

final void setState(State state, final boolean... params) {
    //2.1 改变PullToRefresh当前状态
    mState = state;
    if (DEBUG) {
        Log.d(LOG_TAG, "State: " + mState.name());
    }

    switch (mState) {
        case RESET:
            //2.2 改变相应LoadingLayout的状态。=>(LoadingLayout的abstract 方法)
            //注意:下面的方法是PullToRefreshBase的,在它里面会调用LoadingLayout的abstract方法。
            onReset();
            break;
        case PULL_TO_REFRESH:
            onPullToRefresh();
            break;
        case RELEASE_TO_REFRESH:
            onReleaseToRefresh();
            break;
        case REFRESHING:
        case MANUAL_REFRESHING:
            //注意:下面的方法是PullToRefreshBase的,在它里面会调用LoadingLayout的abstract方法。
            //而且由于是refreshing状态,因此也会调用监听器的abstract方法。
            onRefreshing(params[0]);
            break;
        case OVERSCROLLING:
            // NO-OP
            break;
    }

    // Call OnPullEventListener
    if (null != mOnPullEventListener) {
        //  3. load
        //  3.1 调用监听器方法。=>(监听器的abstract 方法)
        mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
    }
}
3、输出

只是回调了监听器方法去进行数据刷新操作。

PullToRefreshScrollView的实现

PullToRefreshBase这一父类是根据抽象功能设计的,而抽象功能又是基于抽象的UI结构的,因此它的子类必须实现以下两点:
抽象的UI结构 => createRefreshableView()。
抽象功能的工作流程 => 判断当前content的内容是否处于顶部\底部,它们的判断方法不同,因此PullToRefreshBase需要提供两个不同的abstract方法:isReadyForPullStart()、isReadyForPullEnd()。

具体源码如下:

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;
    }

    //创建具体的RefreshableView
    @Override
    protected ScrollView createRefreshableView(Context context, AttributeSet attrs) {
        ScrollView scrollView;
        if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
            scrollView = new InternalScrollViewSDK9(context, attrs);
        } else {
            scrollView = new ScrollView(context, attrs);
        }

        scrollView.setId(R.id.scrollview);
        return scrollView;
    }

    @Override
    protected boolean isReadyForPullStart() {//ScrollView是否已滑动到顶部
        return mRefreshableView.getScrollY() == 0;
    }

    @Override
    protected boolean isReadyForPullEnd() {//ScrollView是否已滑动到底部
        View scrollViewChild = mRefreshableView.getChildAt(0);
        if (null != scrollViewChild) {
            return mRefreshableView.getScrollY() >= (scrollViewChild.getHeight() - getHeight());
        }
        return false;
    }

    @TargetApi(9)
    final class InternalScrollViewSDK9 extends ScrollView {

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

        @Override
        protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX,
                int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {

            final boolean returnValue = super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
                    scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);

            // Does all of the hard work...
            OverscrollHelper.overScrollBy(PullToRefreshScrollView.this, deltaX, scrollX, deltaY, scrollY,
                    getScrollRange(), isTouchEvent);

            return returnValue;
        }

        /**
         * Taken from the AOSP ScrollView source
         */
        private int getScrollRange() {
            int scrollRange = 0;
            if (getChildCount() > 0) {
                View child = getChildAt(0);
                scrollRange = Math.max(0, child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
            }
            return scrollRange;
        }
    }
}

总结


关键:弄清抽象功能,然后根据“输入(合法性、创建、初始化、设置)->处理(逻辑操作,即抽象功能的工作流程)->输出(释放、返回)”,依次:入参的合法性、Field的创建(如:来自标志位)、初始化、设置;代码实现工作流程,用标志位区分具体PullToRefresh,用abstract方法代替某些步骤(比如有太多种类型时,区分麻烦,而且会加大耦合性);输出;

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值