1.縱向Layoutmanager(VerticalLayoutManager)
- 先寫一個類繼承Layoutmanager,默認要實現generateDefaultLayoutParams方法,一般沒有要修改itemview布局參數的話,默認就按下面來寫
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
2.如果只重寫這個方法的話,我們運行起來會發現什么都沒有顯示,這是因為所有的itemView的布局都是在onLayoutChildren方法中,所以我們要重寫這個方法來布局所有的itemview
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return;
}
//擺放可見的itemview
int temp = 0;
for (int i = 0; i < getItemCount(); i++) {
View child = recycler.getViewForPosition(i);
measureChildWithMargins(child, 0, 0);
int itemWidth = getDecoratedMeasuredWidth(child);
int itemHeight = getDecoratedMeasuredHeight(child);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, getPaddingLeft(), temp, itemWidth, temp + itemHeight);
temp += itemHeight;
}
mTotalHeight = Math.max(getVerticalSpace(), temp);
}
- getItemCount()==0的話代表數據為0我們直接return
- 遍歷RecycleView所有的條目
- 每個條目的寬高都一樣,所以每個條目的left和right一樣,只需要將每個條目的高度累加就可以將所有條目縱向排列起來
-
必須先測量再拿寬高否則拿不到
1.jpg
3.你會發現此時不可以上下滑動,想要上下滑動還需要重寫下面二個方法
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
offsetChildrenVertical(-dy);
return dy;
}
- canScrollVertically方法返回ture代表可以豎直滑動
- scrollVerticallyBy方法中 dy>0代表上滑,dy<0代表下滑
- 用offsetChildrenVertical方法來移動所有的item
4.你會發現itemview可以拖出屏幕之外,所以要對拖動范圍進行限制,上滑不能超出RecycleView高度,下滑不能滑動到0以下
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return dy;
}
int travel = dy;
//限定拖動范圍
if (mTotalMoveY + travel < 0) {
travel = -mTotalMoveY;
} else if (mTotalMoveY + travel > mTotalHeight - getVerticalSpace()) {
travel = mTotalHeight - getVerticalSpace() - mTotalMoveY;
}
mTotalMoveY += travel;
offsetChildrenVertical(-travel);
return travel;
}
2.縱向復用(VerticalLayoutManagerRecycled)
1.上面效果看上去和LinerLayoutManager沒有什么區別,但是我們在Adapter的onCreateViewHolder和onBindViewHolder方法中打印Log會發現有多少條目就同時調用了多少次onCreateViewHolder和onBindViewHolder方法,onCreateViewHolder執行一次代表創建了一個itemview,這就代表沒有復用itemview,在數據量大的情況下就會發生anr異常
2.復用用到的幾個重要的方法:
public void detachAndScrapAttachedViews(Recycler recycler)
僅用于onLayoutChildren中,在布局前,將所有在顯示的HolderView從RecyclerView中剝離,將其放在mAttachedScrap中,以供重新布局時使用View view = recycler.getViewForPosition(position)
用于向RecyclerView申請一個HolderView,這個HolderView是從緩存池子拿的,正是這個函數能為我們實現復用。removeAndRecycleView(child, recycler)
這個函數僅用于滾動的時候,在滾動時,我們需要把滾出屏幕的HolderView標記為Removed,這個函數的作用就是把已經不需要的HolderView標記為Removed。在我們標記為Removed以為,會把這個HolderView移到mCachedViews中,如果mCachedViews已滿,就利用先進先出原則,將mCachedViews中老的holderView移到mRecyclerPool中,然后再把新的HolderView加入到mCachedViews中。int getPosition(View view)
這個函數用于得到某個View在Adapter中的索引位置,我們經常將它與getChildAt(int position)聯合使用,得到某個當前屏幕上在顯示的View在Adapter中的位置
3.具體代碼
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return;
}
detachAndScrapAttachedViews(recycler);
View view = recycler.getViewForPosition(0);
measureChildWithMargins(view, 0, 0);
int itemWidth = getDecoratedMeasuredWidth(view);
int itemHeight = getDecoratedMeasuredHeight(view);
//屏幕上可見的itemView個數
int visible = getVerticalSpace() / itemHeight;
int temp = 0;
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(getPaddingLeft(), temp, itemWidth, itemHeight + temp);
mRectArray.put(i, rect);
temp += itemHeight;
}
//擺放可見的itemview
for (int i = 0; i < visible; i++) {
Rect rect = mRectArray.get(i);
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top, rect.right, rect.bottom);
}
mTotalHeight = Math.max(getVerticalSpace(), temp);
}
- 先調用detachAndScrapAttachedViews方法把所有可見的itemview剝離
- 定義一個集合存儲Rect,每個Rect都記錄了每個itemView的位置
- 一屏中能放幾個item就獲取幾個HolderView,撐滿初始化的一屏即可,不要多創建,visible 代表可見的itemView個數
//dy>0 ?? dy<0??
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return dy;
}
int travel = dy;
if (travel + mTotalMoveY < 0) {
travel = -mTotalMoveY;
} else if (travel + mTotalMoveY > mTotalHeight - getVerticalSpace()) {
travel = mTotalHeight - getVerticalSpace() - mTotalMoveY;
}
//回收不在屏幕內的itemview
for (int i = getChildCount()-1; i >=0; i--) {
View view = getChildAt(i);
//下滑回收上面移出屏幕的itemview
if (getDecoratedBottom(view) - travel < 0) {
removeAndRecycleView(view, recycler);
//上滑回收下面移出屏幕的itemview
} else if (getDecoratedTop(view) - travel > getVerticalSpace()) {
removeAndRecycleView(view, recycler);
}
}
//獲取當前可見的屏幕區域
Rect visibleRect = getVisibleRect(travel);
//移動時將回收的itemview從緩存中取出來
if (travel > 0) {
//上滑即將出來的條目的索引
int next = getPosition(getChildAt(getChildCount() - 1)) + 1;
for (int i = next; i < getItemCount(); i++) {
insertView(recycler, visibleRect, i, false);
}
} else {
//下滑即將出來的條目的索引
int last = getPosition(getChildAt(0)) - 1;
for (int i = last; i >= 0; i--) {
insertView(recycler, visibleRect, i, true);
}
}
mTotalMoveY += travel;
offsetChildrenVertical(-travel);
return travel;
}
private void insertView(RecyclerView.Recycler recycler, Rect visibleRect, int pos, boolean flag) {
Rect rect = mRectArray.get(pos);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(pos);
if (flag) {
addView(child, 0);
} else {
addView(child);
}
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mTotalMoveY,
rect.right, rect.bottom - mTotalMoveY);
}
}
private int getVerticalSpace() {
return getHeight() - getPaddingBottom() - getPaddingTop();
}
private Rect getVisibleRect(int travel) {
return new Rect(getPaddingLeft(), mTotalMoveY + travel,
getWidth(), mTotalMoveY + travel + getVerticalSpace());
}
- 回收不在屏幕內的itemview中遍歷屏幕內的itemview,getDecoratedBottom(view) - travel < 0代表上滑屏幕內的第一個itemview即將<0超出屏幕所以remove掉
- getDecoratedTop(view) - travel > getVerticalSpace()代表下滑屏幕內的最后一個itemview即將超出屏幕所以remove掉
- int next = getPosition(getChildAt(getChildCount() - 1)) + 1代表上滑時即將出現的條目,從這開始遍歷把即將出現的條目都添加進來
-
int last = getPosition(getChildAt(0)) - 1代表下滑時即將出現的條目
2.gif - 此時打Log可以看到在調用了幾次onCreateViewHolder以后都是調用onBindViewHolder代表實現了復用
12-19 11:57:51.996 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+49
12-19 11:57:51.997 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+50
12-19 11:57:51.998 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+51
12-19 11:57:52.012 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+52
12-19 11:57:52.013 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+53
12-19 11:57:52.013 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+54
12-19 11:57:52.029 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+55
12-19 11:57:52.030 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+56
12-19 11:57:52.030 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+57
12-19 11:57:52.045 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+58
12-19 11:57:52.046 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+59
12-19 11:57:52.048 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+60
12-19 11:57:52.061 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+61
12-19 11:57:52.062 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+62
12-19 11:57:52.062 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+63
12-19 11:57:52.078 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+64
12-19 11:57:52.079 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+65
3.縱向Layoutmanager帶動畫(VerticalLayoutManagerAnim)
1.使用offsetChildrenVertical(-travel)函數來移動屏幕中所有item。這種方法僅適用于每個item,在移動時,沒有特殊效果的情況,當我們在移動item時,同時需要改變item的角度、透明度等情況時,單純使用offsetChildrenVertical(-travel)來移是不行的。針對這種情況,我們就只有使用第二種方法來實現回收復用了。
2.我們主要替換掉移動item所用的offsetChildrenVertical(-travel);函數,既然要將它棄用,那我們就只能自己布局每個item了。
3.具體代碼
- 定義一個集合來存儲已經布局的itemview的position
//是否在當前屏幕的itemView
private SparseBooleanArray mAttachItems = new SparseBooleanArray();
- 在onLayoutChildren中進行默認全部為false即沒有進行過布局
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(getPaddingLeft(), temp, itemWidth, itemHeight + temp);
mRectArray.put(i, rect);
mAttachItems.put(i, false);
temp += itemHeight;
}
//dy>0 ?? dy<0??
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return dy;
}
int travel = dy;
if (travel + mTotalMoveY < 0) {
travel = -mTotalMoveY;
} else if (travel + mTotalMoveY > mTotalHeight - getVerticalSpace()) {
travel = mTotalHeight - getVerticalSpace() - mTotalMoveY;
}
mTotalMoveY += travel;
//獲取當前可見的屏幕區域
Rect visibleRect = getVisibleRect();
//回收不在屏幕內的itemview
for (int i = getChildCount()-1; i >= 0; i--) {
View view = getChildAt(i);
int position = getPosition(view);
Rect rect = mRectArray.get(position);
if (Rect.intersects(visibleRect, rect)) {
layoutDecoratedWithMargins(view, rect.left, rect.top - mTotalMoveY, rect.right, rect.bottom - mTotalMoveY);
mAttachItems.put(position, true);
view.setRotationY(view.getRotationY() + 1);
} else {
removeAndRecycleView(view, recycler);
mAttachItems.put(position, false);
}
}
int next = getPosition(getChildAt(getChildCount() - 1));
int last = getPosition(getChildAt(0));
//移動時將回收的itemview從緩存中取出來
if (travel > 0) {
//上滑即將出來的條目的索引
for (int i = next; i < getItemCount(); i++) {
insertView(recycler, visibleRect, i, false);
}
} else {
//下滑即將出來的條目的索引
for (int i = last; i >= 0; i--) {
insertView(recycler, visibleRect, i, true);
}
}
return travel;
}
private void insertView(RecyclerView.Recycler recycler, Rect visibleRect, int pos, boolean flag) {
Rect rect = mRectArray.get(pos);
if (Rect.intersects(visibleRect, rect) && !mAttachItems.get(pos)) {
View child = recycler.getViewForPosition(pos);
if (flag) {
addView(child, 0);
} else {
addView(child);
}
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mTotalMoveY,
rect.right, rect.bottom - mTotalMoveY);
child.setRotationY(child.getRotationY() + 1);
mAttachItems.put(pos, true);
}
}
- 我們先進行對當前屏幕的itemview進行遍歷如果在屏幕內直接布局否則就remove掉然后把它們的狀態都存起來,在屏幕內的我們給它設置了一個 view.setRotationY(view.getRotationY() + 1)動畫
-
下面同樣進行即將出現的條目進行添加 if (Rect.intersects(visibleRect, rect) && !mAttachItems.get(pos)) ,如果在屏幕內并且沒有進行過布局的就add進來,然后進行狀態存儲和設置動畫
3.gif
4.橫向滑動HorizontalLayoutmanager與流失布局效果FlowLayoutmanager
-
復用未帶動畫
4.gif
6.gif
-復用帶動畫
5.gif
7.gif - 橫向的LayoutManager與縱向的大體一致,只不過布局擺放有所不同,這里不在贅述
- FlowLayoutManager復用和滑動都和VerticalLayoutManager的scrollVerticallyBy方法一致只是onLayoutChildren有所不同
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
return;
}
detachAndScrapAttachedViews(recycler);
int tempHeight = mDefaultMargin;
int tempWidth = mDefaultMargin;
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int itemWidth = getDecoratedMeasuredWidth(view);
int itemHeight = getDecoratedMeasuredHeight(view);
//超出屏幕寬度換行
if (tempWidth + itemWidth >= getHorizontalSpace()) {
tempWidth = mDefaultMargin;
tempHeight += itemHeight + mDefaultMargin;
}
layoutDecoratedWithMargins(view, tempWidth, tempHeight, itemWidth + tempWidth, tempHeight + itemHeight);
tempWidth += itemWidth + mDefaultMargin;
}
mTotalHeight = Math.max(tempHeight, getVerticalSpace());
}
- 遍歷所有的item如果寬度加起來超出屏幕寬度換行,height累加,width重置這樣就可以實現效果
源碼地址https://github.com/digtal/recycleview-study
本篇內容參考于https://blog.csdn.net/harvic880925/article/details/84979161