(4.2.38.3)UltraPullToRefreshWithLoadMore (为UltraPullToRefresh添加上拉加载更多功能)

下拉刷新和上拉加载应该是当前手机应用中最普遍的一个操作了Android本身提供了一个下拉刷新库,在support-v4包中的SwipeRefreshLayout。但是这个库支持的效果比较单一,只能实现列表不动,刷新头部下拉滑出的效果。并且也没能提供上拉加载的功能。在项目初期的时候,也是因为调研不足,选择了比较经典的PullToRefresh库。然而这个库已经停止更新了,对新的控件(如RecyclerView)并不能够支持。同时,在使用过程中也出现了很多诟病(无法显示分隔线等)。在经历了之前的框架重构之后,我决定替换掉它。当然,项目已经开发近一年,替换掉一个常用控件的重构工作并不轻松。因此,选择需要相对谨慎。

在经过一段时间的搜索之后,我发现只有UltraPullToRefresh能够满足我的需求:能够适配所有的View。这个库是由国内大牛廖祜秋开源出来的,通过接口的形式支持了所有View的适配,以及自定义下拉刷新头部的功能。之前,我就已经阅读过了关于UltraPullToRefresh的分析,也根据学习的结果开发出了自己的一个动画效果库PseudoMaterialHeader。但是,UltraPullToRefresh却很”傲娇”的表示不支持上拉加载更多,因为他认为这两个功能不属于同一个层次的功能。相应的,在其Readme中也给出了实现上拉加载的一个解决方案,即使用cubeSDK中的对应View。然而,这个解决方案只是通过给ListView和GridView添加Footer来实现,效果和扩展性都不好,另外也额外增加了一层布局。这样的效果显然是不能令人满意的,因此,我决定自己拓展一个带上拉加载的库。

Ultra-Pull-To-Refresh-With-Load-More

项目地址:https://github.com/captainbupt/android-Ultra-Pull-To-Refresh-With-Load-More

这个库是从UltraPullToRefresh库fork过来的,已经发起Pull Request,不过原作者只是回信了,并没有Merge进去。因此,想要使用该库,就必须下载下来,手动导入到IDE中去。

下面,对修改过程进行一下讲解。

PtrFrameLayout

PtrFrameLayout是一个核心类,它本质上是一个ViewGroup,目的是包含需要下拉刷新的View,Header和Footer。作为所有控件的Parent View,它能够接收到所有屏幕触碰事件,并选择是否下发,这也是其实现下拉刷新的保障。下面,我就来具体说一下下关键的函数,以及相关的改动。

PtrIndicator

PtrIndicator是在PtrFrameLayout用来记录Header相关属性的工具类,包含了高度,当前偏移位置,阻抗等等必要信息。一开始我本来打算是给Footer也单独定义一个PtrIndicator的。但是后来发现,这样做,就需要把PtrFrameLayout所有关于PtrIndicator的操作都重复一遍,显得十分的冗余。另外,我也注意到,Header和Footer是不可能同时出现的(不可能同时下拉刷新和上拉加载)。因此,PtrFrameLayout中的PtrIndicator可以直接复用,只需要再添加一个布尔变量标记为Header或者Footer,需要进行不同操作的时候再进行判断即可。

private boolean isHeader = true;

public boolean isHeader() {
    return isHeader;
}

public void setIsHeader(boolean isHeader) {
    this.isHeader = isHeader;
}

onFinishInflate

当布局文件加载完成后,这个方法会被调用。原有库中做的操作是判断是否存在Header和Content View,并作相应赋值。在此,我也就简单拓展了一下。即当存在3个子View的时候,分别检测它们分别属于哪个部分(Header,Content,Footer)。这个部分并没有进行过调试,因为一般情况下,都是只包含一个Content View作为展示的。

// not specify header or content or footer
if (mContent == null || mHeaderView == null || mFooterView == null) {
    final View child1 = getChildAt(0);
    final View child2 = getChildAt(1);
    final View child3 = getChildAt(2);
    // all are not specified
    if (mContent == null && mHeaderView == null && mFooterView == null) {
        mHeaderView = child1;
        mContent = child2;
        mFooterView = child3;
    }
    // only some are specified
    else {
        ArrayList<View> view = new ArrayList<View>(3) {{
            add(child1);
            add(child2);
            add(child3);
        }};
        if (mHeaderView != null) {
            view.remove(mHeaderView);
        }
        if (mContent != null) {
            view.remove(mContent);
        }
        if (mFooterView != null) {
            view.remove(mFooterView);
        }
        if (mHeaderView == null && view.size() > 0) {
            mHeaderView = view.get(0);
            view.remove(0);
        }
        if (mContent == null && view.size() > 0) {
            mContent = view.get(0);
            view.remove(0);
        }
        if (mFooterView == null && view.size() > 0) {
            mFooterView = view.get(0);
            view.remove(0);
        }
    }
}

