自個兒寫Android的下拉重新整理/上拉載入控制項

來源:互聯網
上載者:User

標籤:

前段時間自己寫了一個能夠“通用”的,支援下拉重新整理和上拉載入的自訂控制項。可能現如今這已經不新鮮了,但有興趣的朋友還是可以一起來看看的。

  • 與通常的View配合使用(比如ImageView)

  • 與ListView配合使用

  • 與RecyclerView配合使用

  • 與SrcollView配合使用

  • 局部重新整理(但想必這種需要實際應該還是不多的….)

好啦,效果大概就是這樣。如果您看後覺得有一點興趣。那麼,以下是相關的資訊:

  • GitHub地址:
    https://github.com/RawnHwang/SmartRefreshLayout

  • Gradle依賴:
    compile ‘me.rawnhwang.library:smart-refresh-layout:1.0.0-rc’

好了,閑話就到這裡了。現在正式切入正題,於此逐步簡單的記錄和總結一下實現這個自訂View的思路以及實現過程。

首先,我們分析一下:假設我們現在的需求是需要讓ListView支援下拉重新整理和上拉載入,那麼其實我們選擇去擴充系統自身的ListView是最好的。
但我們這裡的初衷是創造一個通用的Pullable的控制項,也就是說它可以配合Android中各種View使用。所以,顯然我們需要的是一個ViewGroup。
那麼,既然有了思路就可以開動了:第一步我們先去建立我們自己的View,並讓其繼承自ViewGroup。例如就像下面這樣:

public class PullableLayout extends ViewGroup{    public PullableLayout(Context context) {        super(context);    }    public PullableLayout(Context context, AttributeSet attrs) {        super(context, attrs);    }}

接下來,我們靜靜的思考一下所謂的下拉重新整理,上拉載入的本質何如。就會發現,其實歸根結底原理仍舊是“視圖的滾動”而已。
那麼,我們來分析下我們為什麼會這麼說呢?假設現在先在腦海中簡單構畫一下如下所示的這樣一個ViewGroup的結構圖:

假設中藍色的部分就是螢幕地區,也就是我們想要呈現內容的地區(比如我們在這裡放一個ListView)。而我們的ViewGroup所需要做的工作就是:
為Content部分加上一個Header(頭視圖)與Footer(尾視圖),並且顯然Header的位置應該位於Content之上,同理Footer則位於其之下。

那麼,在這個基礎上,如果我們讓整個Viewgroup支援滾動,那麼就得以實現一種效果了,即:初始情況下,螢幕上將正常呈現我們的Content視圖。
與此同時:當我們上下滑動螢幕,那麼當滑動到Content視圖的頂部時,就會出現Header視圖;當滑動到Content的底部時,則會出現Footer視圖。

當然,這種紙上談兵式的原理性的東西,永遠都讓人感到無聊。所以,現在我們實際的來“兌換”一下我們目前為止談到的這種效果。看以下布局檔案:

左邊的布局非常簡單和熟悉,就是顯示一個寬高填滿父視窗的ImageView。而在右邊我們則是把父布局替換成了我們自訂的PullableLayout。

好的,現在我們就一起來看看,我們應該怎麼樣逐步完善PullableLayout讓它實現我們說到的效果。
首先,既然我們說到需要一個Header與Footer。那麼,我們就先來定義好這兩個東東的布局。比如說,我們定義一個如下的Header布局:

這個布局還是非常簡單明了的。同樣的,Footer布局的定義其實與Header是非常類似的,所以就不再貼一次代碼了。
準備好Header與Footer布局後,我們應該考慮的工作,就是怎麼把它們按照我們的需要給“放進”我們自己的PullableLayout當中了,其實這並不難。

