Android做下拉刷新的時候,在做些什么

轉(zhuǎn)載注明出處:http://www.itdecent.cn/p/4607129c9efa

1. 簡介

好長時間沒有寫博客了,一來是工作忙,抽不出空,二來是迷上了王者榮耀?,F(xiàn)在正好趕上項(xiàng)目空閑期,寫一篇關(guān)于下拉刷新的文章,個人覺得上來加載更多功能使用場景非常少,而且沒有必要做的那么麻煩,文章最后會提一下加載更多的實(shí)現(xiàn)。

最近項(xiàng)目中遇見了下拉刷新的需求,正好研究了一下,分享一下自己的心得。

主要參考文章或工程:

郭霖大神—Android下拉刷新完全解析,教你如何一分鐘實(shí)現(xiàn)下拉刷新功能

自個兒寫Android的下拉刷新/上拉加載控件

XListView

這三篇文章各自提供了實(shí)現(xiàn)下拉刷新的思路,文章會分別介紹這三種實(shí)現(xiàn)方式的優(yōu)劣。文章中會涉及到點(diǎn)擊事件分發(fā)知識,大家可以查看這篇文章Android事件分發(fā)機(jī)制詳解。自己寫對三種實(shí)現(xiàn)做了部分優(yōu)化,寫了demo,地址鏈接

2. 分析

下拉刷新主要分為兩部分,一部分是刷新頭部Header,一部分是內(nèi)容展示區(qū)域,一般是列表。通過某些方法,來控制刷新頭部Header的展示范圍,達(dá)到下拉刷新的效果,如下圖。

圖-1 下拉刷新原理圖

做下拉刷新之前,分析一下下拉刷新場景以及達(dá)到的效果,常見的下拉刷新最少有四種狀態(tài)

  • 正常狀態(tài),下拉刷新頭部不展示,用戶可以正常操作列表
  • 下拉狀態(tài),用戶下拉列表,但是沒有到達(dá)刷新時機(jī),松開手后,刷新頭部會自動隱藏
  • 松開刷新狀態(tài),到達(dá)這個狀態(tài)時候,刷新頭部是完全展示的,用戶松開手,即可刷新,如果下拉距離過大,列表會自動上移,完整的露出刷新頭部,頭部顯示刷新中文案。
  • 刷新中狀態(tài),請求數(shù)據(jù)的刷新態(tài),在這種狀態(tài)下,根據(jù)交互需求有不同的實(shí)現(xiàn)。
    • 刷新中狀態(tài),用戶不能操作列表
    • 刷新中狀態(tài),可以滑動和操作列表,但刷新頭部一直置頂
    • 刷新中狀態(tài),可以滑動和操作列表,刷新頭部會隨著列表的滑動而一起滑動,參考欣慰微博下拉刷新。(大部分下拉刷新的交互效果)

前三種狀態(tài)會根據(jù)用戶手勢的移動相互切換,大部分下拉刷新中狀態(tài)交互是第三種,以新浪微博為參考藍(lán)本,本文最終實(shí)現(xiàn)的效果也是以這個效果為目標(biāo)。

3. 第一個例子鏈接

郭霖大神文章篇幅寫的比較多,很多可以不用關(guān)心,關(guān)于下拉刷新的核心代碼在ListView的OnTouchListener中,是通過修改Header的MarginTop值控制Header顯示可見范圍,到達(dá)下拉刷新的效果。缺點(diǎn)就是,每次更改Header的MarginTop值時候,會觸發(fā)父布局重新onMeasure()/onLayout()方法,如果ListView中Item內(nèi)容比較復(fù)雜,有卡頓現(xiàn)象,同時沒有處理刷新中狀態(tài)點(diǎn)擊事件,如果要處理,需要額外添加復(fù)雜的邏輯。

3.1 第一個例子實(shí)現(xiàn)過程

3.1.1 初始化Header

