Android 下拉刷新框架實現

前段時間項目中用到了下拉刷新功能,以前在網上也找到過相似的demo,但這些demo的質量良莠不齊,用戶體驗也很差,接口設計也不行。最張沒辦法,終於忍不了了,本身就寫了一個下拉刷新的框架,這個框架是一個通用的框架,效果和設計感受都還不錯,如今分享給各位看官。html

1. 關於下拉刷新

下拉刷新這種用戶交互最先由twitter創始人洛倫•布里切特(Loren Brichter)發明,有理論認爲,下拉刷新是一種適用於按照重新到舊的時間順序排列feeds的應用,在這種應用場景中看完舊的內容時,用戶會很天然地下拉查找更新的內容,所以下拉刷新就顯得很是合理。你們能夠參考這篇文章:有趣的下拉刷新,下面我貼出一個有趣的下拉刷新的案例。java

圖1、有趣的下拉刷新案例(一)android

圖1、有趣的下拉刷新案例(二)
git

2. 實現原理

上面這些例子,外觀作得再好看,他的本質上都同樣,那就是一個下拉刷新控件一般由如下幾部分組成:github

【1】Header框架

Header一般有下拉箭頭,文字,進度條等元素,根據下拉的距離來改變它的狀態,從而顯示不一樣的樣式ide

【2】Content佈局

這部分是內容區域,網上有不少例子都是直接在ListView裏面添加Header,但這就有侷限性,由於好多狀況下並不必定是用ListView來顯示數據。咱們把要顯示內容的View放置在咱們的一個容器中,若是你想實現一個用ListView顯示數據的下拉刷新,你須要建立一個ListView旋轉到個人容器中。咱們處理這個容器的事件(down, move, up),若是向下拉,則把整個佈局向下滑動,從而把header顯示出來。post

【3】Footerui

Footer能夠用來顯示向上拉的箭頭,自動加載更多的進度條等。

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

圖三,下拉刷新的佈局結構

關於上圖,須要說明幾點:

一、這個佈局擴展於LinearLayout,垂直排列

二、從上到下的順序是:Header, Content, Footer

三、Content填充滿父控件,經過設置top, bottom的padding來使Header和Footer不可見,也就是讓它超出屏幕外

四、下拉時,調用scrollTo方法來將整個佈局向下滑動,從而把Header顯示出來,上拉正好與下拉相反。

五、派生類須要實現的是:將Content View填充到父容器中,好比,若是你要使用的話,那麼你須要把ListView, ScrollView, WebView等添加到容器中。

六、上圖中的紅色區域就是屏的大小(嚴格來講,這裏說屏幕大小並不許確,應該說成內容區域更加準確)

3. 具體實現

明白了實現原理與過程,咱們嘗試來具體實現,首先,爲了之後更好地擴展,設計更加合理,咱們把下拉刷新的功能抽象成一個接口:

一、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嗎?


二、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會根據狀態去顯示相應的界面式樣。

三、PullToRefreshBase<T extends View>繼承關係

這裏我實現了三個下拉刷新的派生類,分別是ListView、ScrollView、WebView三個,它們的繼承關係以下:

圖4、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);

四、LoadingLayout

LoadingLayout是刷新Layout的一個抽象,它是一個抽象基類。Header和Footer都擴展於這個類。這類抽象類,提供了兩個抽象方法:

  • getContentSize

這個方法返回當前這個刷新Layout的大小,一般返回的是佈局的高度,爲了之後能夠擴展爲水平拉動,因此方法名字沒有取成getLayoutHeight()之類的,這個返回值,將會做爲鬆手後是否能夠刷新的臨界值,若是下拉的偏移值大於這個值,就認爲能夠刷新,不然不刷新,這個方法必須由派生類來實現。
  • setState

這個方法用來設置當前刷新Layout的狀態,PullToRefreshBase類會調用這個方法,當進入下拉,鬆手等動做時,都會調用這個方法,派生類裏面只須要根據這些狀態實現不一樣的界面顯示,以下拉狀態時,就顯示出箭頭,刷新狀態時,就顯示loading的圖標。

可能的狀態值有:RESET, PULL_TO_REFRESH, RELEASE_TO_REFRESH, REFRESHING, NO_MORE_DATA


LoadingLayout及其派生類的繼承關係以下圖所示:


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

咱們能夠隨意地制定本身的Header和Footer,咱們也能夠實現如圖一和圖二中顯示的各類下拉刷新案例中的Header和Footer,只要重寫上述兩個方法getContentSize()和setState()就好了。HeaderLoadingLayout,它默認是顯示箭頭式樣的佈局,而RotateLoadingLayout則是顯示一個旋轉圖標的式樣。

五、事件處理

咱們必須重寫PullToRefreshBase類的兩個事件相關的方法onInterceptTouchEvent()和onTouchEvent()方法。因爲ListView,ScrollView,WebView它們是放到PullToRefreshBase內部的,所在事件先是傳遞到PullToRefreshBase#onInterceptTouchEvent()方法中,因此咱們應該在這個方法中去處理ACTION_MOVE事件,判斷若是當前ListView,ScrollView,WebView是否在最頂部或最底部,若是是,則開始截斷事件,一旦事件被截斷,後續的事件就會傳遞到PullToRefreshBase#onInterceptTouchEvent()方法中,咱們再在ACTION_MOVE事件中去移動整個佈局,從而實現下拉或上拉動做。

六、滾動佈局(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的運行效果圖。


圖6、ListView下拉刷新,注意Header和Footer的樣式

圖7、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)

相關文章
相關標籤/搜索