private View mHeader,mFooter;    public PullableLayout(Context context, AttributeSet attrs) {        super(context, attrs);        mHeader = LayoutInflater.from(context).inflate(R.layout.header_pullable_layout,null);        mFooter = LayoutInflater.from(context).inflate(R.layout.footer_pullable_layout,null);    }    @Override    protected void onFinishInflate() {        super.onFinishInflate();        // 看這裡哦,親        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams                (RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT);        mHeader.setLayoutParams(params);        mFooter.setLayoutParams(params);        addView(mHeader);        addView(mFooter);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        // 測量        for (int i = 0; i < getChildCount(); i++){            View child = getChildAt(i);            measureChild(child,widthMeasureSpec,heightMeasureSpec);        }    }    private int mLayoutContentHeight;    @Override    protected void onLayout(boolean changed, int l, int t, int r, int 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 if (child == mFooter) { // 尾視圖隱藏在layout所有內容視圖之後                child.layout(0, mLayoutContentHeight, child.getMeasuredWidth(), mLayoutContentHeight + child.getMeasuredHeight());            } else { // 內容視圖根據定義(插入)順序,按由上到下的順序在垂直方向進行排列                child.layout(0, mLayoutContentHeight, child.getMeasuredWidth(), mLayoutContentHeight + child.getMeasuredHeight());                mLayoutContentHeight += child.getMeasuredHeight();            }        }    }

以上的代碼也並不複雜,核心的工作就是填充Header與Footer視圖,並且按需要進行測量和置位的工作。如果作為新手來說,值得注意的可能就是:

  • Header與Footer的addView()工作:如果放在Constructor中,那麼因為此時布局檔案中的內容都還未進行裝載和填充,就可能會在後續的代碼中因為某些代碼邏輯出現意料之外的異常錯誤;而如果放在onMeasure,則會因為onMeasure的內部機製造成重複add。所以放在onFinishInflate算是一個比較合適的選擇。

  • 個人在這裡定義了一個變數mLayoutContentHeight用來記錄內容視圖部分的實際總高度。需要注意的是,要在onLayout開頭的地方將其置零,否則同樣會因為重複累加得到錯誤的結果。

現在,當我們運行程式,就會在螢幕上呈現一個寬高佔滿螢幕的圖片。目前看起來是與把ImageView放在其它常用的Layout中的效果是沒有區別的。

所以,顯然我們接下來要做的工作就是讓視圖能夠跟隨著我們的手指滾動起來。那麼,還有什麼好想的呢?自然就是覆寫onTouchEvent了。

  private int mLastMoveY;    @Override    public boolean onTouchEvent(MotionEvent event) {        int y = (int) event.getY();        switch (event.getAction()){            case MotionEvent.ACTION_DOWN:                mLastMoveY = y;                break;            case MotionEvent.ACTION_MOVE:                int dy = mLastMoveY - y;                scrollBy(0, dy);                break;        }        mLastMoveY = y;        return true;    }

我們看到現在似乎已經有點意思了,但其實顯然是遠遠不夠的。現在說穿了就只是一個支援滾動的視圖而已,看上去非常呆板,更別提下拉重新整理此類了。

那麼,我們想一下應該怎麼改進呢?有了,我們可以給每次的拉動設定一些相關資訊,比如“最大滾動距離,有效距離”等等。這是什麼意思呢?
打個比方:當拉動的距離超過了最大距離,我們就不允許視圖繼續滾動了;而當此次拉動的距離超過有效距離我們就認為這是一次有效行為。
那麼現在我們先做點小改進,當拉動的距離超過有效距離,我們就將文字資訊改為“鬆開重新整理”,以提示使用者你現在鬆開手指就會執行重新整理的行為了。

            case MotionEvent.ACTION_MOVE:                int dy = mLastMoveY - y;                // dy < 0代表是針對下拉重新整理的操作                if(dy < 0) {                    if(Math.abs(getScrollY()) <= mHeader.getMeasuredHeight() / 2) {                        scrollBy(0, dy);                        if(Math.abs(getScrollY()) >= effectiveScrollY){                            tvPullHeader.setText("鬆開重新整理");                        }                    }                }                break;

這裡我們所做的改動實際就是:當進行下拉操作的時候,如果下拉距離已經達到header的一半高度,就不允許繼續下拉了。
同時來說,如果當我們的拉動行為超過了有效距離effectiveScrollY,就提示使用者可以“鬆開重新整理”了。同樣的,看看效果如何:

顯然,我們又向前邁進了小小的一步。但最終的效果依舊有些呆板。因為雖然提示了可以“鬆開重新整理”,但現在即使我們鬆開,也不會有任何效果。
鬆開手指卻沒有對應效果,顯然是因為我們還沒有在Action_Up的時候做對應的操作,那麼現在就來進一步的修改吧:

            case MotionEvent.ACTION_UP:                if(Math.abs(getScrollY()) >= effectiveScrollY){                    mLayoutScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + effectiveScrollY));                    tvPullHeader.setVisibility(View.GONE);                    pbPullHeader.setVisibility(View.VISIBLE);                }else{                    mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());                }                break;

因為僅僅是為了說明原理,所以這一步的改動代碼也非常的簡單。簡單來說就是:如果鬆開手指時,滑動的距離並未超過有效距離,我們就認為這並不是一次成功有效重新整理行為,那麼讓view的位置變動恢複就行了。而如果手指離開時,已經滑動超過了有效驅離,則將view滑動到剛好能夠讓Header顯示出有效距離的部分的位置,來提示使用者正處於重新整理的狀態下。對應下面的就更容易理解我們所做的工作是什麼了:

讓人高興的是,到了這裡看上去效果就很不錯了。但雖然效果是有了,看上去像是在重新整理,實際卻沒有執行任何實際用於重新整理的操作。
所以說,顯然我們還需要提供一個回調介面,讓client端在使用的時候能夠順利在合適的時機執行需要的操作(重新整理/載入)。

 public interface onRefreshListener{        void onRefresh();    }    private onRefreshListener mRefreshListener;    public void setRefreshListener(onRefreshListener listener){        mRefreshListener = listener;    }    public void refreshDone(){        mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());        pbPullHeader.setVisibility(View.GONE);        tvPullHeader.setText("繼續向下拉");        tvPullHeader.setVisibility(View.VISIBLE);    }