父布局中包含下拉刷新的Header和ListView,在父布局的構(gòu)造方法中實(shí)例化Header,并放入父布中。

public PtrFirstRefreshableView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}

/**
 * 實(shí)例化刷新頭部并將刷新頭部添加的父布局
 */
private void init() {
    mHeader = new PtrFirstRefreshHeader(getContext());
    touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    addView(mHeader, 0);
}

3.1.2 隱藏Header

在父布局中onLayout()方法中,設(shè)置Header的topMarigin,隱藏Header,設(shè)置ListView的點(diǎn)擊監(jiān)聽器,記錄一個標(biāo)簽isLayouted保證只設(shè)置一次。

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    
    // 如果是第一次Layout, 做一些設(shè)置
    if (changed && !isLayouted) {
        isLayouted = true;
        // 設(shè)置刷新頭部MarginTop, 隱藏刷新頭部
        mHeaderHeight = mHeader.getHeight();
        mHeaderLayoutParams = (LayoutParams) mHeader.getLayoutParams();
        mHeader.setTopMargin(-mHeaderHeight);

        // 設(shè)置ListView的事件監(jiān)聽
        mListView = (ListView) getChildAt(1);
        mListView.setOnTouchListener(this);
    }
}

3.1.3 處理點(diǎn)擊事件

這個地方邏輯復(fù)雜一些,獲取用戶點(diǎn)擊事件后,調(diào)用checkTopShow()方法檢查當(dāng)前是否需要處理點(diǎn)擊事件,如果ListView的第一個Item展示,且頂部距離父布局為0,則可以下拉刷新。

DOWN事件中記錄用戶起始位置,注意一定要通過getRawY()獲取手指相對屏幕的位置,而不是通過getY()獲取手指相對ListView的位置,因?yàn)長istView會隨著手指滑動而滑動,如果用getY()獲取位置會有偏差。

MOVE事件中,如果用戶手指向上滑動,且刷新頭部是完全隱藏的,不做處理;如果當(dāng)時非刷新中狀態(tài),根據(jù)頭部MarginTop的值更改當(dāng)前刷新狀態(tài),同時更改刷新頭部MarginTop。

UP事件中,用戶松開手,如果當(dāng)前狀態(tài)是下拉狀態(tài),則隱藏刷新頭部;如果當(dāng)前狀態(tài)是松開刷新狀態(tài),則更改狀態(tài)為刷新中狀態(tài),同是隱藏多余margin,僅顯示完整的刷新頭部,同時調(diào)用回調(diào)監(jiān)聽(在RefreshingTask類中)。

在整個過程中,如果當(dāng)前狀態(tài)處于下拉狀態(tài)或者松開刷新狀態(tài),設(shè)置ListView屬性,讓ListView失去焦點(diǎn),否則那點(diǎn)擊Item會一直處于點(diǎn)擊狀態(tài)。

public boolean onTouch(View v, MotionEvent event) {
    checkTopShow(event);
    if (ableToPull) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownY = event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                float yMove = event.getRawY();
                int distance = (int) (yMove - mDownY);
                // 如果手指向上滑動,并且下拉頭是完全隱藏的,不處理
                if (distance <= 0 && mHeader.getTopMargin() <= -mHeaderHeight) {
                    return false;
                }
                if (distance < touchSlop) {
                    return false;
                }
                if (mStatus != STATUS_REFRESHING) {
                    if (mHeader.getTopMargin()  > 0) {
                        mStatus = STATUS_RELEASE_TO_REFRESH;
                    } else {
                        mStatus = STATUS_PULL_TO_REFRESH;
                    }
                    // 通過偏移下拉頭的topMargin值,來實(shí)現(xiàn)下拉效果
                    int topMargin = (distance / 2) - mHeaderHeight;
                    mHeader.setTopMargin(topMargin);

                    // 更新刷新頭部圓環(huán)動畫
                    mHeader.updateCircle(Math.abs(distance * 1f / 2f / mHeaderHeight));
                }
                break;
            case MotionEvent.ACTION_UP:
            default:
                if (mStatus == STATUS_RELEASE_TO_REFRESH) {
                    // 松手時如果是釋放立即刷新狀態(tài),就去調(diào)用正在刷新的任務(wù)
                    mStatus = STATUS_REFRESHING;
                    updateHeaderView();
                    new RefreshingTask().execute();
                    mHeader.startLoading();
                } else if (mStatus == STATUS_PULL_TO_REFRESH) {
                    // 松手時如果是下拉狀態(tài),就去調(diào)用隱藏下拉頭的任務(wù)
                    mStatus = STATUS_NORMAL;
                    updateHeaderView();
                    new HideHeaderTask().execute();
                }
                break;
        }

        if (mStatus == STATUS_PULL_TO_REFRESH ||
                mStatus == STATUS_RELEASE_TO_REFRESH) {
            mListView.setPressed(false);
            mListView.setFocusable(false);
            mListView.setFocusableInTouchMode(false);
            updateHeaderView();
            return true;
        }
    }
    return false;
}


