下拉刷新實現

下拉刷新在Android應用開發中是一種很常見的交互方式,在實際開發中都會引用第三方的下拉刷新庫來實現,第三方庫通常都經過多個應用程序集成測試,有着相對較高的穩定性和可靠性,裏面的代碼邏輯也相對比較龐雜,對新手相對不太友好,學習起來比較費時費力,本節就通過前面學習的Android視圖基本原理來實現自定義的下拉刷新庫。

補白和邊距

補白(Padding)指的是視圖內部的內容與視圖邊界之間的距離,通常上下左右四個方向都可以指定補白寬度,補白就相當於視圖內容的鑲邊,它們處於視圖範圍內。邊距(Margin)指的是當前視圖與其他視圖之間的距離,其他的視圖可以是它的父視圖也可以是兄弟視圖,邊距的位置通常都屬於視圖的父視圖,它主要負責將不同的視圖分隔開防止它們相互疊加。開發中通常Padding和Margin都設置的是正數,假如把Padding和Margin的值設置爲負數又會有什麼樣的效果呢,這裏只測試常見的LinearLayout佈局,在它們的內部添加子視圖並且設置負數的Padding和Margin值。
在這裏插入圖片描述
在這裏插入圖片描述

上圖展示了在LinearLayout中設置了負數值的子視圖展示情況,可以看到負數的Padding不僅會影響子視圖內容的展示還會影響父佈局的尺寸大小。我們知道onMeasure()方法負責測量當前視圖的寬高值,onLayout()負責將佈局中的視圖設置到指定的位置,查看LinearLayout豎向佈局的尺寸測量代碼。

在measureVertical()測量豎向佈局高度時會首先計算內部可見的子視圖高度總值,子佈局的高度還要加上下補白的數值得到heightSize數值,heightSize還有與最小高度作比較,其實大部分情況都能確保最終setMeasureDimension()方法中使用的高度值就是heightSize的值。考慮前面的mPaddingTop設置成負值的情況,負值會減少heightSize最終的計算結果值也就導致LinearLayout的高度減小。接着查閱LinearLayout豎向佈局方法的實現,看它如何排放內部的子視圖位置。

// LinearLayout測量佈局源代碼
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	if (mOrientation == VERTICAL) {
		measureVertical(widthMeasureSpec, heightMeasureSpec);
	} else {
		measureHorizontal(widthMeasureSpec, heightMeasureSpec);
	}
}
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
	// 計算子控件的總高度mTotalLength
	// 總高度會加上自己的上下補白,mPaddingTop爲負值會減小布局高度
	mTotalLength += mPaddingTop + mPaddingBottom;
	int heightSize = mTotalLength;
	heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); 
	int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
	setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
		heightSizeAndState); // 設置LinearLayout的高度爲heightSize
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
	if (mOrientation == VERTICAL) {
		layoutVertical(l, t, r, b);
	} else {
		layoutHorizontal(l, t, r, b);
	}
}

void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;
    int childTop = mPaddingTop; // mPaddingTop爲負值也會影響子視圖展示位置
    int childLeft;
	for (View view : getChildren()) {
		childTop += lp.topMargin; // 此處如果topMargin是負值,會影響子視圖展示位置
	   childLeft = paddingLeft + lp.leftMargin;
		// child.layout(left, top, left + width, top + height);
		setChildFrame(child, childLeft, childTop + getLocationOffset(child),
					childWidth, childHeight);
		childTop += view.getMeasureHeight();
	}
}

在layoutVertical()方法中會根據LinearLayout的paddingLeft和子視圖的LayoutParams.leftMargin計算當前子視圖左邊界距離,mPaddingTop和子視圖的LayoutParams.topMargin計算子視圖的上邊界在佈局中的位置。考慮前面的marginTop邊距設置爲負值的情況,由於負值會使得childTop值變小,也就是說子視圖距離LinearLayout頂部邊界變短,子視圖的位置也就更加靠上。

