SwipeRefreshLayout,用最少的程式碼定製最美的上下拉重新整理樣式

codeGoogle發表於2018-05-21

下拉重新整理框架其實有很多,而且質量都比較高。但是在日常開發中,每一款產品都會有一套自己獨特的一套重新整理樣式。相信有很多小夥伴在個性化定製中都或多或少的遇到過麻煩。今天我就給大家推薦一個在定製方面很出彩的一個重新整理框架SwipeToLoadLayout,該框架自身完成了下拉重新整理與上拉載入功能,同時將頂部檢視與底部檢視的UI定製功能通過介面很方便的提供給使用者自行定義。 相關程式碼已經上傳到github上,歡迎star、fork

基本流程

先簡單瞭解一下SwipeToLoadLayout的使用流程,以下拉重新整理為例:

  1. 完成Header部分,實現SwipeRefreshTrigger與SwipeRefreshTrigger介面
  2. 完成activity或fragment的佈局,在SwipeToLoadLayout節點下配置好Header與下拉目標元件(如RecyclerView等)

這裡還是要稍微說一下,因為這個佈局過程還是有一定的規則的 首先佈局的id是固定的,這個我們在ids.xml中就能看出。框架提供三個View:Header、Target、Footer,分別對應三個位置的View

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="swipe_target" type="id" />
    <item name="swipe_refresh_header" type="id" />
    <item name="swipe_load_more_footer" type="id" />
</resources>

複製程式碼

其次onFinishInflate()方法告訴我們,最多隻能同時存在這三個View,不能有更多的子View了

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        final int childNum = getChildCount();
        if (childNum == 0) {
            // no child return
            return;
        } else if (0 < childNum && childNum < 4) {
            mHeaderView = findViewById(R.id.swipe_refresh_header);
            mTargetView = findViewById(R.id.swipe_target);
            mFooterView = findViewById(R.id.swipe_load_more_footer);
        } else {
            // more than three children: unsupported!
            throw new IllegalStateException("Children num must equal or less than 3");
        }
        if (mTargetView == null) {
            return;
        }
        if (mHeaderView != null && mHeaderView instanceof SwipeTrigger) {
            mHeaderView.setVisibility(GONE);
        }
        if (mFooterView != null && mFooterView instanceof SwipeTrigger) {
            mFooterView.setVisibility(GONE);
        }
    }
複製程式碼

這樣你就能得出下一步該怎麼來實現了吧?沒錯肯定是這樣的

<?xml version="1.0" encoding="utf-8"?>
<com.aspsine.swipetoloadlayout.SwipeToLoadLayout >
    <View
        android:id="@id/swipe_refresh_header" />
    <android.support.v7.widget.RecyclerView
        android:id="@id/swipe_target" />
    <View
        android:id="@id/swipe_load_more_footer" />
</com.aspsine.swipetoloadlayout.SwipeToLoadLayout>

複製程式碼

Header的部分尤為重要。我們需在Header上實現SwipeTrigger與SwipeRefreshTrigger介面,介面中的方法分別對應滑動重新整理在各個狀態下的回撥。它們分別為 onPrepare:代表下拉重新整理開始的狀態 onMove:代表正在滑動過程中的狀態 onRelease:代表手指鬆開後,下拉重新整理進入鬆開重新整理的狀態 onComplete:代表下拉重新整理完成的狀態 onReset:代表下拉重新整理重置恢復的狀態 onRefresh:代表正在重新整理中的狀態 有了這幾個介面,我們就可以完成Header部分的任何動畫效果了。當然上拉載入更多的場景,只是把SwipeRefreshTrigger介面換成SwipeLoadMoreTrigger介面而已,其他跟下拉重新整理情況完全相同

  1. 在activity或fragment中配置下拉監聽事件,並在資料獲取完成後主動觸發重新整理swipeToLoadLayout.setRefreshing(false);完成功能

更深入的部分我們放到原始碼分析裡面再說

看起來好像很簡單,那麼我們就通過幾個小Demo瞭解一下如何使用吧

仿新浪微博

之所以第一個範例選擇新浪微博,是因為它是最傳統重新整理風格:根據箭頭和文字的不同來表明當前不同的狀態

如果你在早期研究過PullToRefresh,那麼很容易在這個框架基礎上實現相應的檢視更新功能

先完成頭部的定義。WeiboRefreshHeaderView作為頭,其實際為一個LinearLayout

class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
複製程式碼

頭部佈局很簡單