private void checkTopShow(MotionEvent event) {
    View firstChild = mListView.getChildAt(0);
    if (firstChild != null) {
        // 如果列表第一個item可見且距離ListView頂部為0,則說明ListView已經(jīng)到最頂部,此時可以下拉刷新
        int firstVisiblePos = mListView.getFirstVisiblePosition();
        if (firstVisiblePos == 0 && firstChild.getTop() == 0) {
            ableToPull = true;
        } else {
            if (mHeader.getTopMargin() != -mHeaderHeight) {
                mHeader.setTopMargin(-mHeaderHeight);
            }
            ableToPull = false;
        }
    } else {
        ableToPull = true;
    }
}

3.1.4 隱藏頭部

在用戶手指離開屏幕時候,會根據(jù)當(dāng)前狀態(tài)選擇是隱藏頭部還是僅展示頭部,僅以隱藏頭部為例,代碼如下。關(guān)于AsyncTask的使用可以查看AsyncTask 第一篇使用篇

class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {

    @Override
    protected Integer doInBackground(Void... params) {
        int topMargin = mHeaderLayoutParams.topMargin;
        while (true) {
            topMargin = topMargin + SCROLL_SPEED;
            if (topMargin <= -mHeaderHeight) {
                topMargin = -mHeaderHeight;
                break;
            }
            publishProgress(topMargin);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return topMargin;
    }

    @Override
    protected void onProgressUpdate(Integer... topMargin) {
        mHeader.setTopMargin(topMargin[0]);
    }

    @Override
    protected void onPostExecute(Integer topMargin) {
        mHeader.setTopMargin(topMargin);
    }
}

3.1.5 請求完畢恢復(fù)原狀態(tài)

網(wǎng)絡(luò)請求完成后,要隱藏刷新頭部,同時恢復(fù)原狀態(tài)。

public void finishRefreshing() {
    mStatus = STATUS_NORMAL;
    new HideHeaderTask().execute();
    mHeader.stopLoading();
}

3.2 第一個例子實(shí)現(xiàn)效果

最終的實(shí)現(xiàn)效果如下:

圖-2 第一個例子實(shí)現(xiàn)效果圖

3.3 第一個例子總結(jié)

該方案思路清晰,不需要對ListView進(jìn)行拓展。缺點(diǎn)也比較明顯,如果ListView中Item過于復(fù)雜,會有卡頓現(xiàn)象,而且代碼中并沒有對刷新中狀態(tài)的點(diǎn)擊事件進(jìn)行處理,如果在刷新中狀態(tài)中,滑動布局,會將刷新頭部隱藏,在完成請求之前,無法將頭部下拉展出,要對此進(jìn)行修復(fù),需要添加額外的邏輯。不推薦。

4. 第二個例子鏈接

原文章是使用scrollTo()/scrollBy()方法實(shí)現(xiàn)下拉刷新,默認(rèn)控件向上位移一段距離,正好將刷新頭部隱藏。然后根據(jù)用戶的手勢通過scrollBy()方法將刷新頭部逐漸展示出來。因?yàn)槭褂?code>scrollTo()/scrollBy()來移動控件,是移動父布局中所有的子控件,如果邏輯處理不當(dāng)會出現(xiàn)子控件部分移出父布局的情況,子控件顯示出現(xiàn)問題。原文章實(shí)現(xiàn)很簡單,下面實(shí)例的代碼是做過優(yōu)化后的代碼實(shí)現(xiàn)。