如果測試橫向的LinearLayout會發現即使設置了負值mPaddingTop,它的高度也不會發生變化,查閱layoutHorizontal()方法會發現在測量高度的時候並不會把mPaddingTop值計算在內,自然也就不會發生最終的佈局高度改變的效果。測試其他四大布局會發現有些負值mPaddingTop能夠改變佈局高度,有些設置負值根本不會對佈局高度產生任何效果,總結來說在setMeasureDimension() 方法中設置的高度值如果計算了mPaddingTop和mPaddingBottom那麼負值補白就可以改變佈局高度。

刷新視圖原理

在下拉刷新中控件的頂部會慢慢地出現下拉視圖,下拉視圖展示過程中就代表正在執行網絡請求操作,等到網絡請求成功返回下拉視圖會慢慢消失,展示已經刷修改完數據的新界面。下拉視圖的展示和消失都是有一個漸進的過程,不是setVisible()那種即刻消失或展示的樣式,想要實現這種漸進展示和消失的動畫效果就可以利用負值補白來改變下拉視圖的高度值,當mPaddingTop爲0的時候刷新視圖正常展示;當mPaddingTop從0到負下拉視圖高度變化時下拉視圖組件高度逐漸變成0,也就是逐漸消失;當mPaddingTop從負下拉視圖高度到0變化時下拉視圖高度逐漸變大,也就是逐漸展示。

// PullRefreshView基礎實現代碼
public class PullRefreshView extends FrameLayout {
    private static final String TAG = "PullRefreshView";

    private View mHeaderView;
    private View mContentView;
    private static final int MAX_PULL_LENGTH = Utils.dp2px(200);
    private static final long MAX_GOBACK_DURATION = 200;

    private static final int REFRESH_IDLE = 0; // 靜止狀態
    private static final int REFRESH_PULL = 1; // 手動下拉
    private static final int REFRESH_RELEASED = 2; // 下拉鬆手
    private static final int REFRESH_REFRESHING = 3; // 正在刷新
    private int mState = REFRESH_IDLE;
    private int mHeaderHeight;
	private int mTouchSlop;

	public interface RefreshListener {
        void onRefresh();
    }

    private RefreshListener mRefreshListener;
    private void notifyRefreshStart() {
        if (mRefreshListener != null) { // 通知開始刷新操作
            mRefreshListener.onRefresh();
        }
    }
    public void notifyRefreshComplete() {
        if (isRefreshing()) { // 刷新結束,頭部視圖彈回到不可見
            headerGoBack();
        }
    }
	// 暫時省略其他部分
}

現在開始自定義的下拉刷新控件的實現,讓它繼承自FrameLayout佈局,內部包含兩個主要的成員mHeaderView也就是下拉視圖,mContentView也就是包含內容的視圖對象,比如後面會提到的ScrollView、ListView和RecyclerView。下拉刷新過程是要消耗比較長的時間,對於不能即刻完成的動作爲了避免錯誤訪問可以使用狀態機來保存它的內部狀態,在某種狀態下只能執行一些合法的操作避免出現錯誤。默認情況下的狀態爲空閒狀態REFRESH_IDLE,當用戶向下拉動內容控件時處於REFRESH_PULL下拉狀態,如果頭部視圖完全展示出來等到用戶鬆手此時控件內部處於REFRESH_RELEASED狀態,用戶鬆手後開始發起網絡請求控件處於刷新狀態REFRESH_REFRESHING,刷新完成後控件又進入了空閒狀態,下拉刷新的狀態遷移如下圖。控件的有些操作只有在特定狀態下纔可以執行,比如onRefreshComplete()完成刷新操作必須要求之前狀態是REFRESH_REFRESHING正在刷新,如果不是就說明內部狀態有問題,需要開發者及時修改內部狀態維護出現的異常情況。
在這裏插入圖片描述
如果用戶下拉時頭部視圖完全可見再釋放下拉刷新後需要觸發網絡請求,定義RefreshListener接口內部包含onRefresh()方法,需要監控下拉刷新事件的開發者可以註冊刷新監聽器。當網絡請求完成後可以調用notifyRefreshComplete()方法通知下拉刷新控件收起下拉視圖修改內部狀態值。如果用戶下拉時頭部僅僅漏出一部分內容如下圖,在用戶釋放刷新時僅僅將頭部視圖回彈到不可見,並不會觸發網絡請求操作。
在這裏插入圖片描述
在headerGoBack()方法中會使用ValueAnimator逐漸修改mHeaderView的mPaddingTop值使得下拉視圖高度組件變小直到消失不見。在動畫結束的時候同時把下拉刷新控件內部的狀態更新成空閒狀態,完成一次下拉刷新狀態遷移。這裏並沒有提到下拉刷新視圖是如何展示出來的,不同的內容控件有不同方式觸發展示邏輯,後面刷新具體的內容控件時再詳述下拉視圖的展示動畫實現。