<?xml version="1.0" encoding="utf-8"?>
<com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="60dip"
    android:gravity="center"
    android:orientation="horizontal">
    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <ProgressBar
            android:id="@+id/pb_weibo"
            style="?android:attr/progressBarStyleSmallInverse"
            android:layout_centerInParent="true"
            android:visibility="gone"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <ImageView
            android:id="@+id/iv_weibo"
            android:src="@mipmap/tableview_pull_refresh_arrow_down"
            android:layout_centerInParent="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </RelativeLayout>
    <TextView
        android:id="@+id/tv_weibo"
        android:layout_marginStart="10dip"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="下拉重新整理"/>
</com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView>
複製程式碼

activity的佈局也很簡單,把頭跟身子一起加在SwipeToLoadLayout裡

<?xml version="1.0" encoding="utf-8"?>
<com.aspsine.swipetoloadlayout.SwipeToLoadLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/swipe_weibo">

    <include
        layout="@layout/header_weibo"
        android:id="@id/swipe_refresh_header" />
    <TextView
        android:id="@id/swipe_target"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="下拉重新整理"/>
</com.aspsine.swipetoloadlayout.SwipeToLoadLayout>
複製程式碼

下面就是完成頭部動畫效果了。新浪微博的這個效果就是檢視被下拉到頭部高度之後,將箭頭位置旋轉一下同時更換文字,重新整理時展現progressbar即可

class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {

    var pb_weibo: ProgressBar? = null
    var iv_weibo: ImageView? = null
    var tv_weibo: TextView? = null

    // 是否發生旋轉
    var rotated = false

    private val rotate_up: Animation by lazy {
        AnimationUtils.loadAnimation(context, R.anim.rotate_up)
    }

    private val rotate_down: Animation by lazy {
        AnimationUtils.loadAnimation(context, R.anim.rotate_down)
    }

    constructor(context: Context) : super(context)
    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)

    override fun onFinishInflate() {
        super.onFinishInflate()

        pb_weibo = findViewById(R.id.pb_weibo)
        iv_weibo = findViewById(R.id.iv_weibo)
        tv_weibo = findViewById(R.id.tv_weibo)
    }

    override fun onReset() {
        pb_weibo?.visibility = View.GONE
        iv_weibo?.visibility = View.VISIBLE
        tv_weibo?.text = "下拉重新整理"
    }

    override fun onComplete() {
        tv_weibo?.text = "重新整理完成"
        pb_weibo?.visibility = View.GONE
    }

    override fun onRelease() {

    }

    override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {
        if (p0 > SizeUtils.dp2px(60f)) {
            if (!rotated) {
                rotated = true
                tv_weibo?.text = "釋放更新"
                iv_weibo?.clearAnimation()
                iv_weibo?.startAnimation(rotate_up)
            }
        }
        else {
            if (rotated) {
                rotated = false
                tv_weibo?.text = "下拉重新整理"
                iv_weibo?.clearAnimation()
                iv_weibo?.startAnimation(rotate_down)
            }
        }
    }

    override fun onPrepare() {

    }

    override fun onRefresh() {
        tv_weibo?.text = "載入中"
        iv_weibo?.clearAnimation()
        iv_weibo?.visibility = View.GONE
        pb_weibo?.visibility = View.VISIBLE
    }
}
複製程式碼

對照一下上文的重新整理週期,應該很好理解

美團外賣

美團外賣是利用ImageView直接播放一段animation直到重新整理完成停止。在下拉過程中,該ImageView隨著位移的距離變化而發生相應的大小變化

美團外賣動畫效果是由一系列的圖片組成的,所以與新浪微博效果相比更為簡單一些

一樣要完成頭部檢視的定義

class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
複製程式碼
<?xml version="1.0" encoding="utf-8"?>
<com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="wrap_content"
    android:gravity="center"
    android:padding="10dip">
    <ImageView
        android:id="@+id/iv_mt"
        android:layout_width="112dp"
        android:layout_height="44dp"
        android:background="@drawable/animation_list_refresh_mt"
        android:transformPivotX="56dp"
        android:transformPivotY="22dp"
        android:scaleY="0.3"
        android:scaleX="0.3"/>
</com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView>
複製程式碼

剩下就是完成動畫的播放與縮放的處理了

class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {

    var iv_mt: ImageView? = null

    val animationDrawable: AnimationDrawable by lazy {
        iv_mt?.background as AnimationDrawable
    }

