1. 程式人生 > >Android自定義控制元件實戰——實現仿IOS下拉重新整理上拉載入 PullToRefreshLayout

Android自定義控制元件實戰——實現仿IOS下拉重新整理上拉載入 PullToRefreshLayout

         下拉重新整理控制元件,網上有很多版本,有自定義Layout佈局的,也有封裝控制元件的,各種實現方式的都有。但是很少有人告訴你具體如何實現的,今天我們就來一步步實現自己封裝的 PullToRefreshLayout 完美的解決下拉重新整理,上拉載入問題。

         首先來分析一下原理,為什麼一下拉就可以拉出來一個佈局,請看下圖,從圖中可以看到整個螢幕來說有可見部分,有隱藏部分,當我們手指在螢幕上下拉的時候滑動距離到一定程度了就會拉出 下拉頭佈局,這樣就達到了下拉效果。那麼具體程式碼如何實現待我慢慢像大家解析。

         1、想要實現  PullToRefreshLayout 下拉重新整理控制元件那麼我們就必須要有個容器,也就是如上圖的容器,知道了需要什麼那麼我們就開始自定義一個容器。

          這裡如果不會自定義控制元件的同學可以參考部落格 http://blog.csdn.net/cscfas/article/details/51330505

/**
 * Created by ZQY on 2016/5/17.
 * <p/>
 * 這個是上拉載入和下拉重新整理的 View
 * <p/>
 * 注:這裡的 android:orientation="vertical" 只能為這個值
 */
public class PullToRefreshLayout extends LinearLayout {

 

public PullToRefreshLayout(Context context) {
    super(context);
    initAnim();
}

public PullToRefreshLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    initAnim();
}

public PullToRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    initAnim();
}


}

         2、有了容器,接下來就拉實現下拉頭,上拉腳。LinearLayout 我們都用過線性佈局嘛,在這裡要注意 android:orientation=“vertical” 只能是垂直佈局。這裡重寫了該控制元件,目的是在程式碼中動態添加布局到控制元件中,實現組合控制元件,就是PullToRefreshLayout ,這裡呼叫了LinearLayout 的addView()  方法將佈局新增到PullToRefreshLayout中。

             (1)、新增頭部佈局,這裡也就是下拉頭

   private void addHeaderView() {

        mHeaderView = mInflater.inflate(R.layout.refresh_header, this, false);

        mHeaderImageView = (ImageView) mHeaderView
                .findViewById(R.id.pull_to_refresh_image);
        mHeaderTextView = (TextView) mHeaderView
                .findViewById(R.id.pull_to_refresh_text);
        mHeaderUpdateTextView = (TextView) mHeaderView
                .findViewById(R.id.pull_to_refresh_updated_at);

        mHeaderUpdateTextView.setText(DataUtil.getRefreshCompleteTime());

        mHeaderProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);

        measureView(mHeaderView);

        mHeaderViewHeight = mHeaderView.getMeasuredHeight();
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mHeaderViewHeight);

        //設定 topMargin 的值為負的 header View 高度,即將其隱藏在最上方
        params.topMargin = -(mHeaderViewHeight);

        //新增頭部到佈局
        addView(mHeaderView, params);
    }

             (2)、新增腳部佈局,這裡也就是 上拉腳
  
