1. 程式人生 > >一個強大的下拉重新整理框架android-Ultra-Pull-To-Refresh

一個強大的下拉重新整理框架android-Ultra-Pull-To-Refresh

最近在學習github上的一個開源專案:android-Ultra-Pull-To-Refresh(下面簡稱UltraPtr) 。這個專案主要用於Android APP中下拉重新整理的功能。

OK,之所以說UltraPtr非常強大,是因為它有以下兩個特點: 
1. content可以是任意的view; 
2. 簡介完善的header抽象,使用者可以對header高度自定義;

在理解了UltraPtr原始碼之後,我仿照它寫了一個簡單的下拉重新整理應用。為了直觀,先貼上效果圖,然後再分析程式碼。 


這裡寫圖片描述 

這裡寫圖片描述 

這裡寫圖片描述 

可以看到,UltraPtr框架支援各種content的下拉重新整理,並且你也可以對頭部header進行自定義,使用各種酷炫的header。

抽象介面

首先抽象出兩個介面:PtrHandler和PtrUIHandler;

public interface PtrHandler
{
    /**
     * check can do refresh or not
     *
     * @param frame
     * @param content
     * @param header
     * @return
     */
    public boolean checkCanDoRefresh(final PtrFrameLayout frame, final View content, final View header);

    /**
     * when refresh begin
     *
     * @param
frame */
public void onRefreshBegin(final PtrFrameLayout frame); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

PtrHandler代表下拉重新整理的功能介面,包含下拉重新整理的回撥,以及判斷是否可以下拉。

public interface PtrUIHandler
{
    public void onUIReset(PtrFrameLayout frame);

    public void onUIRefreshPrepare(PtrFrameLayout frame);

    public
void onUIRefreshBegin(PtrFrameLayout frame); public void onUIRefreshComplete(PtrFrameLayout frame); public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

PtrUIHandler是下拉重新整理的UI介面,包括:準備下拉,下拉中,開始重新整理,重新整理完成,以及下拉過程中位置變化等回撥方法。一般header需要實現此介面。

自定義下拉重新整理控制元件PtrFrameLayout

測量與佈局

PtrFrameLayout代表一個下拉重新整理的自定義控制元件,繼承自ViewGroup。有且只有兩個子view:頭部header和內容content。下面對類PtrFrameLayout中的主要方法進行分析。

和所有自定義控制元件一樣,PtrFrameLayout通過重寫onFinishInflate,onMeasure, onLayout來確定控制元件的大小和位置。

public class PtrFrameLayout extends ViewGroup
{
    //status enum
    public final static byte PTR_STATUS_INIT = 1;
    public byte mStatus = PTR_STATUS_INIT;
    public final static byte PTR_STATUS_PREPARE = 2;
    public final static byte PTR_STATUS_LOADING = 3;
    public final static byte PTR_STATUS_COMPLETE = 4;

    private PtrIndicator mPtrIndicator;

    private int mDurationToClose = 200;
    private int mDurationToCloseHeader = 1000;

    private long mLoadingStartTime = 0;

    private View mHeaderView;
    private View mContentView;

    private int mPagingTouchSlop;

    private final boolean DEBUG = true;
    private static int ID = 1;
    protected final String TAG = "ptr-frame-" + ++ID;

    private int mHeaderHeight;

    private boolean mPreventForHorizontal = false;

    private ScrollChecker mScrollChecker;

    private PtrHandler mPtrHandler;
    private PtrUIHandler mPtrUIHandler;

    public PtrFrameLayout(Context context)
    {
        this(context, null);
    }

    public PtrFrameLayout(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

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

        mPtrIndicator = new PtrIndicator();

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PtrFrameLayout, 0, 0);

        mDurationToClose = a.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close, mDurationToClose);
        mDurationToCloseHeader = a.getInt(R.styleable.PtrFrameLayout_ptr_duration_to_close_header, mDurationToCloseHeader);

        float resistence = a.getFloat(R.styleable.PtrFrameLayout_ptr_resistence, mPtrIndicator.getResistence());
        mPtrIndicator.setResistence(resistence);

        float ratio = a.getFloat(R.styleable.PtrFrameLayout_ptr_ratio_of_header_height_to_refresh, //
                mPtrIndicator.getRatioOfHeaderHeightToRefresh());
        mPtrIndicator.setRatioOfHeaderHeightToRefresh(ratio);