    constructor(context: Context) : super(context)
    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)

    override fun onFinishInflate() {
        super.onFinishInflate()

        iv_mt = findViewById(R.id.iv_mt)
    }

    override fun onReset() {

    }

    override fun onComplete() {
        animationDrawable.stop()
    }

    override fun onRelease() {

    }

    override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {
        val percent = if (p0 * 1.0f / SizeUtils.dp2px(44f) > 1) 1f else p0 * 1.0f / SizeUtils.dp2px(44f)

        iv_mt?.scaleY = (0.3f + 0.7 * percent).toFloat()
        iv_mt?.scaleX = (0.3f + 0.7 * percent).toFloat()
    }

    override fun onPrepare() {
        if (!animationDrawable.isRunning) {
            animationDrawable.start()
        }

        iv_mt?.scaleY = 0.3f
        iv_mt?.scaleX = 0.3f
    }

    override fun onRefresh() {
        if (!animationDrawable.isRunning) {
            animationDrawable.start()
        }

        iv_mt?.scaleY = 1f
        iv_mt?.scaleX = 1f
    }
}
複製程式碼

程式碼都很簡單,很容易理解

餓了麼

餓了麼的效果是通過SVG來實現的

餓了麼app對資源進行了混淆,所以我拿不到圖片,只能隨便從其他地方找一個了

一樣是Header的編寫,這裡面有一點不同,我用android-pathview這個開源框架實現SVG播放進度控制功能

我需要將這個動畫效果在下拉重新整理的過程中實現

image

class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
複製程式碼
<?xml version="1.0" encoding="utf-8"?>
<com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center">
    <com.eftimoff.androipathview.PathView
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/pathView_ele"
        android:layout_width="58dp"
        android:layout_height="58dp"
        app:pathColor="@android:color/black"
        app:svg="@raw/issues"
        app:pathWidth="2dp"/>
</com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView>
複製程式碼

下面就是根據滑動偏移量來處理SVG播放的進度

class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {

    var pathView_ele: PathView? = null

    constructor(context: Context) : super(context)
    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)

    override fun onFinishInflate() {
        super.onFinishInflate()

        pathView_ele = findViewById(R.id.pathView_ele)
    }

    override fun onReset() {

    }

    override fun onComplete() {
        pathView_ele?.setPercentage(1f)
    }

    override fun onRelease() {
    }

    override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {
        val percent = 1 - (SizeUtils.dp2px(58f) - p0) * 1.0f / SizeUtils.dp2px(58f)
        val value = if (percent >= 1) 1f else percent
        pathView_ele?.setPercentage(value)
    }

    override fun onPrepare() {
        pathView_ele?.setPercentage(0f)
    }

    override fun onRefresh() {
        pathView_ele?.setPercentage(1f)
    }
}
複製程式碼

這裡你會發出一個疑問,怎麼效果與餓了麼有的差距?餓了麼是滑動到Header完成展開之後就不再繼續下滑了,那我們們這個怎麼實現呢?那我只能說不好意思,在現有條件下我們們實現不了,只能通過改原始碼完成

那我們就順帶來閱讀原始碼,看看這個地方怎麼改進吧?

原始碼分析

之前的onFinishInflate我們們就不說了,那個就是告訴我們只能有三個View,分別是Header、Target、Footer

然後是測量階段,在測量階段可以得到兩個重要的變數mHeaderHeight與mFooterHeight,他們分別代表Header與Footer的高度。同時如果定義的mRefreshTriggerOffset(鬆開重新整理的高度)比Header或Footer的高度小,則修正這個重新整理位置

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // header
        if (mHeaderView != null) {
            final View headerView = mHeaderView;
            measureChildWithMargins(headerView, widthMeasureSpec, 0, heightMeasureSpec, 0);
            MarginLayoutParams lp = ((MarginLayoutParams) headerView.getLayoutParams());
            mHeaderHeight = headerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (mRefreshTriggerOffset < mHeaderHeight) {
                mRefreshTriggerOffset = mHeaderHeight;
            }
        }
        // target
        if (mTargetView != null) {
            final View targetView = mTargetView;
            measureChildWithMargins(targetView, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
        // footer
        if (mFooterView != null) {
            final View footerView = mFooterView;
            measureChildWithMargins(footerView, widthMeasureSpec, 0, heightMeasureSpec, 0);
            MarginLayoutParams lp = ((MarginLayoutParams) footerView.getLayoutParams());
            mFooterHeight = footerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (mLoadMoreTriggerOffset < mFooterHeight) {
                mLoadMoreTriggerOffset = mFooterHeight;
            }
        }
    }
複製程式碼

在onLayout中對三個檢視進行佈局

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        layoutChildren();

        mHasHeaderView = (mHeaderView != null);
        mHasFooterView = (mFooterView != null);
    }