// HeaderView拉伸回彈實現代碼
private void setHeaderPaddingTop(int top) {
// HeaderView會隨着paddingTop變化逐漸消失或逐漸展示
mHeaderView.setPadding(0, top, 0, 0);
mHeaderView.requestLayout();
}

private void headerGoBack() {
    if (!isReleased() && !isRefreshing()) {
        return;
    }
    ValueAnimator valueAnimator = 
ValueAnimator.ofInt(getHeaderPaddingTop(), -mHeaderHeight);
    valueAnimator.setDuration(MAX_GOBACK_DURATION);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int value = (int) animation.getAnimatedValue();
            setHeaderPaddingTop(value); // 不斷地更改頭部視圖paddingTop
        }
     });
    valueAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mState = REFRESH_IDLE; // 頭部視圖完全不可見時進入REFRESH_IDEL狀態
        }
    });
    valueAnimator.start();
}

ScrollView下拉刷新

PullRefreshView接收到的觸摸事件一概傳遞給它的內容控件來處理,不過原生的ScrollView控件內部的觸摸事件處理已經固定下來,需要使用ScrollView的子類覆蓋dispatchTouchEvent()來修改它默認的處理方式。爲此需要在加載PullRefreshView內部的InternalScrollView控件的時候替換系統提供的原生ScrollView。

// PullRefreshView替換內部的用戶ScrollView
private void initViews() {
    mContentView = getChildAt(0);
    FrameLayout.LayoutParams contentParams = (LayoutParams) 
mContentView.getLayoutParams();
    contentParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
    contentParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
    removeView(mContentView);
      
// 將原生ScrollView替換成支持下拉刷新的InternalScrollView
if (mContentView instanceof ScrollView) { 
         mContentView = new InternalScrollView(getContext(),
 (ScrollView) mContentView);
     }
	 mContentView.setLayoutParams(contentParams);
     addView(mContentView);
}
	
// 將PullRefreshView接收到的所有觸摸事件都傳遞給內容控件
public boolean dispatchTouchEvent(MotionEvent event) {
    return mContentView.dispatchTouchEvent(event);
}

替換後的InternalScrollView作爲PullRefreshView內部的mContentView成員對象,系統派發過來的所有MotionEvent事件都由InternalScrollView負責處理。由於原生ScrollView內部只有用戶內容佈局存在, InternalScrollView需要先將原生的ScrollView內部用戶內容對象添加到豎向LinearLayout底部,LinearLayout的上面部分則負責展示下拉視圖。在dispatchTouchEvent()方法中首先判斷用戶是否在做滑動操作,如果是滑動操作是否滿足下拉刷新的條件,滿足條件就要執行下拉刷新視圖展示動畫,否則需要調用super.dispatchTouchEvent()實現默認的ScrollView觸摸事件處理。

// InternalScrollView實現代碼
private class InternalScrollView extends ScrollView {
    private int mDownY;
    private int mLastY;
    private boolean mIsDragging = false; // 是否在做下拉刷新動作

