下拉刷新上拉加载数据列表实现(Kotlin版)

来源:互联网 发布:java string 去空格 编辑:程序博客网 时间:2024/05/29 15:10
     在Android开发中,我们90%的时间是在操作列表控件,而操作列表控件时候,实现下拉刷新上拉加载数据是最普通最频繁的需求。虽然这样数据刷新的框架很多,但是我们作为一位脱离低级趣味,一位有追求的程序员岂能只满足用别人的轮子,今天我们也从0到1实现这个加载框架,这里我选择用kotlin语言实现。     第一步我们来分析要实现这个框架的原理。     第二步然后用代码的方式庖丁解牛的方式实现其细节。     第三步做个demo来展示实现的最终效果。     首先,实现这个框架原理就是一句话“下拉时候头部控件展示用以刷新,上拉时候底部控件显示用以加载”。此话怎么理解了,就是这个控件由三个部分组成,如果这个控件是垂直方向展示的,那么由头部控件,数据列表控件,底部控件组成。 如图所示:**

这里写图片描述

默认情况下,只有数据展示控件展 示到用户面前,头部控件与底部控件是隐藏的。如图所示:

这里写图片描述

    当用户下拉某一个高度的时候,其头部控件缓缓显示,下拉一定的距离之后,其头部控件完全显示,并且显示刷新数据的效果。这样效果用图表示这样:

这里写图片描述

    上拉加载效果亦然,效果如图所示:

这里写图片描述

    有了这样大概思路之后,我们就开始撸起袖子加油干——上代码。    这个控件其本质也是一个自定义控件的范畴,既然是自定义控件,我们就应该遵守自定义控件三原则。我们回顾一下:    一、在OnMeasure方法中,对控件的大小尺寸进行测量,但是,经过上面分析,这是一种典型的控件容器,我们选择继承与LinearLayout,貌似Onmeasure方法我们这里不需要进行处理。    二、在OnDraw方法中,对控件的进行绘制,这里貌似我们也用不到了。    三、而既然是下拉刷新上拉加载更多的话,永远逃不过一个永恒的话题对用户的手势进行处理。因此对控件的onInterceptTouchEvent(event: MotionEvent)与override fun onTouchEvent(event: MotionEvent): Boolean方法进行探讨。    闲话少说,我们首先分析一下onInterceptTouchEvent(event: MotionEvent)方法中做了那些处理。对于这个方法我们应当有这样的基本常识,倘若return false,就是让其子控件响应其touch方法,倘若return true的话,是在onTouchEvent(event: MotionEvent)中处理touch事件。这是基本知识铺垫。接下来,我们就上代码了。
    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {        if (!isPullToRefreshEnabled()) {            return false;        }        var action = event.getAction();        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {            mIsBeingDragged = false;            return false;        }        if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {            return true;        }        when (action) {            MotionEvent.ACTION_MOVE -> {                // If we're refreshing, and the flag is set. Eat all MOVE events                if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {                    return true;                }                if (isReadyForPull()) {                    var y = event.getY()                    var x = event.getX();                    var diff: Float = 0f                    var oppositeDiff: Float = 0f                    var absDiff: Float = 0f                    // We need to use the correct values, based on scroll                    // direction                    when (getPullToRefreshScrollDirection()) {                        Orientation.HORIZONTAL -> {                            diff = x - mLastMotionX                            oppositeDiff = y - mLastMotionY;                        }                        Orientation.VERTICAL -> {                            diff = y - mLastMotionY;                            oppositeDiff = x - mLastMotionX;                        }                    }                    absDiff = Math.abs(diff);                    if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {                        if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {                            mLastMotionY = y;                            mLastMotionX = x;                            mIsBeingDragged = true;                            if (mMode == Mode.BOTH) {                                mCurrentMode = Mode.PULL_FROM_START;                            }                        } else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {                            mLastMotionY = y;                            mLastMotionX = x;                            mIsBeingDragged = true;                            if (mMode == Mode.BOTH) {                                mCurrentMode = Mode.PULL_FROM_END;                            }                        }                    }                }            }            MotionEvent.ACTION_DOWN -> {                if (isReadyForPull()) {                    mInitialMotionY = event.getY()                    mLastMotionY = event.getY()                    mLastMotionX = event.getX()                    mInitialMotionX = event.getX();                    mIsBeingDragged = false;                }            }        }        return mIsBeingDragged;    }
    在此方法中,我们首先判断这个控件下拉上拉刷新的事件是否正常开启的,判断是否正常开启我们只需用一个布尔变量判断就可以了,如果不能正常刷新的话,我们就只需要要此控件的子控件来响应OnTouch事件。具体怎么理解了,就是不正常刷新的时候,就不需要在本控件中进行事件拦截,交给他的子控件处理。然后,我们来判断他是否触发了MotionEvent.ACTION_CANCEL与ACTION_UP事件,如果触发之后,我们将其touch事件交给onTouch事件处理。紧接着,我们判断他是否触发了MotionEvent.ACTION_DOWN事件,如果是触发了,并且这个控件是准备下拉,如果判断准备下拉,我们分三种状态他是否可以刷新。我们记录下他按下x,Y坐标。 然后了,我们判断他是否MotionEvent.ACTION_MOVE事件,倘若是触发了,我们先判断他是否开始刷新,如果是开始刷新,我们就让子控件处理。然后我们一样判断他是否下拉,这里我多说一嘴子,我们这里刷新数据列表分为垂直方向,水平方向上的列表处理。普通列表其实只是垂直方向展示就够了,一些特殊的情况下有水平方向展示列表的,就像竹简一样。判断这个移动的距离是否大于一定的阈值,并且他在该方向位移偏移量是否大于另一方向上的偏移量,譬如说一个垂直方向的列表在垂直方向的偏移量是否大于在水平方向上偏移量。下拉刷新时候我们就判断头部列表控件是否显示,并且偏移量是否大于一定的值,并且准备下拉刷新,我们就将touch事件交给OnTouch事件处理, 此时列表Mode(模式)是从上向下刷新。同理,上拉加载时候我们就判断尾部列表控件是否显示,并且偏移量是否大于一定的值,并且准备上拉加载,我们就将touch事件交给OnTouch事件处理, 此时列表Mode(模式)是从底向上刷新。这就是此方法大概思路。思维导图如下:

这里写图片描述

    然后了,在OnInterceptTouch事件return true 都是交给OnTouch事件进行处理。我们就窥一窥他的全貌。小二上代码:
   override fun onTouchEvent(event: MotionEvent): Boolean {        if (!isPullToRefreshEnabled()) {            return false;        }        // If we're refreshing, and the flag is set. Eat the event        if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {            return true;        }        if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {            return false;        }        when (event.getAction()) {            MotionEvent.ACTION_MOVE -> {                if (mIsBeingDragged) {                    mLastMotionY = event.getY();                    mLastMotionX = event.getX();                    pullEvent();                    return true;                }            }            MotionEvent.ACTION_DOWN -> {                if (isReadyForPull()) {                    mInitialMotionY = event.getY();                    mLastMotionY = event.getY();                    mInitialMotionX = event.getX();                    mLastMotionX = event.getX()                    return true;                }            }            MotionEvent.ACTION_UP -> {                if (mIsBeingDragged) {                    mIsBeingDragged = false;                    if (mState == State.RELEASE_TO_REFRESH                            && (null != mOnRefreshListener || null != mOnRefreshListener2)) {                        setState(State.REFRESHING, true);                        return true;                    }                    // If we're already refreshing, just scroll back to the top                    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,false);                    return true;                }            }        }        return false;    }
    首先跟OnInterceptTouch()一样判断其是否可以刷新,如果不能刷新的话,就不响应OnTouch事件。然后判断其是否响应刷新事件,如果不响应的话,就让子控件处理OnTouch事件。然后判断用户按下位置是否在屏幕边缘的位置,如果是的话,也不处理OnTouch事件。下面就进入这个方法的核心部分了,首先了在   MotionEvent.ACTION_DOWN条件下判断是否开始是否可以拖拽刷新了,是否拖拽刷新条件上文已经提过,这里不在做长篇累牍的赘述。如果是可以拖拽的,记录其初始位置的值,然后让控件来处理事件。然后了在MotionEvent.ACTION_MOVE条件下重新记录最终按下位置,这个对后面判断有很重要的作用。 最后在MotionEvent.ACTION_UP条件下判断控件是否已经拖拽的状态,如果是的话,就将其布尔变量变成FALSE,便于控制。判断此控件状态是否是RELEASE_TO_REFRESH(松开刷新)的状态,如果是的话,就将其控件的状态变成REFRESHING(正在刷新),并且让子控件响应。如果是已经刷新的状态,就让其控件重置为0,既变成初始化的状态,并且让子控件响应。这就是这个方法的大体流程,思维导图这样子:

这里写图片描述

    说了最关键的两个Touch事件,我们接下来说什么了,说一下这几个刷新控件几种状态切换的方法。这几种方法切换其实我们在上面OnTouch事件已经调用过了,他就是SetState()方法,他就长成这样:
      fun setState( state:State,  params:Boolean)    {        mState = state;        if (DEBUG) {            Log.d(LOG_TAG, "State: " + mState.name);        }        when(mState) {             State.RESET->            onReset();            State.PULL_TO_REFRESH ->            onPullToRefresh()             State.RELEASE_TO_REFRESH->            onReleaseToRefresh();            State.REFRESHING->                onRefreshing(params);             State.MANUAL_REFRESHING ->             onRefreshing(params)        }        // Call OnPullEventListener        if (null != mOnPullEventListener) {            mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);        }    }
    控件如果是RESET状态,就调用OnReset()方法重置控件,    控件如果是PULL_TO_REFRESH状态,就调用onPullToRefresh()方法拖拽刷新控件。    控件如果是RELEASE_TO_REFRESH状态,就调用onReleaseToRefresh()方法释放刷新控件。    控件如果是REFRESHING与MANUAL_REFRESHING方法就调用onRefreshing()方法刷新列表。    这里还有一个回调接口来监听不同状态。由于这个方法本身逻辑很清晰,这里就不需要画思维导图了,一目了然。    我们接下来看一看这几个状态下,不同的方法。    首当其冲是OnReset()方法,代码如下:
    open fun onReset() {        mIsBeingDragged = false;        mLayoutVisibilityChangesEnabled = true;        // Always reset both layouts, just in case...        mHeaderLayout.reset();        mFooterLayout.reset();        smoothScrollTo(0);    }
    非常简单,将头部控件,尾部控件重置,控制变量重置,控件滚动到开始的位置。    第二个介绍的是onPullToRefresh()方法,代码如下:
          open fun onPullToRefresh() {        when (mCurrentMode) {            Mode.PULL_FROM_END ->                mFooterLayout.pullToRefresh();            Mode.PULL_FROM_START ->                mHeaderLayout.pullToRefresh();        }    }
    也挺简单,如果是下拉刷新,头部控件开始可以刷新,如果是上拉加载,底部控件开始加载。    第三个介绍的是OnReleaseRefresh()方法,代码如下:
        open fun onReleaseToRefresh() {        when (mCurrentMode) {            Mode.PULL_FROM_START ->                mFooterLayout.releaseToRefresh();            Mode.PULL_FROM_END ->                mHeaderLayout.releaseToRefresh();        }    }
    同上面如出一辙,不做说明。    最后是OnRefresh()方法,代码如下:
         public open fun onRefreshing(doScroll: Boolean) {        if (mMode.showHeaderLoadingLayout()) {            mHeaderLayout.refreshing();        }        if (mMode.showFooterLoadingLayout()) {            mFooterLayout.refreshing();        }        if (doScroll) {            if (mShowViewWhileRefreshing) {                // Call Refresh Listener when the Scroll has finished                var listener = object : OnSmoothScrollFinishedListener {                    override fun onSmoothScrollFinished() {                        callRefreshListener()                    }                }                when (mCurrentMode) {                    Mode.MANUAL_REFRESH_ONLY->                        smoothScrollTo(getFooterSize(), listener);                    Mode.PULL_FROM_START -> {                        smoothScrollTo(getFooterSize(), listener);                    }                    Mode.PULL_FROM_END ->                        smoothScrollTo(-getHeaderSize(), listener);                }            } else {                smoothScrollTo(0);            }        } else {            // We're not scrolling, so just call Refresh Listener now            callRefreshListener();        }    }
这个方法不同上述方法,我一一给你分析分析,首先头部控件底部控件与底部控件是否正常刷新,然后判断他是否可以正常的滚动,如果是正常的滚动的话根据控件三种不同的状态滚动不同的距离,否则的就调用刷新接口进行刷新。响应的思维导图如下:

这里写图片描述

这个控件大体骨架有了,还说一个方法——当其控件尺寸发生改变的方法中我们这是需要将其内边距进行设置的方法 ,这样就完美了。这个方法叫做refreshLoadingViewsSize()方法,代码如下:
          fun refreshLoadingViewsSize() {        var maximumPullScroll = (getMaximumPullScroll() * 1.2f) as Int;        var pLeft = getPaddingLeft();        var pTop = getPaddingTop();        var pRight = getPaddingRight();        var pBottom = getPaddingBottom();        when (getPullToRefreshScrollDirection()) {            Orientation.HORIZONTAL -> {                if (mMode.showHeaderLoadingLayout()) {                    mHeaderLayout.setWidth(maximumPullScroll);                    pLeft = -maximumPullScroll;                } else {                    pLeft = 0;                }                if (mMode.showFooterLoadingLayout()) {                    mFooterLayout.setWidth(maximumPullScroll);                    pRight = -maximumPullScroll;                } else {                    pRight = 0;                }            }            Orientation.VERTICAL -> {                if (mMode.showHeaderLoadingLayout()) {                    mHeaderLayout.setHeight(maximumPullScroll);                    pTop = -maximumPullScroll;                } else {                    pTop = 0;                }                if (mMode.showFooterLoadingLayout()) {                    mFooterLayout.setHeight(maximumPullScroll);                    pBottom = -maximumPullScroll;                } else {                    pBottom = 0;                }            }        }        if (DEBUG) {            Log.d(LOG_TAG, String.format("Setting Padding. L: %d, T: %d, R: %d, B: %d", pLeft, pTop, pRight, pBottom));        } setPadding(pLeft, pTop, pRight, pBottom);    }
    计算出可以滚动最大位置,最大位置根据一定条件进行计算。如果垂直方向上的数据列表的话,我这里计算头部列表是否正常显示,如果是正常显示的话,我就将左内边距设置为负的可以滚动最大位置,否则的话,左边距设置为0。根据尾部列表是否计算出右边距。反之亦然,如果是水平方向的数据列表,我就计算不同的上边距与下边距。然后将这计算好的上下左右内边距进行复制。思维导图这样:

这里写图片描述

    理解这些方法之后所有拖拽刷新控件的基类都有了,基类搭台子类唱戏,我这里就写一个继承与这个基类的ListView,然后通过一个demo展示我们的效果:    当然了这个ListView不可以直接继承与这个基类,为什么,因为我要抽象出AblistView基类,这样子的话,便于以后各种列表(ListView与RecyleListView)的扩展。那好,我们先看一下PullToRefreshAdapterViewBase的内容,然后看一下PullToRefreshListView的内容    要理解PullToRefreshAdapterViewBase这个类,我们只需要理解addIndicatorViews()添加指示器视图方法,这里我们需要明白一个问题就是指示器是什么玩意,所谓指示器就是提示用户进行上拉下拉刷新的视图,可以是一个简单的指示器箭头,也可以是一副动画。总而言之,他是一个控件。removeIndicatorViews()移除指示器视图方法。isFirstItemVisible()第一个视图是否展示的方法,isLastItemVisible()最后视图是否展示的方法。    首先,介绍的是addIndicatorViews()方法,看代码:
     private fun addIndicatorViews() {        val mode = getMode()        val refreshableViewWrapper = getRefreshableViewWrapper()        if (mode.showHeaderLoadingLayout() && null == mIndicatorIvTop) {            // If the mode can pull down, and we don't have one set already            mIndicatorIvTop = IndicatorLayout(context, PullToRefreshBase.Mode.PULL_FROM_START)            val params = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,                    ViewGroup.LayoutParams.WRAP_CONTENT)            params.rightMargin = resources.getDimensionPixelSize(R.dimen.indicator_right_padding)            params.gravity = Gravity.TOP or Gravity.RIGHT            refreshableViewWrapper.addView(mIndicatorIvTop, params)        } else if (!mode.showHeaderLoadingLayout() && null != mIndicatorIvTop) {            // If we can't pull down, but have a View then remove it            refreshableViewWrapper.removeView(mIndicatorIvTop)            mIndicatorIvTop = null        }        if (mode.showFooterLoadingLayout() && null == mIndicatorIvBottom) {            // If the mode can pull down, and we don't have one set already            mIndicatorIvBottom = IndicatorLayout(context, PullToRefreshBase.Mode.PULL_FROM_END)            val params = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,                    ViewGroup.LayoutParams.WRAP_CONTENT)            params.rightMargin = resources.getDimensionPixelSize(R.dimen.indicator_right_padding)            params.gravity = Gravity.BOTTOM or Gravity.RIGHT            refreshableViewWrapper.addView(mIndicatorIvBottom, params)        } else if (!mode.showFooterLoadingLayout() && null != mIndicatorIvBottom) {            // If we can't pull down, but have a View then remove it            refreshableViewWrapper.removeView(mIndicatorIvBottom)            mIndicatorIvBottom = null        }    }
获取子视图控件容器,如果是需要展示头部控件的话,且头部指示控件为空,就实例化头部指示控件,并且添加到容器中去。如果是添加了头部指示控件,就把头部指示控件移除,添加新的头部控件到容器中去。反之亦然,如果尾部指示控件为空,就实例化尾部指示控件,并且添加到容器中去。如果是添加了尾部指示控件,就把尾部指示控件移除,添加新的尾部控件到容器中去。思维导图如下:

这里写图片描述

   既然是添加指示器中,就移除指示器,上代码:
     private fun removeIndicatorViews() {        if (null != mIndicatorIvTop) {            getRefreshableViewWrapper().removeView(mIndicatorIvTop)            mIndicatorIvTop = null        }        if (null != mIndicatorIvBottom) {            getRefreshableViewWrapper().removeView(mIndicatorIvBottom)            mIndicatorIvBottom = null        }    }
代码挺简单,将头部与尾部指示器从容器中移除。不用思维导图了。然后是判断数据列表的第一个条目是否显示的方法,上代码:
     private fun isFirstItemVisible(): Boolean {        val adapter = mRefreshableView.adapter        if (null == adapter || adapter.isEmpty) {            if (DEBUG) {                Log.d(LOG_TAG, "isFirstItemVisible. Empty View.")            }            return true        } else {            if (mRefreshableView.firstVisiblePosition <= 1) {                val firstVisibleChild = mRefreshableView.getChildAt(0)                if (firstVisibleChild != null) {                    return firstVisibleChild.top >= mRefreshableView.top                }            }        }        return false}
    原理如下:判断他的适配器是否为空,倘若为空就没有第一个条目,然后列表的第一个条目大于头部刷新控件的高度,就返回真,否则返回假。思维导图如下:

这里写图片描述

     最后是判断数据列表的最后条目是否显示的方法,上代码:
     private fun isLastItemVisible(): Boolean {        val adapter = mRefreshableView.adapter        if (null == adapter || adapter.isEmpty) {            if (DEBUG) {                Log.d(LOG_TAG, "isLastItemVisible. Empty View.")            }            return true        } else {            val lastItemPosition = mRefreshableView.count - 1            val lastVisiblePosition = mRefreshableView.lastVisiblePosition            if (DEBUG) {                Log.d(LOG_TAG, "isLastItemVisible. Last Item Position: " + lastItemPosition + " Last Visible Pos: "                        + lastVisiblePosition)            }            if (lastVisiblePosition >= lastItemPosition - 1) {                val childIndex = lastVisiblePosition - mRefreshableView.firstVisiblePosition                val lastVisibleChild = mRefreshableView.getChildAt(childIndex)                if (lastVisibleChild != null) {                    return lastVisibleChild.bottom <= mRefreshableView.bottom                }            }        }        return false    }
    原理同上面方法一样,判断他的适配器是否为空,倘若为空就没有最后条目,然后列表的最后条目小于尾部刷新控件的高度,就返回真,否则返回假。思维导图就免了。    有了这个拖拽列表控件子类,我们就可以专心致志实现PullToRefreshListView这个刷新列表实现类,要理解这个列表类我们只需要弄清楚onRefreshing(doScroll: Boolean)进行数据刷新的方法,onReset()将其数据列表重置的方法,以及handleStyledAttributes(a: TypedArray)初始不同的自定义属性值的方法。首先,我们弄清楚数据刷新的方法,代码长这样:
            override fun onRefreshing(doScroll: Boolean) {        val adapter = mRefreshableView.adapter        if (!mListViewExtrasEnabled || !getShowViewWhileRefreshing() || null == adapter || adapter.isEmpty) {            super.onRefreshing(doScroll)            return        }        super.onRefreshing(false)        var origLoadingView: LoadingLayout?=null        var listViewLoadingView: LoadingLayout?=null        var oppositeListViewLoadingView: LoadingLayout?=null        var selection: Int=0        var scrollToY: Int=0        when (getCurrentMode()) {            PullToRefreshBase.Mode.MANUAL_REFRESH_ONLY, PullToRefreshBase.Mode.PULL_FROM_END -> {                origLoadingView = getFooterLayout()                listViewLoadingView = mFooterLoadingView!!                oppositeListViewLoadingView = mHeaderLoadingView!!                selection = mRefreshableView.count - 1                scrollToY = scrollY - getFooterSize()            }            PullToRefreshBase.Mode.PULL_FROM_START -> {                origLoadingView = getHeaderLayout()                listViewLoadingView = mHeaderLoadingView!!                oppositeListViewLoadingView = mFooterLoadingView!!                selection = 0                scrollToY = scrollY + getHeaderSize()            }        }        origLoadingView!!.reset()        origLoadingView!!.hideAllViews()        oppositeListViewLoadingView!!.visibility = View.GONE         listViewLoadingView!!.visibility = View.VISIBLE        listViewLoadingView!!.refreshing()        if (doScroll) {            disableLoadingLayoutVisibilityChanges()            setHeaderScroll(scrollToY)            mRefreshableView.setSelection(selection)            smoothScrollTo(0)        }    }
     我们首先判断他是否能够正常刷新,如果本身适配器没有元素或者是不能正常刷新的话,就不让列表不进行刷新。然后对列表分不同条件进行考虑,一种是下拉刷新的情况下,该状态下的列表应当刷新的为头部列表控件,并且记录滚动的Y坐标为滚动坐标+头部控件的高度。另一种是上拉加载或者手动刷新的状态,该状态下的列表应当刷新的为尾部列表控件,并且记录滚动的Y坐标为滚动坐标-尾部控件的高度。然后,我们将该显示控件进行显示,该隐藏进行隐藏。并且判断此列表是否真正开启了滚动模式,如果开启之后,我们需要改变加载视图显示值,设置头部的滚动值,并最终将其滚动到初始化的位置。相应的思维导图如下:

这里写图片描述

   紧接着是重置状态的方法,代码如下:
          override fun onReset() {        /**         * If the extras are not enabled, just call up to super and return.         */        if (!mListViewExtrasEnabled) {            super.onReset()            return        }        var originalLoadingLayout: LoadingLayout?=null        var listViewLoadingLayout: LoadingLayout?=null        var scrollToHeight: Int=0        var selection: Int=0        var scrollLvToEdge: Boolean=false        when (getCurrentMode()) {            PullToRefreshBase.Mode.MANUAL_REFRESH_ONLY, PullToRefreshBase.Mode.PULL_FROM_END -> {                originalLoadingLayout = getFooterLayout()                listViewLoadingLayout = mFooterLoadingView!!                selection = mRefreshableView.count - 1                scrollToHeight = getFooterSize()                scrollLvToEdge = Math.abs(mRefreshableView.lastVisiblePosition - selection) <= 1            }            PullToRefreshBase.Mode.PULL_FROM_START-> {                originalLoadingLayout = getHeaderLayout()                listViewLoadingLayout = mHeaderLoadingView!!                scrollToHeight = -getHeaderSize()                selection = 0                scrollLvToEdge = Math.abs(mRefreshableView.firstVisiblePosition - selection) <= 1            }        }        // If the ListView header loading layout is showing, then we need to        // flip so that the original one is showing instead        if (listViewLoadingLayout!!.visibility == View.VISIBLE) {            // Set our Original View to Visible            originalLoadingLayout!!.showInvisibleViews()            // Hide the ListView Header/Footer            listViewLoadingLayout.visibility = View.GONE            /**             * Scroll so the View is at the same Y as the ListView             * header/footer, but only scroll if: we've pulled to refresh, it's             * positioned correctly             */            if (scrollLvToEdge && getState() !== PullToRefreshBase.State.MANUAL_REFRESHING) {                mRefreshableView.setSelection(selection)                setHeaderScroll(scrollToHeight)            }        }        // Finally, call up to super        super.onReset()    }
    我们首先判断他是否能够正常刷新,如果不能进行刷新,就不让列表不进行刷新。然后对列表分不同条件进行考虑,一种是下拉刷新的情况下,该状态下的列表应当刷新的为头部列表控件,并且记录滚动的Y坐标为-头部控件视图,并且判断他是否滚动到顶部。该状态下的列表应当刷新的为尾部列表控件,并且记录滚动的Y坐标为底部控件的高度,并且判断他是否滚动到底部。然后,我们将该显示控件进行显示,该隐藏进行隐藏。判断此控件是否滚动到边缘,如果是将控件移动到相应的位置。相应的思维导图如下:

这里写图片描述

    最后是控件自定义属性的处理方法,代码如下:
      override fun handleStyledAttributes(a: TypedArray) {        super.handleStyledAttributes(a)        mListViewExtrasEnabled = a.getBoolean(R.styleable.PullToRefresh_ptrListViewExtrasEnabled, true)        if (mListViewExtrasEnabled) {            var lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,                    FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL)            // Create Loading Views ready for use later            var frame = FrameLayout(context)            mHeaderLoadingView = createLoadingLayout(context, PullToRefreshBase.Mode.PULL_FROM_START, a)            mHeaderLoadingView!!.visibility = View.GONE            frame.addView(mHeaderLoadingView, lp)            mRefreshableView.addHeaderView(frame, null, false)            mLvFooterLoadingFrame = FrameLayout(context)            mFooterLoadingView = createLoadingLayout(context, PullToRefreshBase.Mode.PULL_FROM_END, a)            mFooterLoadingView!!.visibility = View.GONE            mLvFooterLoadingFrame!!.addView(mFooterLoadingView, lp)            /**             * If the varue for Scrolling While Refreshing hasn't been             * explicitly set via XML, enable Scrolling While Refreshing.             */            if (!a.hasValue(R.styleable.PullToRefresh_ptrScrollingWhileRefreshingEnabled)) {                setScrollingWhileRefreshingEnabled(true)            }        }    }
    通过这个方法,我们看到为这个拖拽刷新列表控件自定义了控件是否启用下拉刷新的属性,然后如果该属性设置为真的话,就创建头部控件,尾部控件。代码很简单,不做过多赘述。    完成了基本代码编写之后,就做个demo给大家看:

这里写图片描述

后记,经过这个拖拽刷新控件的洗礼之后,①我们应当对OnTouchEvent事件处理有所理解②对组合控件头部控件尾部控件何时添加删除 何时显示隐藏有所理解③另外也对控件内边距进行重新计算,以后有这样需求相信大家也会迎刃而解。④其实这个控件只是一个抛砖引玉的作用,在这个基础上大家也可以实现拖拽刷新的recyleView与scrollview,万变不离其宗。代码地址 [GITHUB]

(https://github.com/fattyzeng/Kotlin/tree/Pulldown/pulldown)

[CSDN]

(http://download.csdn.net/detail/laozhumakelovemanuo/9873769)

阅读全文
1 0