private void addFooterView() {
        mFooterView = mInflater.inflate(R.layout.refresh_footer, this, false);
        mFooterImageView = (ImageView) mFooterView
                .findViewById(R.id.pull_to_load_image);
        mFooterTextView = (TextView) mFooterView
                .findViewById(R.id.pull_to_load_text);
        mFooterProgressBar = (ProgressBar) mFooterView
                .findViewById(R.id.pull_to_load_progress);

        // 底部佈局
        measureView(mFooterView);
        mFooterViewHeight = mFooterView.getMeasuredHeight();
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                mFooterViewHeight);

        /**
         * 	int top = getHeight();
         params.topMargin=getHeight();//在這裡getHeight()==0,但在onInterceptTouchEvent()方法裡getHeight()已經有值了,不再是0;

         getHeight()什麼時候會賦值,稍候再研究一下
         由於是線性佈局可以直接新增,只要AdapterView的高度是MATCH_PARENT,那麼footer view就會被新增到最後,並隱藏
         */
        addView(mFooterView, params);

    }

             看完以上程式碼你肯定會想就這麼簡單嘛!當然不是,細心的同學會發現兩個函式都有呼叫 measureView()函式,它是幹嘛的呢!下面就來看下這個函式,這個函式看起來程式碼和註釋很多,這裡的功能無非就是計運算元控制元件在父控制元件中的大小。
 private void  measureView(View child) {

        /**
         * child.getLayoutParams();
         *
         * 返回  該檢視的佈局引數
         *
         * 此檢視的父檢視指定如何安排它的供應引數
         *

         */
        ViewGroup.LayoutParams p = child.getLayoutParams();


        if (p == null) {
            /**
             * 用指定的 寬度和高度 建立一組新的佈局引數
             *
             * @param width 寬度,或者 {@link #WRAP_CONTENT},
             *        {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in
             *        API Level 8),或一個固定大小的畫素
             * @param height  高度,或者 {@link #WRAP_CONTENT},
             *        {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in
             *        API Level 8), 或一個固定大小的畫素
             */
            p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }

        /**
         * 是否measureChildren困難的部分:搞清楚MeasureSpec傳遞給特定的子控制元件。這種方法計算出正確的MeasureSpec一個子檢視中的一維(高度或寬度)。
         * 目標是資訊從我們MeasureSpec與子控制元件的的LayoutParams結合,以獲得最佳的可能結果。例如,如果這個觀點知道它的大小(因為它MeasureSpec有整整模式),
         * 子控制元件在其的LayoutParams已經表示,它想成為的尺寸與父控制元件一樣,父控制元件應讓子控制元件佈置給精確的尺寸。

         * @param spec 該檢視的要求
         * @param padding  該檢視為當前維的填充和利潤(如果適用)
         *
         * @param childDimension  希望為子控制元件設定的尺寸
         * @return  MeasureSpec   一個MeasureSpec整數為孩子
         *
         */
        int childWidthSpec=ViewGroup.getChildMeasureSpec(0,0+0,p.width);


        int lpHeight = p.height;
        int childHeightSpec;
        if (lpHeight > 0) {

            /**
             *
             建立基於所提供的大小和模式的量度規範。該模式必須是下列之一:
             UNSPECIFIED
             EXACTLY
             AT_MOST

             * @param size 該措施說明書的大小
             * @param mode 該措施規範的模式
             * @return 基於規模和模式的措施規範
             */
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
                    MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0,
                    MeasureSpec.UNSPECIFIED);
        }

        /**
         * 這就是所謂的大一個檢視應該如何。父控制元件 約束資訊的寬度和高度引數。

         一個檢視的實際測量工作是在onMeasure(int,int),稱為該方法。因此,只有onMeasure(int,int)可以而且必須由子類重寫。

         @param widthMeasureSpec 橫向空間的需求新增到的父控制元件大小
         @param heightMeasureSpec 垂直間距需求新增到的父控制元件大小
         */
        child.measure(childWidthSpec, childHeightSpec);

    }

         3、知道了 下拉頭,上拉腳 怎麼實現了,接下來就看在哪裡加入到 PullToRefreshLayout控制元件中,又是如何實現動畫的。請看下面程式碼。

             (1)、這裡動畫實現的是重新整理箭頭的方向旋轉,最後一行 addHeaderView() 實現了頭部的新增。

  /**
     * 初始化動畫
     */
    private void initAnim() {

        //載入所有的動畫,我們需要的程式碼,而不是通過 XML
        mFlipAnimation = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF,
                0.5f, Animation.RELATIVE_TO_SELF, 0.5f);

        //設定動畫 均速
        mFlipAnimation.setInterpolator(new LinearInterpolator());

        /**
         * 動畫應該持續多久,持續時間不能為負
         *
         @param durationMillis
          *  @throws java.lang.IllegalArgumentException  如果 durationMillis < 0
         *  @attr 參考 R.styleable #Animation_duration
         */
        mFlipAnimation.setDuration(250);


        /**
         * 如果 fillafter 是 true ,這個動畫進行改造將堅持當它完成。
         * 預設為 false ,如果不設定。
         *
         *請注意,這適用於個別動畫,當使用 {@link android.view.animation.AnimationSet AnimationSet} 鏈動畫
         *
         * @param fillAfter  如果動畫結束後,動畫應該應用它的轉換
         * @attr ref android.R.styleable#Animation_fillAfter
         *
         * @see #setFillEnabled(boolean)
         */
        mFlipAnimation.setFillAfter(true);


        /**
         *建構函式使用時建立一個rotateanimation 物件
         *
         *
         * @param fromDegrees 在動畫開始時應用旋轉偏移。
         *
         * @param toDegrees   在動畫結束時應用旋轉偏移。
         *
         * @param pivotXType 指定如何pivotxvalue應解釋。什麼之中的一個
         *        Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
         *        Animation.RELATIVE_TO_PARENT.
         *
         * @param pivotXValue  X座標的物件被旋轉的點,指定一個絕對數量,0是左邊緣。這個值可以是絕對數如果pivotxtype是絕對的,或一個百分比(1是100%)否則。
         *
         *
         * @param pivotYType 指定如何pivotyvalue應解釋。什麼之中的一個
         *        Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
         *        Animation.RELATIVE_TO_PARENT.
         *
         * @param pivotYValue  X座標的物件被旋轉的點,指定一個絕對數量,0是左邊緣。這個值可以是絕對數如果pivotxtype是絕對的,或一個百分比(1是100%)否則。
         */
        mReverseFlipAnimation = new RotateAnimation(-180, 0,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);


        //設定此動畫的加速曲線。預設為線性插值。 這裡是勻速
        mReverseFlipAnimation.setInterpolator(new LinearInterpolator());

        mReverseFlipAnimation.setDuration(250);
        mReverseFlipAnimation.setFillAfter(true);

        mInflater = LayoutInflater.from(getContext());

        // header view 在此新增,保證是第一個新增到linearlayout的最上端
        addHeaderView();
    }

             (2)、知道了頭部如何加入PullToRefreshLayout中,那麼底部是如何新增的呢!其實底部的加入是有技巧的,接下來請看程式碼。onFihishInflate()  看到@Override 你就知道這個函式是 LinearLayout 提供,那麼它有何作用呢,它的作用就是在所有的XML和頭部佈局都添加了的情況下加入 腳部佈局。
    /**
     *
     完成 填充 XML格式的檢視。這就是所謂的 UI填充 的最後階段,所有子檢視已被新增之後。

     即使子類覆蓋onFinishInflate,他們應始終確保呼叫超級方法,使我們得到呼叫。 既必須呼叫  super.onFinishInflate();
     */
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        // footer view 在此新增保證新增到linearlayout中的最後

        addFooterView();

        initContentAdapterView();
    }

             (3)、在上面的程式碼中你會看到 initContentAdapterView()  這個函式,你會想它又是什麼鬼,它有什麼作用呢?請看程式碼。