4.1 第二個例子實(shí)現(xiàn)過程

4.1.1 初始化Header

實(shí)例化Header,并將其添加至父布局。

public PtrSecondRefreshableView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

private void init(Context context) {
    mHeader = new PtrSecondRefreshHeader(context);
    addView(mHeader, 0);

    mScroller = new Scroller(getContext());
}

4.1.2 隱藏Header

在父布局的onMeasure()方法中測量子View的大小,在onLayout()方法中將刷新頭部向上偏移,達(dá)到隱藏Header效果。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 測量子View大小
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
}

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

    mLayoutContentHeight = 0;
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        if (child == mHeader) { // 如果是刷新頭部,向上偏移
            child.layout(0, 0 - child.getMeasuredHeight(), child.getMeasuredWidth(), 0);
        } else {
            child.layout(0, mLayoutContentHeight, child.getMeasuredWidth(), mLayoutContentHeight + child.getMeasuredHeight());
            mLayoutContentHeight += child.getMeasuredHeight();
        }
    }
}

4.1.3 攔截事件

在父布局的onTouchEvent中設(shè)置Header的可見范圍,所以用戶手勢在操作屏幕時候,在某些情況下父布局需要攔截點(diǎn)擊事件。

public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercept = false;

    if(mStatus == STATUS_REFRESHING) {
        return false;
    }

    // 記錄此次觸摸事件的y坐標(biāo)
    int y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercept = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            if (y > mLastMoveY) { // 下滑操作
                View child = getChildAt(1);
                if (child instanceof AdapterView) {
                    AdapterView adapterChild = (AdapterView) child;
                    // 判斷AbsListView是否已經(jīng)到達(dá)內(nèi)容最頂部(如果已經(jīng)到達(dá)最頂部,就攔截事件,自己處理滑動)
                    if (adapterChild.getFirstVisiblePosition() == 0
                            || adapterChild.getChildAt(0).getTop() == 0) {
                        intercept = true;
                    }
                }
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercept = false;
            break;
        }
    }

    mLastMoveY = y;

    return intercept;
}

4.1.4 處理點(diǎn)擊事件

重寫父布局的onTouchEvent(),根據(jù)用戶手勢做出相應(yīng)展示效果。