        a.recycle();

        mScrollChecker = new ScrollChecker();

        ViewConfiguration vc = ViewConfiguration.get(getContext());
        mPagingTouchSlop = vc.getScaledTouchSlop() * 2;
    }

    @Override
    protected void onFinishInflate()
    {
        int childCount = getChildCount();
        if (childCount > 2) {
            throw new IllegalStateException("PtrFrameLayout only can host 2 elements");
        } else if (childCount == 2) {
            mHeaderView = getChildAt(0);
            mContentView = getChildAt(1);
        } else if (childCount == 1) {
            mContentView = getChildAt(0);
        } else {
            TextView errorView = new TextView(getContext());
            errorView.setClickable(true);
            errorView.setTextColor(0xffff6600);
            errorView.setGravity(Gravity.CENTER);
            errorView.setTextSize(20);
            errorView.setText("The content view in PtrFrameLayout is empty!");
            mContentView = errorView;
            addView(mContentView);
        }

        if (mHeaderView != null) {
            mHeaderView.bringToFront();
        }

        super.onFinishInflate();
    }

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

        if (DEBUG) {
            HLog.d(TAG, "onMeasure frame: width: %s, height: %s, padding: %s %s %s %s", getMeasuredWidth(), //
                    getMeasuredHeight(), getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom());
        }