複製程式碼

這裡有一個重要的方法layoutChildren,這個方法就是改變三個檢視的位置的。當然這個位置要根據不同的型別來處理,預設情況下我們都是STYLE.CLASSIC型別。

private void layoutChildren() {
        final int width = getMeasuredWidth();
        final int height = getMeasuredHeight();

        final int paddingLeft = getPaddingLeft();
        final int paddingTop = getPaddingTop();
        final int paddingRight = getPaddingRight();
        final int paddingBottom = getPaddingBottom();

        if (mTargetView == null) {
            return;
        }

        // layout header
        if (mHeaderView != null) {
            final View headerView = mHeaderView;
            MarginLayoutParams lp = (MarginLayoutParams) headerView.getLayoutParams();
            final int headerLeft = paddingLeft + lp.leftMargin;
            final int headerTop;
            switch (mStyle) {
                case STYLE.CLASSIC:
                    // classic
                    headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
                    break;
                case STYLE.ABOVE:
                    // classic
                    headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
                    break;
                case STYLE.BLEW:
                    // blew
                    headerTop = paddingTop + lp.topMargin;
                    break;
                case STYLE.SCALE:
                    // scale
                    headerTop = paddingTop + lp.topMargin - mHeaderHeight / 2 + mHeaderOffset / 2;
                    break;
                case STYLE.BLEW2CLASSIC:
                    // blew2classic
                    if (mHeaderOffset > mHeaderHeight) {
                        headerTop = paddingTop + lp.topMargin;
                    }
                    else {
                        headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
                    }
                    break;
                default:
                    // classic
                    headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
                    break;
            }
            final int headerRight = headerLeft + headerView.getMeasuredWidth();
            final int headerBottom = headerTop + headerView.getMeasuredHeight();
            headerView.layout(headerLeft, headerTop, headerRight, headerBottom);
        }

        // layout target
        if (mTargetView != null) {
            final View targetView = mTargetView;
            MarginLayoutParams lp = (MarginLayoutParams) targetView.getLayoutParams();
            final int targetLeft = paddingLeft + lp.leftMargin;
            final int targetTop;

            switch (mStyle) {
                case STYLE.CLASSIC:
                    // classic
                    targetTop = paddingTop + lp.topMargin + mTargetOffset;
                    break;
                case STYLE.ABOVE:
                    // above
                    targetTop = paddingTop + lp.topMargin;
                    break;
                case STYLE.BLEW:
                    // classic
                    targetTop = paddingTop + lp.topMargin + mTargetOffset;
                    break;
                case STYLE.SCALE:
                    // classic
                    targetTop = paddingTop + lp.topMargin + mTargetOffset;
                    break;
                case STYLE.BLEW2CLASSIC:
                    // classic
                    targetTop = paddingTop + lp.topMargin + mTargetOffset;
                    break;
                default:
                    // classic
                    targetTop = paddingTop + lp.topMargin + mTargetOffset;
                    break;
            }
            final int targetRight = targetLeft + targetView.getMeasuredWidth();
            final int targetBottom = targetTop + targetView.getMeasuredHeight();
            targetView.layout(targetLeft, targetTop, targetRight, targetBottom);
        }

        // layout footer
        if (mFooterView != null) {
            final View footerView = mFooterView;
            MarginLayoutParams lp = (MarginLayoutParams) footerView.getLayoutParams();
            final int footerLeft = paddingLeft + lp.leftMargin;
            final int footerBottom;
            switch (mStyle) {
                case STYLE.CLASSIC:
                    // classic
                    footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
                    break;
                case STYLE.ABOVE:
                    // classic
                    footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
                    break;
                case STYLE.BLEW:
                    // blew
                    footerBottom = height - paddingBottom - lp.bottomMargin;
                    break;
                case STYLE.SCALE:
                    // scale
                    footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight / 2 + mFooterOffset / 2;
                    break;
                case STYLE.BLEW2CLASSIC:
                    // blew2classic
                    if (mFooterOffset > mFooterHeight) {
                        footerBottom = height - paddingBottom - lp.bottomMargin;
                    }
                    else {
                        footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
                    }
                    break;
                default:
                    // classic
                    footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
                    break;
            }
            final int footerTop = footerBottom - footerView.getMeasuredHeight();
            final int footerRight = footerLeft + footerView.getMeasuredWidth();

            footerView.layout(footerLeft, footerTop, footerRight, footerBottom);
        }

        if (mStyle == STYLE.CLASSIC
                || mStyle == STYLE.ABOVE) {
            if (mHeaderView != null) {
                mHeaderView.bringToFront();
            }
            if (mFooterView != null) {
                mFooterView.bringToFront();
            }
        } else if (mStyle == STYLE.BLEW || mStyle == STYLE.SCALE || mStyle == STYLE.BLEW2CLASSIC) {
            if (mTargetView != null) {
                mTargetView.bringToFront();
            }
        }
    }