public boolean onTouchEvent(MotionEvent event) {
    float nowY = event.getY();

    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            mLastMoveY = nowY;
            break;
        case MotionEvent.ACTION_MOVE:
            float distance = mLastMoveY - nowY;
            if(distance < 0) { // 如果是向下滑動,移動子View
                // 頭部沒有完全展示
                if(Math.abs(getScrollY()) <= mHeader.getMeasuredHeight()) {
                    mStatus = STATUS_PULL_TO_REFRESH;
                    scrollBy(0, (int) (distance * DRAG_COEFFICIENT_NORMAL));
                } else { // 頭部已經(jīng)完全展示
                    scrollBy(0, (int) (distance * DRAG_COEFFICIENT_LIMIT));
                    mHeader.updateText(R.string.ptr_refresh_release);
                    mStatus = STATUS_RELEASE_TO_REFRESH;
                }
            } else { // 如果是向上滑動,移動子View
                if(getScrollY() < 0) {
                    scrollBy(0, (int) distance);
                }
                if(Math.abs(getScrollY()) <= mHeader.getMeasuredHeight()) {
                    mStatus = STATUS_PULL_TO_REFRESH;
                    mHeader.updateText(R.string.ptr_refresh_normal);
                }
            }

            // 更新刷新頭部動畫
            mHeader.updateCircle(Math.abs(getScrollY() * 1f/ mHeader.getMeasuredHeight()));

            break;
        case MotionEvent.ACTION_UP:
        default:
            if(mStatus == STATUS_RELEASE_TO_REFRESH) { // 用戶松開手后,如果是松開刷新狀態(tài),則回彈顯示完整Header,并刷新數(shù)據(jù)
                mHeader.updateText(R.string.ptr_refresh_refreshing);
                mHeader.startLoading();
                mStatus = STATUS_REFRESHING;
                    
                // 刷新狀態(tài)回調(diào)
                if(mListener != null) {
                    mListener.onRefresh();
                }

                mScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + mHeader.getMeasuredHeight()), 200);
                invalidate();
            } else if(mStatus == STATUS_PULL_TO_REFRESH){ // 用戶松開手后,如果是下拉刷新狀態(tài),則隱藏Header
                mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 200);
                invalidate();
                mHeader.updateText(R.string.ptr_refresh_normal);
                mStatus = STATUS_NORMAL;
            }
            break;
    }

    mLastMoveY = nowY;
    return true;
}

4.1.5 處理刷新中狀態(tài)

onInterceptTouchEvent()方法中,如果是刷新中狀態(tài),攔截事件,會導(dǎo)致用戶無法操作ListView;如果不攔截事件,則事件會傳遞到ListView,這樣當(dāng)用戶滾動列表ListView時候,刷新頭部會一直懸浮在頂部。所以需要在dispatchTouchEvent()方法中處理刷新中狀態(tài)。

public boolean dispatchTouchEvent(MotionEvent event) {
    int nowY = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastDownY = event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            if(mStatus == STATUS_REFRESHING) {
                float distance = mLastDownY - nowY;
                // 如果手勢向下滑動且列表中第一個Item可見,向下移動全部子View
                if(distance < 0
                        && mListView.getFirstVisiblePosition() == 0
                        && mListView.getChildAt(0).getTop()==0)  {
                    scrollBy(0, (int) (distance * DRAG_COEFFICIENT_LIMIT));
                    isListViewMove = true;
                    mLastDownY = nowY;

                    return true;
                } else { // 如果手勢向上滑動
                    if(getScrollY() < 0) { // 當(dāng)Header沒有完全隱藏,移動全部子View;當(dāng)Header完全隱藏,將事件傳遞給ListView
                        if(getScrollY() + distance > 0) {
                            scrollBy(0, 0);
                        } else {
                            scrollBy(0, (int) distance);
                        }
                        mLastDownY = nowY;
                        isListViewMove = true;
                        return true;
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
        default:
            // 用戶抬起手,如果子View通過scrollBy移動過
            if(isListViewMove) {
                isListViewMove = false;
                // 如果子View向下移動,向下移動距離大于Header高度,則自動回彈,顯示完整Header
                if(getScrollY() < 0 && Math.abs(getScrollY()) > mHeader.getMeasuredHeight()) {
                    mScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + mHeader.getMeasuredHeight()), 200);
                    invalidate();
                }
                return true;
            }
            isListViewMove = false;
            break;
    }

    return super.dispatchTouchEvent(event);
}

4.1.6 請求完畢恢復(fù)原狀態(tài)

請求完成后,隱藏Header,恢復(fù)原狀態(tài)。

public void finishRefresh() {
    if(!mScroller.isFinished()) {
        mScroller.abortAnimation();
    }
    scrollTo(0, 0);

    mHeader.updateText(R.string.ptr_refresh_normal);
    mHeader.stopLoading();
    mStatus = STATUS_NORMAL;
}

4.2 第二個例子最終實(shí)現(xiàn)效果

