天天看點

Android 下拉重新整理架構實作

前段時間項目中用到了下拉重新整理功能,之前在網上也找到過類似的demo,但這些demo的品質參差不齊,使用者體驗也不好,接口設計也不行。最張沒辦法,終于忍不了了,自己就寫了一個下拉重新整理的架構,這個架構是一個通用的架構,效果和設計感覺都還不錯,現在分享給各位看官。

緻謝:

1. 感謝lk6233160同學提出的問題,旋轉View時調用setRotation方法隻能是在API Level11(3.0)以上才能用,這個問題的解決辦法是給ImageView設定一個Matrix,把Matrix上面作用一個旋轉矩陣,但是如果不是ImageView的話,可能實作起來比較麻煩,再次謝謝lk6233160同學。

2. 謝謝如毛毛風提出的問題,向下滑動後,再向上滑動到頭,隻能再松手後才能再次下拉。這個問題的回複請參考評論。

技術交流群:

QQ:197990971(人員已滿)

1. 關于下拉重新整理

下拉重新整理這種使用者互動最早由twitter創始人洛倫•布裡切特(Loren Brichter)發明, 有理論認為,下拉重新整理是一種适用于按照從新到舊的時間順序排列feeds的應用,在這種應用場景中看完舊的内容時,使用者會很自然地下拉查找更新的内容,是以下拉重新整理就顯得非常合理。大家可以參考這篇文章: 有趣的下拉重新整理,下面我貼出一個有趣的下拉重新整理的案例。

Android 下拉重新整理架構實作

圖一、有趣的下拉重新整理案例(一)

Android 下拉重新整理架構實作

圖一、有趣的下拉重新整理案例(二)

2. 實作原理

上面這些例子,外觀做得再好看,他的本質上都一樣,那就是一個下拉重新整理控件通常由以下幾部分組成: 【1】Header Header通常有下拉箭頭,文字,進度條等元素,根據下拉的距離來改變它的狀态,進而顯示不同的樣式 【2】Content 這部分是内容區域,網上有很多例子都是直接在ListView裡面添加Header,但這就有局限性,因為好多情況下并不一定是用ListView來顯示資料。我們把要顯示内容的View放置在我們的一個容器中,如果你想實作一個用ListView顯示資料的下拉重新整理,你需要建立一個ListView旋轉到我的容器中。我們處理這個容器的事件(down, move, up),如果向下拉,則把整個布局向下滑動,進而把header顯示出來。 【3】Footer Footer可以用來顯示向上拉的箭頭,自動加載更多的進度條等。

以上三部分總結的說來,就是如下圖所示的這種布局結構:

Android 下拉重新整理架構實作

圖三,下拉重新整理的布局結構

關于上圖,需要說明幾點: 1、這個布局擴充于 LinearLayout,垂直排列 2、從上到下的順序是:Header, Content, Footer 3、Content填充滿父控件,通過設定top, bottom的padding來使Header和Footer不可見,也就是讓它超出螢幕外 4、下拉時,調用scrollTo方法來将整個布局向下滑動,進而把Header顯示出來,上拉正好與下拉相反。 5、派生類需要實作的是:将Content View填充到父容器中,比如,如果你要使用的話,那麼你需要把ListView, ScrollView, WebView等添加到容器中。 6、上圖中的紅色區域就是屏的大小(嚴格來說,這裡說螢幕大小并不準确,應該說成内容區域更加準确)

3. 具體實作

明白了實作原理與過程,我們嘗試來具體實作,首先,為了以後更好地擴充,設計更加合理,我們把下拉重新整理的功能抽象成一個接口:

1、IPullToRefresh<T extends View>

它具體的定義方法如下:

public interface IPullToRefresh<T extends View> {
    public void setPullRefreshEnabled(boolean pullRefreshEnabled);
    public void setPullLoadEnabled(boolean pullLoadEnabled);
    public void setScrollLoadEnabled(boolean scrollLoadEnabled);
    public boolean isPullRefreshEnabled();
    public boolean isPullLoadEnabled();
    public boolean isScrollLoadEnabled();
    public void setOnRefreshListener(OnRefreshListener<T> refreshListener);
    public void onPullDownRefreshComplete();
    public void onPullUpRefreshComplete();
    public T getRefreshableView();
    public LoadingLayout getHeaderLoadingLayout();
    public LoadingLayout getFooterLoadingLayout();
    public void setLastUpdatedLabel(CharSequence label);
}
           