複製程式碼

以下拉重新整理為例,看這行程式碼。 paddingTop與lp.topMargin都是0,mHeaderHeight是Header的高度,mHeaderOffset就是手指滑動的距離(這個稍後會有說明)。在下拉過程中,mHeaderOffset的值會越來越大,所以headerTop的值是從-mHeaderHeight開始逐漸增大的,所以headerView會向下逐步移動

headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset
複製程式碼

而Target更為簡單,你手指滑動多少它就跟著滑動多少

targetTop = paddingTop + lp.topMargin + mTargetOffset;
複製程式碼

這樣能夠想象出餓了麼滑動到mHeaderHeight高度之後如何處理的吧,請參考我自己定義的style--BLEW2CLASSIC

if (mHeaderOffset > mHeaderHeight) {
    headerTop = paddingTop + lp.topMargin;
}
else {
    headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
}
複製程式碼

SwipeRefreshLayout,用最少的程式碼定製最美的上下拉重新整理樣式

繼續往下來到事件分發部分了

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        switch (action) {
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // swipeToRefresh -> finger up -> finger down if the status is still swipeToRefresh
                // in onInterceptTouchEvent ACTION_DOWN event will stop the scroller
                // if the event pass to the child view while ACTION_MOVE(condition is false)
                // in onInterceptTouchEvent ACTION_MOVE the ACTION_UP or ACTION_CANCEL will not be
                // passed to onInterceptTouchEvent and onTouchEvent. Instead It will be passed to
                // child view's onTouchEvent. So we must deal this situation in dispatchTouchEvent
                onActivePointerUp();
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
複製程式碼

獲取事件之後,在手指釋放的時候執行onActivePointerUp(),我們們來看看。分別判斷了當前是處在下拉以重新整理、上拉以載入更多、鬆開以重新整理、鬆開以載入更多,然後滾動到響應的位置上去。注意在鬆開狀態時,執行了onRelease()回撥

    private void onActivePointerUp() {
        if (STATUS.isSwipingToRefresh(mStatus)) {
            // simply return
            scrollSwipingToRefreshToDefault();

        } else if (STATUS.isSwipingToLoadMore(mStatus)) {
            // simply return
            scrollSwipingToLoadMoreToDefault();

        } else if (STATUS.isReleaseToRefresh(mStatus)) {
            // return to header height and perform refresh
            mRefreshCallback.onRelease();
            scrollReleaseToRefreshToRefreshing();

        } else if (STATUS.isReleaseToLoadMore(mStatus)) {
            // return to footer height and perform loadMore
            mLoadMoreCallback.onRelease();
            scrollReleaseToLoadMoreToLoadingMore();

        }
    }
複製程式碼

隨後就是事件攔截的判斷。只要你向下滑動時Target確實不能再向下移動了或者向上滑動時Target確實不能再向上移動了,那麼SwipeRefreshLayout就把事件攔截,執行onTouchEvent裡面的位移操作了

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        final int action = MotionEventCompat.getActionMasked(event);
        switch (action) {
            case MotionEvent.ACTION_DOWN:

                mActivePointerId = MotionEventCompat.getPointerId(event, 0);
                mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
                mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);

                // if it isn't an ing status or default status
                if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isSwipingToLoadMore(mStatus) ||
                        STATUS.isReleaseToRefresh(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {
                    // abort autoScrolling, not trigger the method #autoScrollFinished()
                    mAutoScroller.abortIfRunning();
                    if (mDebug) {
                        Log.i(TAG, "Another finger down, abort auto scrolling, let the new finger handle");
                    }
                }

                if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus)
                        || STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {
                    return true;
                }

                // let children view handle the ACTION_DOWN;

                // 1\. children consumed:
                // if at least one of children onTouchEvent() ACTION_DOWN return true.
                // ACTION_DOWN event will not return to SwipeToLoadLayout#onTouchEvent().
                // but the others action can be handled by SwipeToLoadLayout#onInterceptTouchEvent()

                // 2\. children not consumed:
                // if children onTouchEvent() ACTION_DOWN return false.
                // ACTION_DOWN event will return to SwipeToLoadLayout's onTouchEvent().
                // SwipeToLoadLayout#onTouchEvent() ACTION_DOWN return true to consume the ACTION_DOWN event.

                // anyway: handle action down in onInterceptTouchEvent() to init is an good option
                break;
            case MotionEvent.ACTION_MOVE:
                if (mActivePointerId == INVALID_POINTER) {
                    return false;
                }
                float y = getMotionEventY(event, mActivePointerId);
                float x = getMotionEventX(event, mActivePointerId);
                final float yInitDiff = y - mInitDownY;
                final float xInitDiff = x - mInitDownX;
                mLastY = y;
                mLastX = x;
                boolean moved = Math.abs(yInitDiff) > Math.abs(xInitDiff)
                        && Math.abs(yInitDiff) > mTouchSlop;
                boolean triggerCondition =
                        // refresh trigger condition
                        (yInitDiff > 0 && moved && onCheckCanRefresh()) ||
                                //load more trigger condition
                                (yInitDiff < 0 && moved && onCheckCanLoadMore());
                if (triggerCondition) {
                    // if the refresh's or load more's trigger condition  is true,
                    // intercept the move action event and pass it to SwipeToLoadLayout#onTouchEvent()
                    return true;
                }
                break;
            case MotionEvent.ACTION_POINTER_UP: {
                onSecondaryPointerUp(event);
                mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
                mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mActivePointerId = INVALID_POINTER;
                break;
        }
        return super.onInterceptTouchEvent(event);
    }