        //測量子view
        if (mHeaderView != null) {
            measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0);
            MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
            mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            mPtrIndicator.setHeaderHeight(mHeaderHeight);
        }
        if (mContentView != null) {
            measureContentView(mContentView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    private void measureContentView(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
    {
        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        int widthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, //
                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width);
        int heightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, //
                getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin, lp.height);

        child.measure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
        int offsetY = mPtrIndicator.getCurrentPosY();
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();

        if (mHeaderView != null) {
            MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
            final int left = paddingLeft + lp.leftMargin;
            final int top = paddingTop + lp.topMargin - mHeaderHeight + offsetY;
            final int right = left + mHeaderView.getMeasuredWidth();
            final int bottom = top + mHeaderView.getMeasuredHeight();
            mHeaderView.layout(left, top, right, bottom);
            if (DEBUG) {
                HLog.d(TAG, "onLayout header: %s %s %s %s", left, top, right, bottom);
            }
        }

        if (mContentView != null) {
            MarginLayoutParams lp = (MarginLayoutParams) mContentView.getLayoutParams();
            final int left = paddingLeft + lp.leftMargin;
            final int top = paddingTop + lp.topMargin + offsetY;
            final int right = left + mContentView.getMeasuredWidth();
            final int bottom = top + mContentView.getMeasuredHeight();
            mContentView.layout(left, top, right, bottom);
            if (DEBUG) {
                HLog.d(TAG, "onLayout content: %s %s %s %s", left, top, right, bottom);
            }
        }
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams()
    {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p)
    {
        return new MarginLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs)
    {
        return new MarginLayoutParams(getContext(), attrs);
    }
    ......
    ......
    ......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185

首先,我們在建構函式PtrFrameLayout(Context context, AttributeSet attrs, int defStyleAttr)獲取自定義屬性值,以及初始化一些成員變數,其中mScrollChecker是一個runnable物件,主要用來實現View的平滑移動,下面會有詳細解釋。

在onFinishInflate()回撥方法中,根據佈局檔案中子view的個數對成員變數mHeaderView和mContentView進行初始化。外部可以同時在佈局檔案中指定mHeaderView和mContentView,也可以只指定mContentView,mHeaderView通過程式碼進行設定。

在onMeasure()回撥方法中,對子view進行了測量。首先使用measureChildWithMargins()對頭部mHeaderView進行了測量,之後將頭部的測量的高度更新到PtrIndicator變數中,PtrIndicator是一個工具類,主要負責跟蹤記錄滑動過程中Y方向的偏移量等等。

在onLayout()回撥方法中,通過top = paddingTop + lp.topMargin - mHeaderHeight + offsetY; 計算出header的top值,可以看到header向上偏移了mHeaderHeight,這樣頭部header初始情況下就會被隱藏。注意,程式碼中有個offsetY,初始值為0,隨著下拉過程中,offsetY會逐漸增大,這樣header和content都會向下移動,header就會顯示出來,出現下拉位置移動的效果。

計運算元view大小的時候用到了MarginLayoutParams,所以我們需要重寫generateDefaultLayoutParams()方法。

事件處理

ViewGroup的事件處理,通常重寫onInterceptTouchEvent 方法或者 dispatchTouchEvent 方法,PtrFrameLayout重寫了dispatchTouchEvent 方法。

事件處理流程圖如下: 


這裡寫圖片描述 

後面我會附上原始碼,感興趣的朋友可以對比原始碼去理解事件處理流程。

自定義header

經典下拉重新整理的頭部

PtrClassicDefaultHeader.Java

private void resetView()
{
    mProgressBar.setVisibility(INVISIBLE);
    hideRotateView();
}

private void hideRotateView()
{
    mRotateView.clearAnimation();
    mRotateView.setVisibility(INVISIBLE);
}

@Override
public void onUIReset(PtrFrameLayout frame)
{
    resetView();

    mShoulShowLastUpdate = false;
    tryUpdateLastUpdateTime();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

重置header view,隱藏進度條,隱藏箭頭,更新最後重新整理事件

@Override
public void onUIRefreshPrepare(PtrFrameLayout frame)
{
    mShoulShowLastUpdate = true;
    tryUpdateLastUpdateTime();
    mLastUpdateTimeUpdater.start();

    mProgressBar.setVisibility(INVISIBLE);
    mRotateView.setVisibility(VISIBLE);
    mTitleView.setText(R.string.hebut_ptr_pull_down);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

準備重新整理,隱藏進度條,顯示旋轉箭頭,提示文字為:pull down;啟動一個runnable物件,來實時更新上次重新整理時間。

@Override
public void onUIRefreshBegin(PtrFrameLayout frame)
{
    hideRotateView();
    mProgressBar.setVisibility(VISIBLE);
    mTitleView.setVisibility(VISIBLE);
    mTitleView.setText(R.string.hebut_ptr_updating);

    mShoulShowLastUpdate = false;
    tryUpdateLastUpdateTime();
    mLastUpdateTimeUpdater.stop();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

開始重新整理,隱藏旋轉箭頭,顯示進度條,提示文字:updating。停止mLastUpdateTimeUpdater。

@Override
public void onUIRefreshComplete(PtrFrameLayout frame)
{
    hideRotateView();
    mProgressBar.setVisibility(INVISIBLE);
    mTitleView.setVisibility(VISIBLE);
    mTitleView.setText(R.string.hebut_ptr_update_complete);

    //update last update time
    SharedPreferences sharedPreferences = getContext().getSharedPreferences(KEY_SharedPreferences, 0);
    if(!TextUtils.isEmpty(mLastUpdateTimeKey)) {
        mLastUpdateTime = new Date().getTime();
        sharedPreferences.edit().putLong(mLastUpdateTimeKey, mLastUpdateTime).commit();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

重新整理完成,隱藏進度條,隱藏旋轉箭頭,提示文字:updated;向shared檔案中更新最新重新整理時間。

@Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator)
{
    final int offsetToRefresh = ptrIndicator.getOffsetToRefresh();
    final int currentPos = ptrIndicator.getCurrentPosY();
    final int lastPos = ptrIndicator.getLastPos();

    if(currentPos < offsetToRefresh && lastPos >= offsetToRefresh) {
        if(isUnderTouch && status == frame.PTR_STATUS_PREPARE) {
            crossRotateLineFromBottomUnderTouch();
        }
    } else if(currentPos > offsetToRefresh && lastPos <= offsetToRefresh) {
        if(isUnderTouch && status == frame.PTR_STATUS_PREPARE) {
            crossRotateLineFromTopUnderTouch();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

根據使用者下拉拖拽的距離,動態改變箭頭的方向以及提示文字的內容。當下拉距離從小於下拉重新整理高度到大於重新整理高度,箭頭從向下,變成向上,同時改變提示文字的顯示;當下拉距離從大於下拉重新整理高度到小於重新整理高度,箭頭從向上,變成向下,同時改變提示文字的顯示。

OK,這裡就實現了一個經典的下拉重新整理頭部header。專案原始碼中還有很多自定義頭部,比如,Material Design風格,StoreHouse風格的頭部等等。大家感興趣可以直接閱讀原始碼。