這個接口是一個泛型的,它接受View的派生類, 因為要放到我們的容器中的不就是一個View嗎?

2、PullToRefreshBase<T extends View> 這個類實作了IPullToRefresh接口,它是從LinearLayout繼承過來,作為下拉重新整理的一個 抽象基類,如果你想實作ListView的下拉重新整理,隻需要擴充這個類,實作一些必要的方法就可以了。這個類的職責主要有以下幾點:

  • 處理onInterceptTouchEvent()和onTouchEvent()中的事件:當内容的View(比如ListView)正如處于最頂部,此時再向下拉,我們必須截斷事件,然後move事件就會把後續的事件傳遞到onTouchEvent()方法中,然後再在這個方法中,我們根據move的距離再進行scroll整個View。
  • 負責建立Header、Footer和Content View:在構造方法中調用方法去建立這三個部分的View,派生類可以重寫這些方法,以提供不同式樣的Header和Footer,它會調用createHeaderLoadingLayout和createFooterLoadingLayout方法來建立Header和Footer建立Content View的方法是一個抽象方法,必須讓派生類來實作,傳回一個非null的View,然後容器再把這個View添加到自己裡面。
  • 設定各種狀态:這裡面有很多狀态,如下拉、上拉、重新整理、加載中、釋放等,它會根據使用者拉動的距離來更改狀态,狀态的改變,它也會把Header和Footer的狀态改變,然後Header和Footer會根據狀态去顯示相應的界面式樣。

3、PullToRefreshBase<T extends View>繼承關系 這裡我實作了三個下拉重新整理的派生類,分别是ListView、ScrollView、WebView三個,它們的繼承關系如下:

Android 下拉重新整理架構實作

圖四、PullToRefreshBase類的繼承關系

關于PullToRefreshBase類及其派和類,有幾點需要說明:

  • 對于ListView,ScrollView,WebView這三種情況,他們是否滑動到最頂部或是最底部的實作是不一樣的,是以,在PullToRefreshBase類中需要調用兩個抽象方法來判斷目前的位置是否在頂部或底部,而其派生類必須要實作這兩個方法。比如對于ListView,它滑動到最頂部的條件就是第一個child完全可見并且first postion是0。這兩個抽象方法是:
/**
     * 判斷重新整理的View是否滑動到頂部
     * 
     * @return true表示已經滑動到頂部,否則false
     */
    protected abstract boolean isReadyForPullDown();
    
    /**
     * 判斷重新整理的View是否滑動到底
     * 
     * @return true表示已經滑動到底部,否則false
     */
    protected abstract boolean isReadyForPullUp();
           
  • 建立可下拉重新整理的View(也就是content view)的抽象方法是
/**
     * 建立可以重新整理的View
     * 
     * @param context context
     * @param attrs 屬性
     * @return View
     */
    protected abstract T createRefreshableView(Context context, AttributeSet attrs);
           

4、LoadingLayout LoadingLayout是重新整理Layout的一個抽象,它是一個抽象基類。Header和Footer都擴充于這個類。這類抽象類,提供了兩個抽象方法:

  • getContentSize
這個方法傳回目前這個重新整理Layout的大小,通常傳回的是布局的高度,為了以後可以擴充為水準拉動,是以方法名字沒有取成getLayoutHeight()之類的,這個傳回值,将會作為松手後是否可以重新整理的臨界值,如果下拉的偏移值大于這個值,就認為可以重新整理,否則不重新整理,這個方法必須由派生類來實作。
  • setState
這個方法用來設定目前重新整理Layout的狀态,PullToRefreshBase類會調用這個方法,當進入下拉,松手等動作時,都會調用這個方法,派生類裡面隻需要根據這些狀态實作不同的界面顯示,如下拉狀态時,就顯示出箭頭,重新整理狀态時,就顯示loading的圖示。 可能的狀态值有: RESET, PULL_TO_REFRESH, RELEASE_TO_REFRESH, REFRESHING, NO_MORE_DATA

LoadingLayout及其派生類的繼承關系如下圖所示:

Android 下拉重新整理架構實作