onMeasure

在这个类中,就可以分别获取到Header和Footer的高度了。

    if (mHeaderView != null) {
        measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0);
        MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
        mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        mPtrIndicator.setHeaderHeight(mHeaderHeight);
    }

    if (mFooterView != null) {
        measureChildWithMargins(mFooterView, widthMeasureSpec, 0, heightMeasureSpec, 0);
        MarginLayoutParams lp = (MarginLayoutParams) mFooterView.getLayoutParams();
        mFooterHeight = mFooterView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        mPtrIndicator.setFooterHeight(mFooterHeight);
    }

layoutChildren

这个类是用来指定各个布局的边界的。正如前面所说,PtrIndicator中包含了当前的偏移量,即Header或者Footer展示出来的高度。根据这个值,我们就需要相应的将Header/Footer和Content进行上移或者下移,从而使得Header和Footer可以被显示。

整个过程中,视图范围是固定的,即整个控件的高宽是一定的。超出的部分就会被自动隐藏掉。例如,当Header的上边框为负值时,Header应当是隐藏或者部分显示的。对于Footer也是同样的道理。根据偏移量的不断变化,不断的设定这几个部分的上下边框,就可以达到上拉和下拉的效果。

    if (mHeaderView != null) {
        MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
        final int left = paddingLeft + lp.leftMargin;
        final int top = paddingTop + lp.topMargin + offsetHeaderY - mHeaderHeight;
        final int right = left + mHeaderView.getMeasuredWidth();
        final int bottom = top + mHeaderView.getMeasuredHeight();
        mHeaderView.layout(left, top, right, bottom);
    }
    if (mContent != null) {
        MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams();
        int left;
        int top;
        int right;
        int bottom;
        if (mPtrIndicator.isHeader()) {
            left = paddingLeft + lp.leftMargin;
            top = paddingTop + lp.topMargin + (isPinContent() ? 0 : offsetHeaderY);
            right = left + mContent.getMeasuredWidth();
            bottom = top + mContent.getMeasuredHeight();
        } else {
            left = paddingLeft + lp.leftMargin;
            top = paddingTop + lp.topMargin - (isPinContent() ? 0 : offsetFooterY);
            right = left + mContent.getMeasuredWidth();
            bottom = top + mContent.getMeasuredHeight();
        }
        contentBottom = bottom;
        mContent.layout(left, top, right, bottom);
    }
    if (mFooterView != null) {
        MarginLayoutParams lp = (MarginLayoutParams) mFooterView.getLayoutParams();
        final int left = paddingLeft + lp.leftMargin;
        final int top = paddingTop + lp.topMargin + contentBottom - (isPinContent() ? offsetFooterY : 0);
        final int right = left + mFooterView.getMeasuredWidth();
        final int bottom = top + mFooterView.getMeasuredHeight();
        mFooterView.layout(left, top, right, bottom);
    }

dispatchTouchEvent

这个就是用来处理触碰事件的关键类了。在UltraPullToRefresh中,其中心思想就是只处理不拦截。即,所有的事件都会通过调用super.dispatchTouchEvent(e),最终被传递到子类中去,这就可以保证子View的事件不会被覆盖。而当上拉或者下拉事件开始处理的时候,那么就会向子View发送Cancel事件,使得上拉和下拉不会被其他事件打断。