    public InternalScrollView(Context context, ScrollView origin) {
        super(context);
        setId(origin.getId()); // 將原生ScrollView的id設置給InternalScrollView
        LinearLayout linearLayout = new LinearLayout(getContext());
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        // 獲取原生ScrollView中的用戶內容佈局
        View content = origin.getChildAt(0);
        origin.removeAllViews();
        // 豎向LinearLayout內部包含下拉視圖和用戶內容佈局
        linearLayout.addView(mHeaderView);
        linearLayout.addView(content);
        // InternalScrollView內部的佈局包含下拉刷新視圖和用戶內容視圖
        addView(linearLayout);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();
        int y = (int) event.getRawY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int motionY = y - mDownY; // 代表用戶滑動的方向
                int diff = y - mLastY; // 用戶本次滑動與上次滑動的偏差
                // 如果當前沒有滑動操作而且用戶移動距離超出最小滑動
// 距離mTouchSlop,如果用戶向下滑動且內容控件的第一
// 條數據處在內容頂部,此時需要準備開始下拉操作;如果
// 用戶向上滑動而且頭部視圖部分可見,準備向上滑動頭部視圖
                if (!mIsDragging && Math.abs(motionY) > mTouchSlop &&
 ((motionY > 0 && isFirstAtTop()) ||
                                  isFirstAtTop() && motionY < 0 &&
 getHeaderPaddingTop() > -mHeaderHeight)) {
                     mIsDragging = true;
                }

                if (mIsDragging) { // 用戶正在做滑動操作
                    mState = REFRESH_PULL;
                    offsetHeader(diff);  // 漸進增大或減小下拉視圖
                    if (getHeaderPaddingTop() <= -mHeaderHeight) {
                        // 如果用戶手動將下拉視圖推到了
// 不可見位置,不再修改下拉視圖的大小
                        mState = REFRESH_IDLE;
                        mIsDragging = false;
                        setHeaderPaddingTop(-mHeaderHeight);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsDragging = false;
                if (isPulling()) { // 如果用戶正在下拉過程中鬆手
                    mState = REFRESH_RELEASED;
// 如果下拉視圖已經全部展示出來需要
// 先退回展示全部,再觸發刷新操作
                    if (shouldRefresh()) { 
                        mState = REFRESH_REFRESHING;
                        goBackAndShowRefresh(); // 參考代碼4-38
                    } else {
// 如果下拉視圖沒有全部展示,只下拉了一下部分,
// 直接退回去不觸發刷新
                        headerGoBack();
                    }
                }
                break;
       }
       mLastY = y;
       // 如果mIsDragging爲true代表用戶正在做下拉刷新,
// 否則就執行ScrollView內部滾動
       return mIsDragging || super.dispatchTouchEvent(event);
   }
   // 判定當前用戶內容視圖的頂部在InternalScrollView的頂部,沒有內容被捲起來
// 用戶這時向下拉就是要做下拉刷新
   public boolean isFirstAtTop() {
       return mContentView.getScrollY() <= 0;
    }
}

上面的代碼完整展示了InternalScrollView內部處理下拉刷新的整個過程,最開始的構造函數中先要爲原始用戶內容控件添加下拉刷新頭部視圖,最終替換成下圖所示,在初始情況下HeaderView是完全不展示的,僅僅展示底部原始用戶內容佈局。當用戶在InternalScrollView上按下,首先記錄下最初的按下位置mDownY,並且由super.dispatchTouchEvent(event)處理返回true代表接受後續的觸摸事件,如果用戶接着移動手指就會發送ACTION_MOVE事件,判定用戶正在做滑動操作,除了要求用戶從ACTION_DOWN到ACTION_MOVE移動的距離超出最小滑動距離外,還要求用戶向上或向下滑動時內容視圖沒有捲起高度,也就是mScrollY的值爲0,而且此時的HeaderView需要完全不可見,此時認定用戶正在做下拉刷新操作,之所以存在用戶向上滑動是因爲下拉過程中用戶是可以向上滑動的。
在這裏插入圖片描述
確定用戶在做下拉滑動操作後就需要根據用戶滑動偏移不斷調整HeaderView的paddingTop大小,此時就能見到HeaderView不斷變大或者不斷減小的效果,當然如果用戶一直向上移動HeaderView的paddingTop值就可能越減越小,當paddingTop減小到HeaderView的負值高度時可以忽略用戶向上移動。當用戶最終釋放下拉拖動時在ACTION_UP中判定HeaderView是否已經完全展示,如果是就觸發刷新操作,否就直接將部分展示的HeaderView彈回不可見。爲了保證用戶操作的平滑性用戶下拉可以把HeaderView拉到比實際高度高很多的距離,這種情況下就需要先將多拉出來的高度隱藏再開始觸發刷新工作。
在這裏插入圖片描述
上圖中用戶下拉很長距離導致HeaderView整體的高度比原始高度高了很多,此時就需要先把HeaderView被多拉出來的高度隱藏起來,等到超長高度隱藏結束後就可以通知觸發刷新操作。

// HeaderView拉伸過長彈回到實際高度展示
private void goBackAndShowRefresh() {
    if (!isRefreshing()) {
        return;
    }

int paddingTop = getHeaderPaddingTop();
// paddingTop爲零的時候下拉視圖完全展示,超出0時需要先回到0
if (paddingTop > 0) {             
ValueAnimator valueAnimator = ValueAnimator.ofInt(paddingTop, 0);
        valueAnimator.setDuration(MAX_GOBACK_DURATION);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int value = (int) animation.getAnimatedValue();
                setHeaderPaddingTop(value);
            }
        });
         valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
               // 下拉視圖paddingTop爲零時觸發刷新操作
                notifyRefreshStart();
             }
        });
        valueAnimator.start();
    } else {
        notifyRefreshStart();// 下拉視圖paddingTop爲零時觸發刷新操作
    }
}

