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