当接收到一个ACTION_DOWN事件的时候,会将该坐标记录到PtrIndicator中。当ACTION_MOVE时,就可以根据这些信息判断出移动距离和方向。接着,判断Header和Footer是否已经显示,或者判断是否可以显示Header和Footer(mPtrHandler.checkCanDoRefresh()是开发人员可以定义的结果)。获取到这些判断信息后,就可以决定下一步的工作了:

  • 如果Header或者Footer都没有显示,那么就可以根据滑动方向,和是否显示Header和Footer来判断,是否需要开始显示Header或者Footer。如果显示,则开始设置PtrIndicator的偏移量,并作相关判断(是否达到刷新高度等)。如果不需要显示,则直接将事件传递给子View即可。
  • 如果Header或者Footer已经显示,那么就只需要根据滑动距离更新PtrIndicator的偏移量即可。因为这个时候,必然是处于上拉或者下拉的过程中,不能够让子类去进行处理,也不应当产生其他的事件。

            boolean moveDown = offsetY > 0;
            boolean moveUp = !moveDown;
    
            boolean canMoveUp = mPtrIndicator.isHeader() && mPtrIndicator.hasLeftStartPosition(); // if the header is showing
    
            boolean canMoveDown = mFooterView != null && !mPtrIndicator.isHeader() && mPtrIndicator.hasLeftStartPosition(); // if the footer is showing
    
            boolean canHeaderMoveDown = mPtrHandler != null && mPtrHandler.checkCanDoRefresh(this, mContent, mHeaderView) && (mMode.ordinal() & 1) > 0;
            boolean canFooterMoveUp = mPtrHandler != null && mFooterView != null // The footer view could be null, so need double check
                    && mPtrHandler instanceof PtrHandler2 && ((PtrHandler2) mPtrHandler).checkCanDoLoadMore(this, mContent, mFooterView) && (mMode.ordinal() & 2) > 0;
    
            if (DEBUG) {
                PtrCLog.v(LOG_TAG, "ACTION_MOVE: offsetY:%s, currentPos: %s, moveUp: %s, canMoveUp: %s, moveDown: %s: canMoveDown: %s canHeaderMoveDown: %s canFooterMoveUp: %s", offsetY, mPtrIndicator.getCurrentPosY(), moveUp, canMoveUp, moveDown, canMoveDown, canHeaderMoveDown, canFooterMoveUp);
            }
    
            // if either the header and footer are not showing
            if (!canMoveUp && !canMoveDown) {
                // disable move when header not reach top
                if (moveDown && !canHeaderMoveDown) {
                    return dispatchTouchEventSupper(e);
                }
                if (moveUp && !canFooterMoveUp) {
                    return dispatchTouchEventSupper(e);
                }
    
                // should show up header
                if (moveDown) {
                    moveHeaderPos(offsetY);
                    return true;
                }
    
                // should show up footer
                if (moveUp) {
                    moveFooterPos(offsetY);
                    return true;
                }
            }
    
            // if header is showing, then no need to move footer
            if (canMoveUp) {
                moveHeaderPos(offsetY);
                return true;
            }
    
            // if footer is showing, then no need to move header
            if (canMoveDown) {
                moveFooterPos(offsetY);
                return true;
            }
    

这几个关键函数修改完成后,其他函数的修改基本只需要根据PtrIndicator中的isHeader来进行不同的处理即可(基本上只是将offset反转的操作)。

PtrHandler2

原有的PtrHandler中,只包含了下拉刷新所需要的方法。为了保证向下兼容性,我定义了PtrHandler2并继承了PtrHandler。这样,就可以保证使用这个库的时候不会和之前的部分产生冲突。同样的,作为默认的PtrDefaultHandler,我也提供了PtrDefaultHandler2来实现默认的判断方法。

public static boolean canChildScrollDown(View view) {
    if (android.os.Build.VERSION.SDK_INT < 14) {
        if (view instanceof AbsListView) {
            final AbsListView absListView = (AbsListView) view;
            return absListView.getChildCount() > 0
                    && (absListView.getLastVisiblePosition() < absListView.getChildCount() - 1
                    || absListView.getChildAt(absListView.getChildCount() - 1).getBottom() > absListView.getPaddingBottom());
        } else if (view instanceof ScrollView) {
            ScrollView scrollView = (ScrollView) view;
            if (scrollView.getChildCount() == 0) {
                return false;
            } else {
                return scrollView.getScrollY() < scrollView.getChildAt(0).getHeight() - scrollView.getHeight();
            }
        } else {
            return false;
        }
    } else {
        return view.canScrollVertically(1);
    }
}

Mode

为了提供更加多样化的操作,我提供了Mode属性。通过设置Mode属性,开发人员就可以随时开启或者禁用PtrFrameLayout的上拉或者下拉功能。

public enum Mode {
    NONE, REFRESH, LOAD_MORE, BOTH
}

不足

这个修改是因为项目需要,临时赶工而成,很多地方都没有完善。例如:没能将Header和Footer完全分开,因此,Header和Footer的所有属性必然保持一致。不能满足更加多样化的需求。另外,在使用过程中,我也在不断的发现BUG,并进行修复。

总结

修改完之后UltraPullToRefreshWithLoadMore,我已经正式使用到项目中,目前表现良好。另外,这也是我第一次fork并发起PullRequest的git操作,尽管目前还没能被成功Merge进去,还是给了我不少信心。这次经历之后,我也会慢慢开始对开源库进行自己的修改,来帮助开源社区的发展。

PS: 目前已经Merge成功的一个开源库:CircleProgressView

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值