Android 打造專(zhuān)屬的下拉刷新 加載更多

最近忙成狗...

前面都有文章寫(xiě)怎么去實(shí)現(xiàn)加載下拉刷新和加載更多,這篇文章會(huì)接著講,所以沒(méi)有看的,可以先看看喲。

Android 自定義View UC下拉刷新效果(一)
Android 自定義View UC下拉刷新效果(二)
Android 自定義View UC下拉刷新效果(三)


現(xiàn)在MD的設(shè)計(jì)風(fēng)格逐漸在被接受,Android 對(duì)應(yīng)的嵌套滑動(dòng)機(jī)制下的
CoordinatorLayout AppBarLayout CollapsingToolbarLayout 等都在被大量使用了。

其實(shí),我覺(jué)得 SwipeRefreshLayout 這種刷新效果就很好了,簡(jiǎn)約又不簡(jiǎn)單。一看就知道是 Android 的。但是,總會(huì)有老板或者產(chǎn)品要么是根本就不玩 Android 的,要么就是要標(biāo)新立異的,一定要弄一個(gè)怎樣怎樣炫酷的下拉刷新效果。那你怎么辦,吐完槽還是要擼代碼咯?,F(xiàn)在就業(yè)形勢(shì)這么嚴(yán)峻,你還敢不老實(shí)??哈哈,開(kāi)個(gè)玩笑。

所以說(shuō),支持嵌套滑動(dòng)的下拉刷新加載更多是迫在眉睫啊。

對(duì)比 iOS, Android 不是天然支持下拉刷新這種東西的,之前的文章已經(jīng)討論過(guò),現(xiàn)在的下拉刷新效果其實(shí)都是先把 Header 隱藏起來(lái),根據(jù)手勢(shì),將它有展示出來(lái)。前面說(shuō)過(guò)了通過(guò) margin,或者 translationY 的方式。今天講講另外一種方式,就是在 layout() 的時(shí)候控制它的擺放位置,然后通過(guò) scroll() 的方式控制其展示。

先看 onLayout() 的方法:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int contentHeight = 0;
    int width = getMeasuredWidth();
    int height = getMeasuredHeight();
    int childLeft = getPaddingLeft();
    int childRight = getPaddingLeft();
    int childTop = getPaddingTop();
    int childWidth = width - childLeft - childRight;
    int childHeight;
    View child;
    if (header != null) {
        child = header;
        headerHeight = child.getMeasuredHeight();
        //藏起header
        child.layout(childLeft, childTop - headerHeight, childLeft + childWidth, childTop);
    }
    //make sure there is the target!
    if (mTarget == null) {
        ensureTarget();
    }
    if (mTarget == null) {
        return;
    }
    child = mTarget;
    childLeft = getPaddingLeft();
    childTop = getPaddingTop();
    childWidth = width - getPaddingLeft() - getPaddingRight();
    childHeight = height - getPaddingTop() - getPaddingBottom();
    child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
    contentHeight += child.getMeasuredHeight();

    if (footer != null) {
        child = footer;
        footHeight = child.getMeasuredHeight();
        child.layout(0, contentHeight, child.getMeasuredWidth(), contentHeight + footHeight);
    }
    // 滾動(dòng)到bottom
    bottomScroll = contentHeight - getMeasuredHeight();
}

這樣之后,下拉刷新布局和加載更多的布局都隱藏起來(lái)了。因?yàn)槭且蛟鞂?zhuān)屬的下來(lái)刷新和加載更多嘛,所以,這里的 HeaderFooter 都是咱們自己定義然后傳進(jìn)去的。 mTarget 顧名思義就是需要滾動(dòng)的的控件了,比如說(shuō)是 RecyclerView 或者巴拉巴拉了。

嵌套滑動(dòng)機(jī)制