複製程式碼

下面就是位移過程。 如果當期處於初始STATUS_DEFAULT狀態,則進入STATUS_SWIPING_TO_REFRESH,同時回撥onPrepare()方法 如果在下拉重新整理流程中向上滑動並且滑動偏移量小於0,為了不讓Target部分移動到螢幕之外,則將體系流程恢復到初始STATUS_DEFAULT狀態,同時使用fixCurrentStatusLayout()方法調整三個View的位置。上拉載入更多流程同理 在正常下拉重新整理流程中,如果當期狀態是STATUS_SWIPING_TO_REFRESH或者是STATUS_RELEASE_TO_REFRESH,即處於下拉以重新整理、鬆開以重新整理狀態,如果下拉的距離超過mRefreshTriggerOffset,則進入鬆開以重新整理狀態,反之則進入下拉以重新整理狀態。上拉載入更多流程同理 這時候會觸發位移發生fingerScroll()

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = MotionEventCompat.getActionMasked(event);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mActivePointerId = MotionEventCompat.getPointerId(event, 0);
                return true;

            case MotionEvent.ACTION_MOVE:
                // take over the ACTION_MOVE event from SwipeToLoadLayout#onInterceptTouchEvent()
                // if condition is true
                final float y = getMotionEventY(event, mActivePointerId);
                final float x = getMotionEventX(event, mActivePointerId);

                final float yDiff = y - mLastY;
                final float xDiff = x - mLastX;
                mLastY = y;
                mLastX = x;

                if (Math.abs(xDiff) > Math.abs(yDiff) && Math.abs(xDiff) > mTouchSlop) {
                    return true;
                }

                if (STATUS.isStatusDefault(mStatus)) {
                    if (yDiff > 0 && onCheckCanRefresh()) {
                        mRefreshCallback.onPrepare();
                        setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);
                    } else if (yDiff < 0 && onCheckCanLoadMore()) {
                        mLoadMoreCallback.onPrepare();
                        setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE);
                    }
                } else if (STATUS.isRefreshStatus(mStatus)) {
                    if (mTargetOffset <= 0) {
                        setStatus(STATUS.STATUS_DEFAULT);
                        fixCurrentStatusLayout();
                        return true;
                    }
                } else if (STATUS.isLoadMoreStatus(mStatus)) {
                    if (mTargetOffset >= 0) {
                        setStatus(STATUS.STATUS_DEFAULT);
                        fixCurrentStatusLayout();
                        return true;
                    }
                }

                if (STATUS.isRefreshStatus(mStatus)) {
                    if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus)) {
                        if (mTargetOffset >= mRefreshTriggerOffset) {
                            setStatus(STATUS.STATUS_RELEASE_TO_REFRESH);
                        } else {
                            setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);
                        }
                        fingerScroll(yDiff);
                    }
                } else if (STATUS.isLoadMoreStatus(mStatus)) {
                    if (STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {
                        if (-mTargetOffset >= mLoadMoreTriggerOffset) {
                            setStatus(STATUS.STATUS_RELEASE_TO_LOAD_MORE);
                        } else {
                            setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE);
                        }
                        fingerScroll(yDiff);
                    }
                }
                return true;

            case MotionEvent.ACTION_POINTER_DOWN: {
                final int pointerIndex = MotionEventCompat.getActionIndex(event);
                final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex);
                if (pointerId != INVALID_POINTER) {
                    mActivePointerId = pointerId;
                }
                mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
                mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
                break;
            }
            case MotionEvent.ACTION_POINTER_UP: {
                onSecondaryPointerUp(event);
                mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
                mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mActivePointerId == INVALID_POINTER) {
                    return false;
                }
                mActivePointerId = INVALID_POINTER;
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }
複製程式碼