代碼中paddingTop大於零就代表用戶將HeaderView下拉的比實際高度要高出paddingTop的長度,需要先將HeaderView縮回到paddingTop爲零的正常高度再觸發刷新操作。到目前爲止ScrollView的下拉刷新就成功觸發了網絡請求,等到網絡請求成功後會通知刷新操作已完成並調用headGoBack()實現下拉視圖漸進消失操作,ScrollView的一次下拉刷新交互就完成了。

ListView下拉刷新

在初始化PullRefreshView內部控件時如果子控件是ListView類型,需要將它替換成InternalListView自定義控件,InternalListView需要控件會在調用addHeaderView()方法添加HeaderView爲頭部視圖,這樣當頭部視圖的高度發生變化的時候ListView內部的用戶內容控件也會隨之變化位置。

// InternalListView實現代碼
private class InternalListView extends ListView {
	private int mDownY;
	private int mLastY;
	private boolean mIsDragging = false;
	public InternalListView(Context context, ListView origin) {
		super(context);
		ListView.LayoutParams layoutParams = 
new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 
ViewGroup.LayoutParams.WRAP_CONTENT);
		FrameLayout frameLayout = new FrameLayout(getContext());
		frameLayout.addView(mHeaderView);
		frameLayout.setLayoutParams(layoutParams);
		addHeaderView(frameLayout); // 將下拉視圖添加成ListView的頭部視圖
		setId(origin.getId());
	}
	// 與InternalScrollView的處理基本一樣
	public void dispatchTouchEvent(MotionEvent e) { .... }
	public boolean isFirstAtTop() { // 判定ListView頭部沒有內容被捲起
		if (getChildCount() < 2) {
			return false;
		}
		View view = getChildAt(1);
        // 第二個View,其實就是第一個用戶內容View並且展示的是第一條用戶數據
		return view.getTop() < mTouchSlop && getFirstVisiblePosition() <= 1;
	}
}

InternalListView和InternalScrollView在判定頂部內容沒有捲起稍有不同,InternalListView要判定它內部的第二個視圖處於頂部位置,用戶在這種情況下向下滑動才能夠被判定是在做下拉刷新操作。在InternalListView替換ListView控件時會在頭部添加下拉視圖,下拉視圖就是它內部的第一個視圖。ListView內部會使用回收複用機制防止過多創建視圖對象,第二個視圖並不代表它展示的是用戶數據中的第一條內容,需要加上getFirstVisiblePosition() <= 1確保第二個視圖展示的是用戶數據列表裏的第一條數據。
在這裏插入圖片描述

RecycleView下拉刷新