最終效果如下圖:

圖-3 第二個例子效果圖

4.3 第二個例子總結(jié)

涉及點(diǎn)擊事件的三個方法dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()都有對點(diǎn)擊事件不同的處理邏輯。雖然能勉強(qiáng)到達(dá)文章開頭提到的效果,但是在零界點(diǎn),特別是刷新頭部Header剛好隱藏的零界點(diǎn),會有卡頓現(xiàn)象,加上處理邏輯比較復(fù)雜。不推薦。

5. 第三個例子鏈接

第三個例子是很久的開源項(xiàng)目,不同于前兩種實(shí)現(xiàn)方式,前面兩種都是自定義一個父布局,然后將刷新頭部和列表放入其中,第三個例子是直接將刷新頭部放在列表ListView的頭部,然后動態(tài)的設(shè)置刷新頭部的高度,達(dá)到下拉刷新的效果。

5.1 實(shí)現(xiàn)過程

5.1.1 初始化Header

在ListView的構(gòu)造函數(shù)中,初始化Heade作為ListView的HeaderView,這里有兩點(diǎn)要注意,一是Header的布局文件,因?yàn)橐獎討B(tài)設(shè)置Header的高度,所以布局文件需要嵌套一層,外面一層動態(tài)設(shè)置高度,里面一層包容所有的Header布局,高度不變;二是因?yàn)槌跏糎eader是不顯示的,想要獲取Header的真正高度,要在所有的View初始化以后才能獲取。

public PtrThirdRefreshableView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

private void init(Context context) {
    mScroller = new Scroller(context, new DecelerateInterpolator());
    
    // 初始化Header,在初始化時候設(shè)置高度為0
    mHeader = new PtrThirdRefreshHeader(context);
    mHeaderContainer = mHeader.findViewById(R.id.dgp_header_container);

    addHeaderView(mHeader);

    mHeader.getViewTreeObserver().addOnGlobalLayoutListener(
            new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    // 獲取Header的完全展示時候的高度
                    getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    mHeaderViewHeight = mHeaderContainer.getHeight();
                    mHeader.setContentHeight(mHeaderViewHeight);
                }
            });
}

5.1.2 處理點(diǎn)擊事件

MOVE事件中,根據(jù)當(dāng)前狀態(tài),動態(tài)更新刷新Header的高度;在UP事件中根據(jù)當(dāng)前Header展示高度,來做相應(yīng)處理。

public boolean onTouchEvent(MotionEvent ev) {
    if (mLastY == -1) {
        mLastY = ev.getRawY();
    }

    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastY = ev.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            final float deltaY = ev.getRawY() - mLastY;
            mLastY = ev.getRawY();
            // 如果列表中第一個Item是可見的, 且Header的部分可見或者向下滑動,則動態(tài)設(shè)置Header高度
            if (getFirstVisiblePosition() == 0
                    && (mHeader.getShowHeight() > 0 || deltaY > 0)) {
                updateHeaderHeight(deltaY * OFFSET_RADIO);
                return true;
            }
            break;
        default:
            mLastY = -1;
            // 用戶松開手時候,如果列表第一個Item可以見
            if (getFirstVisiblePosition() == 0) {
                // 如果Header展示的高度大于Header的真正高度,則可刷新
                if (mHeader.getShowHeight()  > mHeaderViewHeight) {
                    mPullRefreshing = true;
                    mHeader.updateText(R.string.ptr_refresh_refreshing);
                    mHeader.startLoading();
                    if (mRefreshListener != null) {
                        mRefreshListener.onRefresh();
                    }
                }
                // 根據(jù)當(dāng)前情況重置Header高度
                resetHeaderHeight();
                return true;
            }
            break;
    }
    return super.onTouchEvent(ev);
}

/**
 * 動態(tài)更新Header高度
 * 
 * @param delta
 */