位移無非就是對mTargetOffset進行賦值,同時調整三個View的位置。注意這裡呼叫了onMove()回撥

    private void fingerScroll(final float yDiff) {
        float ratio = mDragRatio;
        float yScrolled = yDiff * ratio;

        // make sure (targetOffset>0 -> targetOffset=0 -> default status)
        // or (targetOffset<0 -> targetOffset=0 -> default status)
        // forbidden fling (targetOffset>0 -> targetOffset=0 ->targetOffset<0 -> default status)
        // or (targetOffset<0 -> targetOffset=0 ->targetOffset>0 -> default status)
        // I am so smart :)

        float tmpTargetOffset = yScrolled + mTargetOffset;
        if ((tmpTargetOffset > 0 && mTargetOffset < 0)
                || (tmpTargetOffset < 0 && mTargetOffset > 0)) {
            yScrolled = -mTargetOffset;
        }

        if (mRefreshFinalDragOffset >= mRefreshTriggerOffset && tmpTargetOffset > mRefreshFinalDragOffset) {
            yScrolled = mRefreshFinalDragOffset - mTargetOffset;
        } else if (mLoadMoreFinalDragOffset >= mLoadMoreTriggerOffset && -tmpTargetOffset > mLoadMoreFinalDragOffset) {
            yScrolled = -mLoadMoreFinalDragOffset - mTargetOffset;
        }

        if (STATUS.isRefreshStatus(mStatus)) {
            mRefreshCallback.onMove(mTargetOffset, false, false);
        } else if (STATUS.isLoadMoreStatus(mStatus)) {
            mLoadMoreCallback.onMove(mTargetOffset, false, false);
        }
        updateScroll(yScrolled);
    }

    private void updateScroll(final float yScrolled) {
        if (yScrolled == 0) {
            return;
        }
        mTargetOffset += yScrolled;

        if (STATUS.isRefreshStatus(mStatus)) {
            mHeaderOffset = mTargetOffset;
            mFooterOffset = 0;
        } else if (STATUS.isLoadMoreStatus(mStatus)) {
            mFooterOffset = mTargetOffset;
            mHeaderOffset = 0;
        }

        if (mDebug) {
            Log.i(TAG, "mTargetOffset = " + mTargetOffset);
        }
        layoutChildren();
        invalidate();
    }
複製程式碼

最後就是執行結束重新整理操作,完成閉環。結束的時候,refreshing值為false,執行onComplete()回撥,同時回滾到初始位置

    public void setRefreshing(boolean refreshing) {
        if (!isRefreshEnabled() || mHeaderView == null) {
            return;
        }
        this.mAutoLoading = refreshing;
        if (refreshing) {
            if (STATUS.isStatusDefault(mStatus)) {
                setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);
                scrollDefaultToRefreshing();
            }
        } else {
            if (STATUS.isRefreshing(mStatus)) {
                mRefreshCallback.onComplete();
                postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        scrollRefreshingToDefault();
                    }
                }, mRefreshCompleteDelayDuration);
            }
        }
    }
複製程式碼

這裡還有一個補充,關於自動滑動方面。自動滾動一般都是通過AutoScroller類,呼叫其autoScroll()方法來完成,而實際上也是呼叫Scroller.startScroll()。但是不知道你有沒有注意到post(this),它在反覆呼叫這個Runnable的run()來判斷滑動是否已經結束。如果沒有結束,則通過autoScroll()方法來呼叫move()回撥;如果已經結束,則通過autoScrollFinished()方法來判斷下一步應該到達何種狀態

private class AutoScroller implements Runnable {

        private Scroller mScroller;

        private int mmLastY;

        private boolean mRunning = false;

        private boolean mAbort = false;

        public AutoScroller() {
            mScroller = new Scroller(getContext());
        }