RecyclerView是Android Design包中提供的用於替換ListView和GridView等動態視圖的控件,通過設置不同的LayoutManager對象就可以實現展示成ListView樣式還是GridView樣式,這裏僅僅討論ListView樣式展示的RecyclerView的下拉刷新實現。RecyclerView自帶了ViewHolder機制實現,但不包含添加頭部視圖和底部視圖的功能,想要像ListView那樣通過添加頭部視圖來實現下拉刷新就需要先實現RecyclerView的頭部和底部視圖添加功能。

//  InternalRecyclerView實現代碼
private class InternalRecyclerView extends BaseRecyclerView {
	private boolean mIsDragging = false;
	private View mHeaderView;

	public InternalRecyclerView(Context context, RecyclerView origin) {
		super(context);
		setId(origin.getId());
		setLayoutManager(new LinearLayoutManager(context));
		mHeaderView = LayoutInflater.from(context).inflate(R.layout.layout_header, this, false);
		addHeaderView(mHeaderView);
	}
	// 與InternalScrollView的處理基本一樣
	public void dispatchTouchEvent(MotionEvent e) { .... }
	public boolean isFirstAtTop() {
		return !canScrollVertically(-1);
	}
}

總體上來說RecyclerView的實現和ListView基本類似,不過RecyclerView的下拉刷新判定還是有點特殊的,RecyclerView可以使用 !canScrollVertically(-1)判定它是否能夠向下拉動,如果無法向下拉動表示用戶目前正在做下拉刷新操作。
在實現了下拉操作的判定後只剩下如何實現在RecyclerView中添加頭部視圖的實現,參考ListView的源代碼中實現添加頭部和底部視圖的實現,源碼中會創建HeaderWrapperAdapter對象,它會包含用戶添加的HeaderView,FooterView和用戶設置的Adapter對象。

// HeaderWrapperAdapter實現代碼
public class HeaderWrapperAdapter extends RecyclerView.Adapter<BaseRecyclerViewHolder> {
    private List<View> mHeaders; // HeaderView列表
    private List<View> mFooters; // FooterView列表
    private BaseRecyclerAdapter<BaseRecyclerViewHolder> mAdapter; // 用戶Adapter對象
    private static final int HEADER_VIEW_TYPE = 0x8888; // HeaderView類型
    private static final int FOOTER_VIEW_TYPE = 0x9999; // FooterView類型

	HeaderWrapperAdapter(List<View> headers, List<View> footers, 
			BaseRecyclerAdapter adapter) {
        this.mHeaders = headers;
        this.mFooters = footers;
        this.mAdapter = adapter;
    }

    @Override
	public BaseRecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
		 int viewType) {
        int realType = getType(viewType),  position = getPosition(viewType);
        if (realType == HEADER_VIEW_TYPE) {
            return new HeaderViewHolder(mHeaders.get(position));
        } else if (realType == FOOTER_VIEW_TYPE) {
            return new HeaderViewHolder(mFooters.get(position – 
mAdapter.getItemCount() - mHeaders.size()));
        } else {
            return mAdapter.onCreateViewHolder(viewGroup, realType);
        }
    }

    @Override // 綁定ViewHolder是只需要執行用戶內容Adapter的綁定操作
	public void onBindViewHolder(@NonNull BaseRecyclerViewHolder viewHolder, 
		int position) {
        if (position >= mHeaders.size() && 
position < mHeaders.size() + mAdapter.getItemCount()) {
            mAdapter.onBindViewHolder(viewHolder, position - mHeaders.size());
        }
    }

    @Override // 綁定ViewHolder是只需要執行用戶內容Adapter的綁定操作
	public void onBindViewHolder(@NonNull BaseRecyclerViewHolder viewHolder,
		 int position, @NonNull List<Object> payloads) {
        if (position >= mHeaders.size() && 
position < mHeaders.size() + mAdapter.getItemCount()) {
            mAdapter.onBindViewHolder(viewHolder, position - mHeaders.size());
        }
    }

    @Override
	public int getItemCount() { 
        // RecyclerView內的元素個數,頭視圖、尾視圖和用戶視圖總個數
		return mHeaders.size() + mAdapter.getItemCount() + mFooters.size();
    }

    @Override
    public int getItemViewType(int position) { // 根據position決定視圖類型
        if (position < mHeaders.size()) {
            return makeTypePos(HEADER_VIEW_TYPE, position);
        } else if (position < mHeaders.size() + mAdapter.getItemCount()) {
            return makeTypePos(mAdapter.getItemViewType(
position - mHeaders.size()), position);
        } else {
            return makeTypePos(FOOTER_VIEW_TYPE, position);
        }
    }

    private int makeTypePos(int type, int pos) { // viewType和position綁定到int中
        return (type << 16) + pos;
    }

    private int getType(int typePos) { // 從int高16爲獲取viewType
        return typePos >>> 16;
    }

    private int getPosition(int typePos) { // 從int低16位獲取position
        return typePos & 0xffff;
    }

    private static final class HeaderViewHolder extends BaseRecyclerViewHolder {
        HeaderViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }
}

