一個強大的下拉重新整理框架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風格的頭部等等。大家感興趣可以直接閱讀原始碼。