之前都聊過(guò)這個(gè)了,大概意思就是,在這個(gè)之前, Android 的事件分發(fā)j簡(jiǎn)單來(lái)說(shuō)就是從 ViewGroupView,如果 View 不消費(fèi),那么又返回給 ViewGroup 讓它消費(fèi)。這里就存在一個(gè)問(wèn)題:「餓死了爸爸,撐死了孩紙」。如果這個(gè)事件我們可以讓 View 它爸消費(fèi)一點(diǎn),剩下的很多都自己消費(fèi),這樣是不是最好了?所以,嵌套滑動(dòng)的機(jī)制就出現(xiàn)了。

這樣,在事件傳遞到 View 后,在 View 消費(fèi)事件之前,總會(huì)關(guān)心的問(wèn)問(wèn)它爸爸,你到底要來(lái)一點(diǎn)兒不?都是一家人,千萬(wàn)別客氣。然后父子倆就其樂(lè)融融,誰(shuí)都餓不死了。

這里就有了 NestedScrollingParent , NestedScrollingChild 這兩個(gè)接口??催@個(gè)名字都知道,這個(gè)就是爸爸和孩紙的關(guān)系嘛。

這里再來(lái)看我們要實(shí)現(xiàn)的刷新,它是 RecyclerView NestedScrollView
的爸爸,但是有時(shí) CoordinatorLayout 等的孩紙,所以,它就是又當(dāng)?shù)质呛⒓?,它孩紙的事件在?wèn)它需要先消費(fèi)點(diǎn)兒不的時(shí)候它作為孩紙也要按照慣例去問(wèn)問(wèn) CoordinatorLayout 它爸爸是不是要來(lái)一點(diǎn)兒,嗯,滿(mǎn)滿(mǎn)的和諧社會(huì)啊。好了,說(shuō)的這么形象了,應(yīng)該沒(méi)有誰(shuí)不懂吧。

還是先來(lái)看看 RecyclerView 的代碼吧。

        case MotionEvent.ACTION_MOVE: {
            final int index = e.findPointerIndex(mScrollPointerId);
            if (index < 0) {
                Log.e(TAG, "Error processing scroll; pointer index for id " +
                        mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                return false;
            }

            final int x = (int) (e.getX(index) + 0.5f);
            final int y = (int) (e.getY(index) + 0.5f);
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;
            //劃重點(diǎn),先問(wèn)問(wèn)爸爸要不要消費(fèi)
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // Updated the nested offsets
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
            }

在自己消費(fèi)之前,先問(wèn)爸爸要不要消費(fèi)。如果消費(fèi)了,這個(gè)dispatchNestedPreScroll() 將會(huì)返回 true , 然后就將爸爸消費(fèi)了的值減出去,剩下就是自己可以使用的了。到這里,事件是到 RecyclerView 了 ,但是它壓根還沒(méi)有消費(fèi)任何事件。

            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];

                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                if (mGapWorker != null && (dx != 0 || dy != 0)) {
                    mGapWorker.postFromTraversal(this, dx, dy);
                }
            }

到這里,RecyclerView 終于調(diào)用了 scrollByInternal() 開(kāi)始消費(fèi)事件啦。接著看看這個(gè)方法的詳細(xì)代碼。

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0, unconsumedY = 0;
    int consumedX = 0, consumedY = 0;

    consumePendingUpdateOperations();
     ...

    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
        // Update the last touch co-ords, taking any scroll offset into account
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        if (ev != null) {
            ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        }
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
    } 
    ....
    return consumedX != 0 || consumedY != 0;
}

滑動(dòng)的還是,又得調(diào)用 dispatchNestedScroll() 方法來(lái)問(wèn)問(wèn)爸爸,我真的可以消費(fèi)了哇?然后事件有傳給了爸爸,爸爸如果消費(fèi)了,又返回 true,那么又需要將已經(jīng)消費(fèi)了減掉,然后才是自己可以消費(fèi)的了。

這說(shuō)明了啥? RecyclerView 真是個(gè)孝順聽(tīng)話(huà)的好孩紙啊,不過(guò)也好慘啊,到嘴的肉的被會(huì)被爸爸吃了,心疼它一秒鐘。