如果你有了解過,我的上一篇部落格:http://blog.csdn.net/cscfas/article/details/51330505 ;那麼你就知道在自定義控制元件中,如果XML佈局中引入了控制元件,會載入該自定義控制元件的第二個建構函式,那麼addHeaderView() 會被載入到佈局中,PullToRefreshLayout 在xml 中加入的佈局也會被新增到控制元件中。該佈局可以包裹 ListView 和 GridView 及 ScrollView 控制元件。

   /**
     *
     * 初始化 adapterview像ListView,GridView等;或init ScrollView
     */
    private void initContentAdapterView(){

        int count=getChildCount();

        if (count<3)
            throw new IllegalArgumentException(
                    "this layout must contain 3 child views,and AdapterView or ScrollView must in the second position!");


        View  view=null;

        for (int i=0;i<count-1;++i){
            view=getChildAt(i);

            if (view instanceof AdapterView<?>){

                System.out.println("the type is AdapterView");

                mAdapterView=(AdapterView<?>)view;
            }

            if (view instanceof  ScrollView){

                System.out.println("thie type is ScrollView");

                mScrollView= (ScrollView) view;
            }
        }


        if (mAdapterView==null&&mScrollView==null){

            throw new IllegalArgumentException(
                    "must contain a AdapterView or ScrollView in this layout!");
        }
    }

         4、接下來看下專案中用到的常量和變數註釋,這對閱讀後續程式碼有幫助。

   /**
     * 下拉重新整理
     */
    private static final int PULL_TO_REFRESH = 2;

    /**
     * 釋放重新整理
     */
    private static final int RELEASE_TO_REFRESH = 3;

    /**
     * 重新整理
     */
    private static final int REFRESHING = 4;


    /**
     * 上拉載入
     */
    private static final int PULL_UP_STATE = 10;
    /**
     * 下拉重新整理
     */
    private static final int PULL_DOWN_STATE = 11;


    /**
     * 最後Y軸距離
     */
    private int mLastMotionY;

    /**
     * 鎖定
     */
    private boolean mLock;


         5、瞭解了佈局如何實現,接下來就到了手勢如何實現,也就是我們下拉為什麼可以拉出 下拉頭,這裡涉及到手勢相關的概念,如果不瞭解手可以參考部落格:http://blog.csdn.net/cscfas/article/details/51372342

               這裡就不講事件是如何攔截,如何分發的了,我們重點來看如下程式碼,這裡在 ACTION_DOWN時並沒有攔截事件只是記錄下了 Y軸座標,為什麼呢?因為PullToRefreshLayout 是屬於 ViewGroup 容器型的控制元件,如果ACTION_DOWN 直接被攔截了那麼 ListVeiw 和 GridView 中的 item點選事件及 ScrollView中點選事件和長按事件將無法觸發。

    /**
     * 事件攔截
     * @param ev
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int y = (int) ev.getRawY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:   //手指按下時記錄 Y軸座標

                // 首先攔截down事件,記錄y座標
                mLastMotionY = y;

                break;
            case MotionEvent.ACTION_MOVE:  //滑動時 拿到移動距離 判斷是否攔截手勢

                // deltaY > 0 是向下運動,< 0是向上運動
                int deltaY = y - mLastMotionY;
                if (isRefreshViewScroll(deltaY)) {

//				System.out.println("正在移動:返回true");
                    return true;
                }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }

        return false;
    }

               細心的同學會發現在 ACTION_MOVE 中有呼叫 isRefreshViewScroll() 函式,那麼它又有什麼功能呢!仔細看程式碼會發現它返回了一個 boolean 型別的值,是它控制這事件是否攔截,看到這裡你是不是覺得它至關重要,那麼就來分析一下它的結構吧!

                mAdapterView 這個控制元件從何而來,有認真看過上面程式碼你就應該知道了。那麼它是何方神聖呢?它就是 介面卡填充控制元件後得到的結果,AdapterView 是介面卡和控制元件的組合,這裡主要是拿到AdapterView中的子控制元件,也就是ListView或 GridView中的Item,通過獲取子控制元件的狀態來動態設定 是否要攔截手勢,以及設定 mPullState 狀態。

                mScrollView 控制元件也是同理,拿到子控制元件的狀態來判斷是否要攔截事件。具體程式碼都有註釋請看程式碼,這裡就不詳解了。

  /**
     * 是否應該到了父View,即PullToRefreshView滑動
     *
     * @param deltaY
     *            , deltaY > 0 是向下運動,< 0是向上運動
     * @return
     */
    private boolean isRefreshViewScroll(int deltaY) {

        // 當頭部狀態是 重新整理 或 底部狀態是重新整理時 返回 false 不攔截
        if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
            return false;
        }


        //對於ListView和GridView
        if (mAdapterView != null) {
            // 子view(ListView or GridView)滑動到最頂端
            if (deltaY > 0) {

                View child = mAdapterView.getChildAt(0);
                if (child == null) {

                    //設定狀態為下拉重新整理
                    mPullState = PULL_DOWN_STATE;

                    //設定狀態為攔截
                    return true;
                }

                // 適配中 第一個控制元件高度為 0 且 第一個控制元件可見
                if (mAdapterView.getFirstVisiblePosition() == 0
                        && child.getTop() == 0) {

                    //設定狀態為下拉重新整理
                    mPullState = PULL_DOWN_STATE;

                    return true;
                }


                int top = child.getTop();
                int padding = mAdapterView.getPaddingTop();
                if (mAdapterView.getFirstVisiblePosition() == 0
                        && Math.abs(top - padding) <= 8) {//這裡之前用3可以判斷,但現在不行,還沒找到原因
                    mPullState = PULL_DOWN_STATE;
                    return true;
                }

            } else if (deltaY < 0) {  //如果移動的距離為 負值

                //獲取適配中最後一個控制元件
                View lastChild = mAdapterView.getChildAt(mAdapterView
                        .getChildCount() - 1);

                if (lastChild == null) {
                    mPullState = PULL_UP_STATE;
                    // 如果mAdapterView中沒有資料,不攔截
                    return true;
                }
                // 最後一個子view的Bottom小於父View的高度說明mAdapterView的資料沒有填滿父view,
                // 等於父View的高度說明mAdapterView已經滑動到最後
                if (lastChild.getBottom() <= getHeight()
                        && mAdapterView.getLastVisiblePosition() == mAdapterView
                        .getCount() - 1) {
                    mPullState = PULL_UP_STATE;
                    return true;
                }
            }
        }

        // 對於ScrollView
        if (mScrollView != null) {
            // 子scroll view滑動到最頂端
            View child = mScrollView.getChildAt(0);

            //當移動距離為 正值  且滾動條沒有滾動
            if (deltaY > 0 && mScrollView.getScrollY() == 0) {
                mPullState = PULL_DOWN_STATE;  //設定狀態為下拉重新整理
                return true;
            } else if (deltaY < 0
                    && child.getMeasuredHeight() <= getHeight()
                    + mScrollView.getScrollY()) {
                mPullState = PULL_UP_STATE;   //設定為上拉載入

                return true;
            }
        }
        return false;
    }



         6、接下來就要見證奇蹟了,就是具體如何實現 下拉重新整理上拉載入更多效果的業務了,還記得上面我們有講到手勢攔截吧!如果你瞭解手勢就知道被攔截後會執行什麼函式,那就是 onTouchEvent() 函數了。

              (1)、首先看下 ACTION_MOVE 這裡我們來計算使用者手指在螢幕上的滑動距離,還記得在 onInterceptTounchEvent()中已經對 mPullState 狀態做過改變,這裡開始就通過判斷當前狀態是下拉還是上拉來處理 HeaderView 和 FootView的顯示及動畫效果。

    /*
	 * 如果在onInterceptTouchEvent()方法中沒有攔截(即onInterceptTouchEvent()方法中 return false)
	 *
	 * 則由PullToRefreshView 的子View來處理;否則由下面的方法來處理(即由PullToRefreshView自己來處理)
	 */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mLock) {   //當處於鎖定狀態時
            return true;
        }

        //拿到Y軸座標
        int y = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:    //手指按下時觸發  ACTION_DOWN
                // onInterceptTouchEvent已經記錄
                // mLastMotionY = y;
                break;
            case MotionEvent.ACTION_MOVE:  //手指在螢幕上滑動時觸發  ACTION_MOVE

                //拿到使用者滑動的距離
                int deltaY = y - mLastMotionY;

                if (mPullState == PULL_DOWN_STATE) {   //如果當前狀態處於下拉重新整理 PULL_DOWN_STATE  那麼執行 headerPrepareToRefresh() 函式實現重新整理效果
                    // PullToRefreshView執行下拉
                    Log.i(TAG, " pull down!parent view move!");
                    headerPrepareToRefresh(deltaY);
                    // setHeaderPadding(-mHeaderViewHeight);
                } else if (mPullState == PULL_UP_STATE) { //如果當前狀態處於上拉載入 PULL_UP_STATE

                    if (pullUpLoad) {   //判斷使用者是否啟用上拉載入

                        // PullToRefreshView執行上拉
                        Log.i(TAG, "pull up!parent view move!");

                        footerPrepareToRefresh(deltaY);
                    }
                }
                mLastMotionY = y;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: //當事件被取消時

                //獲取當前header view 的topMargin 值
                int topMargin = getHeaderTopMargin();

                if (mPullState == PULL_DOWN_STATE) {  //如果當前狀態是下拉重新整理

                    if (topMargin >= 0) {
                        // 開始重新整理
                        headerRefreshing();
                    } else {
                        // 還沒有執行重新整理,重新隱藏
                        setHeaderTopMargin(-mHeaderViewHeight);
                    }
                } else if (mPullState == PULL_UP_STATE) {  //如果當前狀態處於上拉載入

                    if (pullUpLoad) {

                        if (Math.abs(topMargin) >= mHeaderViewHeight
                                + mFooterViewHeight) {
                            // 開始執行footer 重新整理
                            footerRefreshing();
                        } else {
                            // 還沒有執行重新整理,重新隱藏
                            setHeaderTopMargin(-mHeaderViewHeight);
                        }
                    }

                }
                break;
        }
        return super.onTouchEvent(event);
    }

              (2)、處理下拉或上拉布局被拉出效果,接下來看 headPrepareToRefresh() 和 footerPrepareToRefresh() 這兩個函式實現了上拉及下拉效果 ,這裡要注意 mHeaderState、mFooterState 的狀態改變,它決定這是否釋放重新整理

  /**
     * header 準備重新整理,手指移動過程,還沒有釋放
     *
     * @param deltaY
     *            ,手指滑動的距離
     */
    private void headerPrepareToRefresh(int deltaY) {
        int newTopMargin = changingHeaderViewTopMargin(deltaY);

        // 當header view的topMargin>=0時,說明已經完全顯示出來了,修改header view 的提示狀態
        if (newTopMargin >= 0 && mHeaderState != RELEASE_TO_REFRESH) {

            mHeaderTextView.setText(R.string.pull_to_refresh_release_label);
            mHeaderUpdateTextView.setVisibility(View.VISIBLE);
            mHeaderImageView.clearAnimation();
            mHeaderImageView.startAnimation(mFlipAnimation);

            //改變狀態為釋放重新整理
            mHeaderState = RELEASE_TO_REFRESH;

        } else if (newTopMargin < 0 && newTopMargin > -mHeaderViewHeight) {// 拖動時沒有釋放
            mHeaderImageView.clearAnimation();
            mHeaderImageView.startAnimation(mFlipAnimation);
            mHeaderTextView.setText(R.string.pull_to_refresh_pull_label);
            mHeaderState = PULL_TO_REFRESH;
        }
    }

 /**
     * footer 準備重新整理,手指移動過程,還沒有釋放 移動footer view高度同樣和移動header view
     * 高度是一樣,都是通過修改header view的topmargin的值來達到
     *
     * @param deltaY
     *            ,手指滑動的距離
     */
    private void footerPrepareToRefresh(int deltaY) {
        int newTopMargin = changingHeaderViewTopMargin(deltaY);
        // 如果header view topMargin 的絕對值大於或等於header + footer 的高度
        // 說明footer view 完全顯示出來了,修改footer view 的提示狀態
        if (Math.abs(newTopMargin) >= (mHeaderViewHeight + mFooterViewHeight)
                && mFooterState != RELEASE_TO_REFRESH) {
            mFooterTextView
                    .setText(R.string.pull_to_refresh_footer_release_label);
            mFooterImageView.clearAnimation();
            mFooterImageView.startAnimation(mFlipAnimation);
            mFooterState = RELEASE_TO_REFRESH;
        } else if (Math.abs(newTopMargin) < (mHeaderViewHeight + mFooterViewHeight)) {
            mFooterImageView.clearAnimation();
            mFooterImageView.startAnimation(mFlipAnimation);
            mFooterTextView.setText(R.string.pull_to_refresh_footer_pull_label);
            mFooterState = PULL_TO_REFRESH;
        }
    }

              (3)、仔細閱讀上面程式碼,會發現所有的判斷跟隨著這個 headerPrepareToRefresh() 函式的返回值決定,接下來看下這個函式。判斷當前 mPullState 狀態 及 拉動距離是否大於設定距離,動態返回 TopMargin 及拉出的距離
  /**
     * 修改Header view top margin的值
     *
     * @description
     * @param deltaY
     */
    private int changingHeaderViewTopMargin(int deltaY) {

        LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();

        float newTopMargin = params.topMargin + deltaY * 0.4f;

        //這裡對上拉做一下限制,因為當前上拉後然後不釋放手指直接下拉,會把下拉重新整理給觸發了
        //表示如果是在上拉後一段距離,然後直接下拉
        if(deltaY>0&&mPullState == PULL_UP_STATE&&Math.abs(params.topMargin) <= mHeaderViewHeight){
            return params.topMargin;
        }
        //同樣地,對下拉做一下限制,避免出現跟上拉操作時一樣的bug
        if(deltaY<0&&mPullState == PULL_DOWN_STATE&&Math.abs(params.topMargin)>=mHeaderViewHeight){
            return params.topMargin;
        }
        params.topMargin = (int) newTopMargin;
        mHeaderView.setLayoutParams(params);

        /**
         * 無效整個檢視。如果檢視是可見的,
         *
         *  {@link #onDraw(android.graphics.Canvas)} 將在某個時候被呼叫
         *
         *  這必須從UI執行緒呼叫。從非UI執行緒,致電致電
         *
         *  {@link #postInvalidate()}.
         */
        invalidate();
        return params.topMargin;
    }

              (4)、ACTION_UP、ACTION_CANCEL 處理釋放重新整理和取消執行重新整理,首先拿到 topMargin 既拉動的距離,通過判斷拉動距離和 mPullState 狀態來決定是釋放重新整理還是取消執行重新整理。

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: //當事件被取消時

                //獲取當前header view 的topMargin 值
                int topMargin = getHeaderTopMargin();

                if (mPullState == PULL_DOWN_STATE) {  //如果當前狀態是下拉重新整理

                    if (topMargin >= 0) {
                        // 開始重新整理
                        headerRefreshing();
                    } else {
                        // 還沒有執行重新整理,重新隱藏
                        setHeaderTopMargin(-mHeaderViewHeight);
                    }
                } else if (mPullState == PULL_UP_STATE) {  //如果當前狀態處於上拉載入

                    if (pullUpLoad) {

                        if (Math.abs(topMargin) >= mHeaderViewHeight
                                + mFooterViewHeight) {
                            // 開始執行footer 重新整理
                            footerRefreshing();
                        } else {
                            // 還沒有執行重新整理,重新隱藏
                            setHeaderTopMargin(-mHeaderViewHeight);
                        }
                    }

                }
                break;

              (5)、headerRefreshing() 、footerRefreshing() 釋放重新整理,這裡將 Runnable 新增到UI執行緒中,延遲1500 毫秒達到,下拉頭或上拉腳停頓效果,這裡主要回調監聽介面,該介面是呼叫 PullToRefreshLayout 控制元件的 Activity或FrangMent 中實現。

    /**
     *  下拉頭釋放重新整理
     *
     */
    private void headerRefreshing() {
        mHeaderState = REFRESHING;
        setHeaderTopMargin(0);
        mHeaderImageView.setVisibility(View.GONE);
        mHeaderImageView.clearAnimation();
        mHeaderImageView.setImageDrawable(null);
        mHeaderProgressBar.setVisibility(View.VISIBLE);
        mHeaderTextView.setText(R.string.pull_to_refresh_refreshing_label);
        if (mOnHeaderRefreshListener != null) {


            /**
             * 使Runnable被新增到訊息佇列,經過規定的時間之後執行。
             *
             * 執行將執行在使用者介面執行緒。既UI執行緒中
             */
            this.postDelayed(new Runnable() {

                @Override
                public void run() {
                    mOnHeaderRefreshListener.onHeaderRefresh(PullToRefreshView.this);
                }
            }, 1500);
        }
    }
    /**
     * 底部釋放重新整理
     */
    private void footerRefreshing() {
        mFooterState = REFRESHING;
        int top = mHeaderViewHeight + mFooterViewHeight;
        setHeaderTopMargin(-top);
        mFooterImageView.setVisibility(View.GONE);
        mFooterImageView.clearAnimation();
        mFooterImageView.setImageDrawable(null);
        mFooterProgressBar.setVisibility(View.VISIBLE);
        mFooterTextView
                .setText(R.string.pull_to_refresh_footer_refreshing_label);
        if (mOnFooterRefreshListener != null) {
            this.postDelayed(new Runnable() {

                @Override
                public void run() {
                    mOnFooterRefreshListener.onFooterRefresh(PullToRefreshView.this);
                }
            }, 1500);

        }
    }

              (5)、注意在重新整理失敗的時候會執行 setHeaderMargin() 該函式作用主要是實現佈局的隱藏

 /**
     * 設定header view 的topMargin的值
     *
     * @description
     * @param topMargin
     *            ,為0時,說明header view 剛好完全顯示出來; 為-mHeaderViewHeight時,說明完全隱藏了
     */
    private void setHeaderTopMargin(int topMargin) {
        LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();
        params.topMargin = topMargin;
        mHeaderView.setLayoutParams(params);
        invalidate();
    }


         7、以上步驟基本實現了整個下拉重新整理,上拉載入的功能,但是美中不足,重新整理完成後我們還需要隱藏我們的佈局,下面的程式碼是更新完後恢復初始化狀態

  /**
     * header view 完成更新後恢復初始狀態
     *
     * @description hylin 2012-7-31上午11:54:23
     */
    public void onHeaderRefreshComplete() {
        setHeaderTopMargin(-mHeaderViewHeight);
        mHeaderImageView.setVisibility(View.VISIBLE);
        mHeaderImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow);
        mHeaderTextView.setText(R.string.pull_to_refresh_pull_label);
        mHeaderProgressBar.setVisibility(View.GONE);

        mHeaderState = PULL_TO_REFRESH;
    }


    /**
     * footer view 完成更新後恢復初始狀態
     */
    public void onFooterRefreshComplete() {
        setHeaderTopMargin(-mHeaderViewHeight);
        mFooterImageView.setVisibility(View.VISIBLE);
        mFooterImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow_up);
        mFooterTextView.setText(R.string.pull_to_refresh_footer_pull_label);
        mFooterProgressBar.setVisibility(View.GONE);

        mFooterState = PULL_TO_REFRESH;
    }

         8、以上基本實現了下拉重新整理上拉載入,部落格也寫累了,剩餘的功能我就不貼程式碼了,可以參看Demo