private void updateHeaderHeight(float delta) {
    mHeader.setShowHeight((int) (delta + mHeader.getShowHeight()));
    if (!mPullRefreshing) {
        if (mHeader.getShowHeight() > mHeaderViewHeight) {
            mHeader.updateText(R.string.ptr_refresh_release);
        } else {
            mHeader.updateText(R.string.ptr_refresh_normal);
        }
    }
    setSelection(0);
}

5.1.3 請求完畢恢復(fù)原狀態(tài)

請求完畢后,恢復(fù)原狀態(tài),這里沒有使用設(shè)置Header的高度來隱藏Header,為了移動平滑通過Scroller將Header移動到屏幕外,不顯示在屏幕中,達(dá)到隱藏的目的。使用Scroller需要復(fù)寫computeScroll()方法,才能移動。

public void finishRefresh() {
    if (mPullRefreshing == true) {
        mPullRefreshing = false;
        resetHeaderHeight();
        mHeader.stopLoading();
    }
}

private void resetHeaderHeight() {
    int height = mHeader.getShowHeight();
    if (height == 0)
        return;
    if (mPullRefreshing && height <= mHeaderViewHeight) {
        return;
    }
    int finalHeight = 0;
    // 如果當(dāng)前是刷新中狀態(tài),且Header的展示高度要大于Header的真實(shí)高度,則滑動列表,完整展示Header,否則隱藏Header
    if (mPullRefreshing && height > mHeaderViewHeight) {
        finalHeight = mHeaderViewHeight;
    }
    mScrollBack = SCROLL_BACK_HEADER;
    mScroller.startScroll(0, height, 0, finalHeight - height,
            SCROLL_DURATION);
    invalidate();
}

/**
 * 使用了Scroller, 需要復(fù)寫該方法
 */
@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        if (mScrollBack == SCROLL_BACK_HEADER) {
            mHeader.setShowHeight(mScroller.getCurrY());
        }
        postInvalidate();
    }
    super.computeScroll();
}

5.2 第三個例子最終實(shí)現(xiàn)效果

最終效果如下圖:

圖-4 第三個例子效果圖

5.3 第三個例子總結(jié)

相比前兩種實(shí)現(xiàn)方法,第三種是最簡單最方便的實(shí)現(xiàn)方式,而且完全不用考慮刷新中狀態(tài)的點(diǎn)擊事件處理,唯一的缺點(diǎn)可能要對某些手機(jī)做一些適配,個人比較推薦。

6. 上拉加載

這三個例子中,后面兩個例子都實(shí)現(xiàn)了上拉加載更多,而在市面上大部分應(yīng)用沒有上拉加載,看得出在實(shí)際場景中,上拉加載更多的使用頻率不高。以大量使用列表的應(yīng)用新浪微博為例子,滑動到列表最下方,繼續(xù)向上拉時候不會像下拉刷新一樣,有一定拉伸的彈簧效果,而是直接在加載了。我猜測這種實(shí)現(xiàn)是在Adapter中,當(dāng)滑動到最后一個Item時候,直接返回一個加載中的View,同時請求數(shù)據(jù),當(dāng)用戶看見這個View時候,其實(shí)請求已經(jīng)發(fā)出去了(部分應(yīng)用是設(shè)置一個按鈕,然用戶手動點(diǎn)擊請求數(shù)據(jù))。

7. 總結(jié)

下拉刷新開源庫很多,上面列舉出的幾種實(shí)現(xiàn)可能不是最優(yōu)的,個人認(rèn)為最好的下拉刷新庫是這個下拉刷新庫,它基本支持所有的布局。但是在選擇使用哪一個開源庫的時候,并不是實(shí)現(xiàn)的最全最好的那個,而是最貼合實(shí)際業(yè)務(wù)的庫。

在做下拉刷新時候,因?yàn)闆]有去對這個功能做出具體分析,走了很多彎路,浪費(fèi)很多時間,以此為戒。

最后附上三個例子工程地址:https://github.com/Kyogirante/PtrDemo

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

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

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