        @Override
        public void run() {
            boolean finish = !mScroller.computeScrollOffset() || mScroller.isFinished();
            int currY = mScroller.getCurrY();
            int yDiff = currY - mmLastY;
            if (finish) {
                finish();
            } else {
                mmLastY = currY;
                SwipeToLoadLayout.this.autoScroll(yDiff);
                post(this);
            }
        }

        /**
         * remove the post callbacks and reset default values
         */
        private void finish() {
            mmLastY = 0;
            mRunning = false;
            removeCallbacks(this);
            // if abort by user, don't call
            if (!mAbort) {
                autoScrollFinished();
            }
        }

        /**
         * abort scroll if it is scrolling
         */
        public void abortIfRunning() {
            if (mRunning) {
                if (!mScroller.isFinished()) {
                    mAbort = true;
                    mScroller.forceFinished(true);
                }
                finish();
                mAbort = false;
            }
        }

        /**
         * The param yScrolled here isn't final pos of y.
         * It's just like the yScrolled param in the
         * {@link #updateScroll(float yScrolled)}
         *
         * @param yScrolled
         * @param duration
         */
        private void autoScroll(int yScrolled, int duration) {
            removeCallbacks(this);
            mmLastY = 0;
            if (!mScroller.isFinished()) {
                mScroller.forceFinished(true);
            }
            mScroller.startScroll(0, 0, 0, yScrolled, duration);
            post(this);
            mRunning = true;
        }
    }
複製程式碼

如果是鬆開以重新整理,則進入重新整理狀態,同時回撥onRefresh()方法 如果是正在重新整理狀態,則復原,執行onReset()方法 如果是鬆開以重新整理並且通過setRefresh(true)方法進來的,則進入正在重新整理狀態,執行onRefresh()方法;反之則執行復原操作,執行onReset()方法。 上拉載入更多流程同理

private void autoScrollFinished() {
        int mLastStatus = mStatus;

        if (STATUS.isReleaseToRefresh(mStatus)) {
            setStatus(STATUS.STATUS_REFRESHING);
            fixCurrentStatusLayout();
            mRefreshCallback.onRefresh();

        } else if (STATUS.isRefreshing(mStatus)) {
            setStatus(STATUS.STATUS_DEFAULT);
            fixCurrentStatusLayout();
            mRefreshCallback.onReset();

        } else if (STATUS.isSwipingToRefresh(mStatus)) {
            if (mAutoLoading) {
                mAutoLoading = false;
                setStatus(STATUS.STATUS_REFRESHING);
                fixCurrentStatusLayout();
                mRefreshCallback.onRefresh();
            } else {
                setStatus(STATUS.STATUS_DEFAULT);
                fixCurrentStatusLayout();
                mRefreshCallback.onReset();
            }
        } else if (STATUS.isStatusDefault(mStatus)) {

        } else if (STATUS.isSwipingToLoadMore(mStatus)) {
            if (mAutoLoading) {
                mAutoLoading = false;
                setStatus(STATUS.STATUS_LOADING_MORE);
                fixCurrentStatusLayout();
                mLoadMoreCallback.onLoadMore();
            } else {
                setStatus(STATUS.STATUS_DEFAULT);
                fixCurrentStatusLayout();
                mLoadMoreCallback.onReset();
            }
        } else if (STATUS.isLoadingMore(mStatus)) {
            setStatus(STATUS.STATUS_DEFAULT);
            fixCurrentStatusLayout();
            mLoadMoreCallback.onReset();
        } else if (STATUS.isReleaseToLoadMore(mStatus)) {
            setStatus(STATUS.STATUS_LOADING_MORE);
            fixCurrentStatusLayout();
            mLoadMoreCallback.onLoadMore();
        } else {
            throw new IllegalStateException("illegal state: " + STATUS.getStatus(mStatus));
        }

        if (mDebug) {
            Log.i(TAG, STATUS.getStatus(mLastStatus) + " -> " + STATUS.getStatus(mStatus));
        }
    }
複製程式碼

原始碼分析到此結束。怎麼樣,是不是很簡單

參考文章 MNSwipeToLoadDemo

連結:https://www.jianshu.com/p/fc8c73db72b3

更多文章

上半年技術文章集合—184篇文章分類彙總

NDK專案實戰—高仿360手機助手之解除安裝監聽

破解Android版微信跳一跳,一招教你挑戰高分

高階UI特效仿直播點贊效果—一個優美炫酷的點贊動畫

一個實現錄音和播放的小案例

相信自己,沒有做不到的,只有想不到的

如果你覺得此文對您有所幫助,歡迎入群 QQ交流群 :644196190 微信公眾號:終端研發部

技術+職場

相關文章