case MotionEvent.ACTION_UP:if(Math.abs(getScrollY()) >= effectiveScrollY){   // 省略之前的代碼......   // 執行回調   mRefreshListener.onRefresh();}else{   mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());}break;
public class MainActivity extends AppCompatActivity {    private PullableLayout plMain;    private ImageView iv;    private Handler mHandler = new Handler() {        @Override        public void handleMessage(Message msg) {            iv.setBackgroundResource(R.drawable.ace);            plMain.refreshDone();        }    };    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        iv = (ImageView) findViewById(R.id.iv);        plMain = (PullableLayout) findViewById(R.id.pl_main);        plMain.setRefreshListener(new PullableLayout.onRefreshListener() {            @Override            public void onRefresh() {                 new Thread(new Runnable() {                     @Override                     public void run() {                         try {                             Thread.sleep(3000);                         } catch (InterruptedException e) {                             e.printStackTrace();                         }                         mHandler.sendEmptyMessage(0);                     }                 }).start();            }        });    }}

OK,大功告成,現在我們在來看一看效果如何:

可以看到,到這裡我們就已經完全實現了“下拉重新整理”這一功能了。當然這裡只是為了示範原理的demo,所以很多代碼都沒有那麼的追求嚴謹。
當然,這裡要總結的重點其實也只是個人的思路和實現原理而已。所以同理,只要理解了這種思路,“上拉載入”也同樣就能夠實現了,故不再贅述。

那麼,是不是到了這裡,我們就可以結束了呢?當然不是,因為之前我們說過需要讓我們的PullableLayout是通用的。而以目前來說:
我們絕大多數普通的常用控制項,是能夠通用的。但是呢?對另一類以ListView,GridView,RecyclerView,ScrollView為代表的控制項就不靈了。
顯然,這類控制項與普通的View相比,最大的特點就是:它們自身就是支援滾動的。所以無法避免的,就會與我們的控制項出現“滑動衝突”。

那麼,關於“滑動衝突”的解決方案,可以參考《Android開發藝術探索》,作者針對各種常見的滑動衝突都給出了非常實用的乾貨方案。
OK,這裡我們假設以ListView與我們自訂的Layout配合使用為例。那麼出現的滑動衝突就是,雙方都需要處理上下滑動的行為。
《Android開發藝術探索》中已經說過,這種衝突往往都可以從商務邏輯上找到突破口。那麼,我們來思考一下這個所謂的“突破口”:
顯然,如果我們的ListView需要下拉重新整理或者上拉載入,那麼重新整理行為的發生時機就是在ListView的內容已經到達最現有的最頂部時,再繼續下拉。
同理,載入的行為發生的時機就是內容已經到達最現有的最底部時,繼續上拉。所以,如此一分析,這個突破口就已經出現了:
以下拉行為為例,我們就應該在ListView未到達頂部的情況下,將滑動事件交給ListView處理。而如果已經到達頂部,就將事件攔截,自己處理

現在我們的思路已經明確了,接著要做的,自然就是將思路轉化到代碼上面了。其實,所謂的“滑動衝突”的處理,最終實際就是迴歸到在ViewGroup的onInterceptTouchEvent方法上根據商務邏輯處理事件的攔截。對應我們這裡的需求來說,以ListView的下拉操作為例,就可以這樣做:

    @Override    public boolean onInterceptTouchEvent(MotionEvent event) {        boolean intercept = false;        // 記錄此次觸摸事件的y座標        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(0);                    if (child instanceof AdapterView) {                        AdapterView adapterChild = (AdapterView) child;                        // 判斷AbsListView是否已經到達內容最頂部(如果已經到達最頂部,就攔截事件,自己處理滑動)                        if (adapterChild.getFirstVisiblePosition() == 0                                || adapterChild.getChildAt(0).getTop() == 0) {                            intercept = true;                        }                    }                }                break;            }            // Up事件            case MotionEvent.ACTION_UP: {                intercept = false;                break;            }        }        mLastMoveY = y;        return intercept;    }

好了,差不多就是這樣了。再次說明這裡主要旨在總結和分享一下個人對於此類需求的實現思路。當然大家可能會有更加優秀的實現方式,請多多指教!
另外,也可能有朋友注意到在最初的示範圖中,使用了兩個比較有趣的Loading動畫。一個是下拉時的小幽靈,一個時上拉時的吃豆子的形象。
同樣再次申明:這兩種效果都來自Github上一位作者開源的庫:https://github.com/ldoublem/LoadingView,裡面有很多有意思的Loading效果。
個人而言,對那個小幽靈的形象比較有興趣,所以也簡單研究了下作者的源碼。如果您也有興趣,那也可以看一看我之前寫的:用Canvas和屬性動畫造一隻萌蠢的“小鬼”。

自個兒寫Android的下拉重新整理/上拉載入控制項

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.