下拉刷新上拉加載數據列表實現(Kotlin版)

     在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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章