好了,到我們的下拉刷新這里,第一點(diǎn),我們也要當(dāng)孩紙,但是我們不能像 RecyclerView 那么聽(tīng)話(huà)孝順,我們的原則是只要事件孩紙傳遞給我了,我要優(yōu)先消費(fèi),而不是優(yōu)先去問(wèn)爸爸要不要消費(fèi),如果自己不能消費(fèi),或者已經(jīng)吃飽了,那么這個(gè)事件才去詢(xún)問(wèn)爸爸要不要消費(fèi)。

private final int[] mParentScrollConsumed = new int[2];
private final int[] mParentOffsetInWindow = new int[2];
private int mTotalUnconsumed;
private int mTotalUnconsumedLoadMore;

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    // If we are in the middle of consuming, a scroll, then we want to move the spinner back up
    // before allowing the list to scroll
    if (refreshEnable && header != null && !isRefreshing && currentStatus == STATE_REFRESH && dy > 0 && mTotalUnconsumed > 0) {
        mTotalUnconsumed -= dy;

        if (mTotalUnconsumed <= 0) {//over
            mTotalUnconsumed = 0;
            dy = (int) (-getScrollY() / DRAG_RATE);
        }
        goToRefresh(-dy);
        consumed[1] = dy;
    }

    if (loadEnable && !isLoading && footer != null && dy < 0 && getScrollY() >= bottomScroll && mTotalUnconsumedLoadMore > 0 && currentStatus == STATE_LOADMORE) {
        mTotalUnconsumedLoadMore += dy;
        goToLoad(dy);
        consumed[1] = dy;
    }
    // 最后,我們?cè)偃?wèn)問(wèn)爸爸需要消費(fèi)事件不。
    final int[] parentConsumed = mParentScrollConsumed;
    if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
        consumed[0] += parentConsumed[0];
        consumed[1] += parentConsumed[1];
    }
}

請(qǐng)注意,上面這個(gè)都是針對(duì)在下拉后回退的情況(onNestedPreScroll()方法中的情況。),意思就是說(shuō),如果我現(xiàn)在 Header已經(jīng)拉出來(lái)了,然后又向上滑動(dòng),這個(gè)時(shí)候肯定要優(yōu)先先隱藏 Header,然后剩下的才讓CoordinatorLayout 等爸爸去消費(fèi)。

但是但是如果是在下來(lái)的過(guò)程呢?這個(gè)時(shí)候還是應(yīng)該先問(wèn)問(wèn)爸爸要不要消費(fèi),因?yàn)槟悴荒茉谙吕念^都出現(xiàn)完了之后再去把 AppBarLayout 等展開(kāi)吧,肯定是先先展開(kāi)它們,最后才是下來(lái)刷新。

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                           int dxUnconsumed, int dyUnconsumed) {
    // Dispatch up to the nested parent first
    dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
            mParentOffsetInWindow);

    final int dy = dyUnconsumed + mParentOffsetInWindow[1];
    if (refreshEnable && header != null && dy < 0 && !isRefreshing && !canChildScrollUp()) {
        mTotalUnconsumed += Math.abs(dy);
        if (currentStatus == STATE_DEFAULT || mTotalUnconsumed != 0)
            currentStatus = STATE_REFRESH;
        goToRefresh(Math.abs(dy));
    }

    if (loadEnable && (isAutoLoad || footer != null) && getScrollY() >= bottomScroll && dy > 0 && !isLoading && mTotalUnconsumedLoadMore <= 4 * footHeight) {
        mTotalUnconsumedLoadMore += dy;
        if (currentStatus == STATE_DEFAULT || mTotalUnconsumedLoadMore != 0)
            currentStatus = STATE_LOADMORE;
        goToLoad(dy);
    }
}

到這里,嵌套機(jī)制基本上就說(shuō)完了。對(duì)于加載更多,原理類(lèi)似,不在分析代碼。

具體滑動(dòng)

至于具體的滑動(dòng),就是上面的兩個(gè)方法所示,一個(gè) goToRefresh() 用于下拉,一個(gè) goToLoad() 用于加載更多,最后都是調(diào)用 scrollBy() 的方法。

狀態(tài)回調(diào)