代碼中通過繼承RecyclerView.Adapter創建了HeaderWrapperAdapter類型,該類型內部增加了HeaderView列表與FooterView列表,同時包含用戶自己的RecyclerView.Adapter對象 ,在計算適配器內部數量長度包含了頭部列表、尾部列表和用戶適配器數據長度。當RecyclerView展示內部控件時先調用getItemViewType()根據position判定是頭部視圖、尾部視圖還是用戶內容視圖,然後在將視圖類型viewType和position綁定到int中傳遞到onCreateViewHolder()創建不同類型的ViewHolder對象。onCreateViewHolder()方法中只允許傳遞進來viewType類型數據,不過這裏使用移位運算符實現在int數字中保存viewType和當前position兩個數據,本節就使用如下的處理方式使用int的前兩個字節保存viewType,後兩個字節保存position。
在這裏插入圖片描述
接着創建自定義的BaseRecyclerView繼承自RecyclerView,它內部使用HeaderWrapperAdapter管理內容視圖,當用戶調用setAdapter()內容適配器的時候就把它封裝到HeaderWrapperAdapter內部,同時要添加addHeaderView()/removeHeaderView()等接口實現頭部視圖的添加移除工作。

// 支持Header和Fooer的BaseRecyclerView實現代碼
public class BaseRecyclerView extends RecyclerView {
    private List<View> mHeaders = new ArrayList<>();
    private List<View> mFooters = new ArrayList<>();
    private HeaderWrapperAdapter mWrapperAdapter;
    private Adapter mAdapter;

    @Override
    public void setAdapter(@Nullable Adapter adapter) {
        if (adapter instanceof BaseRecyclerAdapter) {
            mAdapter = adapter;
            mWrapperAdapter = new HeaderWrapperAdapter(mHeaders, 
mFooters, (BaseRecyclerAdapter) adapter);
            super.setAdapter(mWrapperAdapter);
        } else {
            super.setAdapter(adapter);
        }
    }

    public void addHeaderView(View headerView) {
        if (mWrapperAdapter != null) {
            mWrapperAdapter.notifyItemInserted(mHeaders.size() - 1);
        }
    }

    public void removeHeaderView(View headerView) {
        int index = mHeaders.indexOf(headerView);
        if (index < 0) {
            return;
        }

        mHeaders.remove(headerView);
        if (mWrapperAdapter != null) {
            mWrapperAdapter.notifyItemRemoved(index);
        }
    }
// 省略底部視圖添加、刪除代碼
}

代碼中BaseRecyclerView繼承自RecyclerView同時覆蓋了setAdapter()方法,在設置適配器時會自動將BaseRecyclerAdapter封裝到HeaderWrapperAdapter中,當用戶調用addHeaderView()方法時實際上會把HeaderView添加到HeaderWrapperAdapter中,此時只要調用notifyItemInserted()就能夠將添加的HeaderView展示出來。BaseRecyclerView通過addHeaderView() 添加下拉刷新的HeaderView後,在下拉刷新中使用canScrollVertical()判定頂部沒有捲起內容,其他的用戶事件處理與ScrollView基本相同,這樣RecyclerView就實現了下拉刷新功能。

下拉刷新組件Demo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章