圖五、LoadingLayout及其派生類的類圖

我們可以随意地制定自己的Header和Footer,我們也可以實作如圖一和圖二中顯示的各種下拉重新整理案例中的Header和Footer,隻要重寫上述兩個方法getContentSize()和setState()就行了。HeaderLoadingLayout,它預設是顯示箭頭式樣的布局,而RotateLoadingLayout則是顯示一個旋轉圖示的式樣。

5、事件處理 我們必須重寫PullToRefreshBase類的兩個事件相關的方法 onInterceptTouchEvent()和onTouchEvent()方法。由于ListView,ScrollView,WebView它們是放到PullToRefreshBase内部的,所在事件先是傳遞到PullToRefreshBase#onInterceptTouchEvent()方法中,是以我們應該在這個方法中去處理ACTION_MOVE事件,判斷如果目前ListView,ScrollView,WebView是否在最頂部或最底部,如果是,則開始截斷事件,一旦事件被截斷,後續的事件就會傳遞到PullToRefreshBase#onInterceptTouchEvent()方法中,我們再在ACTION_MOVE事件中去移動整個布局,進而實作下拉或上拉動作。

6、滾動布局(scrollTo) 如圖三的布局結構可知,預設情況下Header和Footer是放置在Content View的最上面和最下面,通過設定padding來讓他跑到螢幕外面去了,如果我們将整個布局向下滾動(scrollTo)一定距離,那麼Header就會被顯示出來,基于這種情況,是以在我的實作中,最終我是調用 scrollTo來實作下拉動作的。

總的說來,實作的重要的點就這些,具體的一些細節在實作在會碰到很多,可以參考代碼。

4. 如何使用

使用下拉重新整理的代碼如下

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        mPullListView = new PullToRefreshListView(this);
        setContentView(mPullListView);
        
        // 上拉加載不可用
        mPullListView.setPullLoadEnabled(false);
        // 滾動到底自動加載可用
        mPullListView.setScrollLoadEnabled(true);
        
        mCurIndex = mLoadDataCount;
        mListItems = new LinkedList<String>();
        mListItems.addAll(Arrays.asList(mStrings).subList(0, mCurIndex));
        mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListItems);
        
        // 得到實際的ListView
        mListView = mPullListView.getRefreshableView();
        // 綁定資料
        mListView.setAdapter(mAdapter);       
        // 設定下拉重新整理的listener
        mPullListView.setOnRefreshListener(new OnRefreshListener<ListView>() {
            @Override
            public void onPullDownToRefresh(PullToRefreshBase<ListView> refreshView) {
                mIsStart = true;
                new GetDataTask().execute();
            }

            @Override
            public void onPullUpToRefresh(PullToRefreshBase<ListView> refreshView) {
                mIsStart = false;
                new GetDataTask().execute();
            }
        });
        setLastUpdateTime();
        
        // 自動重新整理
        mPullListView.doPullRefreshing(true, 500);
    }
           

這是初始化一個下拉重新整理的布局,并且調用setContentView來設定到Activity中。 在下拉重新整理完成後,我們可以調用 onPullDownRefreshComplete()和onPullUpRefreshComplete()方法來停止重新整理和加載

5. 運作效果

這裡列出了demo的運作效果圖。

Android 下拉重新整理架構實作

圖六、ListView下拉重新整理,注意Header和Footer的樣式

Android 下拉重新整理架構實作

圖七、WebView和ScrollView的下拉重新整理效果圖

6. 源碼下載下傳

實作這個下拉重新整理的架構,并不是我的原創,我也是參考了很多開源的,把我認為比較好的東西借鑒過來,進而形成我的東西,我主要是參考了下面這個demo: https://github.com/chrisbanes/Android-PullToRefresh 這個demo寫得不錯,不過他這個太複雜了,我們都知道,一旦複雜了,萬一我們要添加一些需要,自然也要費勁一些,我其實就是把他的簡化再簡化,以滿足我們自己的需要。

源碼下載下傳請猛點我

轉載請說明出處 http://blog.csdn.net/leehong2005/article/details/12567757  謝謝!!!

7. Bug修複

已知bug修複情況如下,發現了代碼bug的看官也可以給我回報,謝謝~~~