因?yàn)?HeaderFooter 都是傳入的嘛,所以在滑動(dòng)的過(guò)程中,需要把相關(guān)狀態(tài)通知到吧,然后就可以顯示出 下拉刷新、松開(kāi)刷新、正在刷新等狀態(tài)了吧。

public interface HeaderListener {


void onRefreshBefore(int scrollY, int headerHeight);


void onRefreshAfter(int scrollY, int headerHeight);


void onRefreshReady(int scrollY, int headerHeight);


void onRefreshing(int scrollY, int headerHeight);

void onRefreshComplete(int scrollY, int headerHeight, boolean isRefreshSuccess);


void onRefreshCancel(int scrollY, int headerHeight);

int getRefreshHeight();
}

這里來(lái)定義了 HeaderListener 的接口,這個(gè)就需要對(duì)應(yīng)的 Header 來(lái)實(shí)現(xiàn),然后就可以得到是是的狀態(tài)回調(diào)了。 getRefreshHeight() 該方法定義什么高度就可以執(zhí)行松手刷新了。系統(tǒng)默認(rèn)的是 Header 的高度。

布局

      <com.lovejjfg.powerrefresh.PowerRefreshLayout
        android:id="@+id/refresh_layout"
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler"
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context="com.lovejjfg.demo.MainActivity">
        </android.support.v7.widget.RecyclerView>

    </com.lovejjfg.powerrefresh.PowerRefreshLayout>

相關(guān)方法調(diào)用

mRefreshLayout.setOnRefreshListener(new OnRefreshListener() {
        @Override
        public void onRefresh() {
            mRefreshLayout.postDelayed(new Runnable() {
                @Override
                public void run() {
                    //刷新完畢
                    mRefreshLayout.stopRefresh(true);
                }
            }, 1000);
        }

        @Override
        public void onLoadMore() {
            mRefreshLayout.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mRefreshLayout.stopLoadMore(true);
                    List<String> mlist = new ArrayList<>();
                    for (int i = 0; i < 10; i++) {
                        mlist.add("nice" + i);
                    }
                    mAdapter.appendList(mlist);
                    //是否還能加載更多
                    mRefreshLayout.setLoadEnable(mAdapter.getList().size() < 50);
                }
            }, 1000);
        }
    });
    CircleHeaderView header = new CircleHeaderView(getContext());
    FootView footView = new FootView(getContext());
    //添加 header
    mRefreshLayout.addHeader(header);
    //添加 footer
    mRefreshLayout.addFooter(footView);

如果進(jìn)入頁(yè)面需要顯示出下拉刷新,可以調(diào)用:

    mRefreshLayout.setAutoRefresh(true);

如果不需要Footer,但是需要加載更多,可以調(diào)用:

      mRefreshLayout.setAutoLoadMore(true);

刷新完畢Header會(huì)馬上隱藏上去,如果需要延遲的話(huà),可以調(diào)用:

      mRefreshLayout.stopRefresh(true,5000);

一般項(xiàng)目的下拉刷新加載更多就是一套,每次都去調(diào)用 addHeader() 或者 addFooter() 就會(huì)很煩躁了,這時(shí)候你應(yīng)該考慮繼承自PowerRefreshLayout,將 HeaderFooter 直接初始化好。

項(xiàng)目地址

辛苦擼了這么久,點(diǎn)個(gè)start 唄。
https://github.com/lovejjfg/PowerRefresh

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,319評(píng)論 25 708
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 47,176評(píng)論 22 665
  • 1遇見(jiàn)蘇涼純屬偶然 2005年的夏天,我在一個(gè)常去的四川小餐館里,對(duì)對(duì)面走來(lái)的女孩揮揮手;女孩,有點(diǎn)瘦,穿白色棉布...
    花間微語(yǔ)閱讀 964評(píng)論 4 0
  • 最近項(xiàng)目需要mac系統(tǒng),由于本機(jī)是win,公司也不給配就只能裝虛擬機(jī)了。好不容易裝成功后,需要ios打包鏈接真機(jī),...
    潛鳥(niǎo)閱讀 2,168評(píng)論 1 1

友情鏈接更多精彩內(nèi)容