1,對于ListView的下拉重新整理,當啟用滾動到底自動加載時,如果footer由隐藏變為顯示時,出現顯示異常的情況 這個問題已經修複了,修正的代碼如下:

  • PullToRefreshListView#setScrollLoadEnabled方法,修正後的代碼如下:
@Override
    public void setScrollLoadEnabled(boolean scrollLoadEnabled) {
        if (isScrollLoadEnabled() == scrollLoadEnabled) {
            return;
        }
        
        super.setScrollLoadEnabled(scrollLoadEnabled);
        
        if (scrollLoadEnabled) {
            // 設定Footer
            if (null == mLoadMoreFooterLayout) {
                mLoadMoreFooterLayout = new FooterLoadingLayout(getContext());
                mListView.addFooterView(mLoadMoreFooterLayout, null, false);
            }
            
            mLoadMoreFooterLayout.show(true);
        } else {
            if (null != mLoadMoreFooterLayout) {
                mLoadMoreFooterLayout.show(false);
            }
        }
    }
           
  • LoadingLayout#show方法,修正後的代碼如下:
/**
     * 顯示或隐藏這個布局
     * 
     * @param show flag
     */
    public void show(boolean show) {
        // If is showing, do nothing.
        if (show == (View.VISIBLE == getVisibility())) {
            return;
        }
        
        ViewGroup.LayoutParams params = mContainer.getLayoutParams();
        if (null != params) {
            if (show) {
                params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
            } else {
                params.height = 0;
            }
            
            requestLayout();
            setVisibility(show ? View.VISIBLE : View.INVISIBLE);
        }
    }
           

在更改LayoutParameter後,調用requestLayout()方法。

  • 圖檔旋轉相容2.x系統

我之前想的是這個隻需要相容3.x以上的系統,但發現有很多網友在使用過程中遇到過相容性問題,這次抽空将這個相容性一并實作了。        onPull的修改如下:

@Override
    public void onPull(float scale) {
        if (null == mRotationHelper) {
            mRotationHelper = new ImageViewRotationHelper(mArrowImageView);
        }
        
        float angle = scale * 180f; // SUPPRESS CHECKSTYLE
        mRotationHelper.setRotation(angle);
    }
           

ImageViewRotationHelper主要的作用就是實作了ImageView的旋轉功能,内部作了版本的區分,實作代碼如下:

/**
     * The image view rotation helper
     * 
     * @author lihong06
     * @since 2014-5-2
     */
    static class ImageViewRotationHelper {
        /** The imageview */
        private final ImageView mImageView;
        /** The matrix */
        private Matrix mMatrix;
        /** Pivot X */
        private float mRotationPivotX;
        /** Pivot Y */
        private float mRotationPivotY;
        
        /**
         * The constructor method.
         * 
         * @param imageView the image view
         */
        public ImageViewRotationHelper(ImageView imageView) {
            mImageView = imageView;
        }
        
        /**
         * Sets the degrees that the view is rotated around the pivot point. Increasing values
         * result in clockwise rotation.
         *
         * @param rotation The degrees of rotation.
         *
         * @see #getRotation()
         * @see #getPivotX()
         * @see #getPivotY()
         * @see #setRotationX(float)
         * @see #setRotationY(float)
         *
         * @attr ref android.R.styleable#View_rotation
         */
        public void setRotation(float rotation) {
            if (APIUtils.hasHoneycomb()) {
                mImageView.setRotation(rotation);
            } else {
                if (null == mMatrix) {
                    mMatrix = new Matrix();
                    
                    // 計算旋轉的中心點
                    Drawable imageDrawable = mImageView.getDrawable();
                    if (null != imageDrawable) {
                        mRotationPivotX = Math.round(imageDrawable.getIntrinsicWidth() / 2f);
                        mRotationPivotY = Math.round(imageDrawable.getIntrinsicHeight() / 2f);
                    }
                }
                
                mMatrix.setRotate(rotation, mRotationPivotX, mRotationPivotY);
                mImageView.setImageMatrix(mMatrix);
            }
        }
    }
           

最核心的就是,如果在2.x的版本上,旋轉ImageView使用Matrix。

  • PullToRefreshBase構造方法相容2.x

在三個參數的構造方法聲明如下标注:     @SuppressLint("NewApi")

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)

大家如果還有什麼問題,歡迎留言~~~