自定義LayoutManager的詳解及其使用
RecyclerView不斷的普及,越來越多的人使用來代替傳統的ListView,GridView等,為了跟進時代也要不斷的學習RecyclerView的相關知識,下面就來了解一下RecyclerView的LayoutManger。
Recycler
RecyclerView內部有一個Recycler,顧名思義它就是一個回收的工具,當定義LayoutManager時,它可以訪問到一個Recycler的例項,從而用於來回收或者獲取View。當需要一個新的view時,呼叫getViewForPosition()這個方法,它會返回一個View,這個View可能是之前Recycler回收的View再利用,也可能是一個新的View。
兩級快取機制
Scrap和Recycle
在RecyclerView中有兩級快取機制:Scrap和Recycle。
Scrap Heap(垃圾堆)是一個輕量的集合,View不會經過介面卡而是直接返回給LayoutManager,當需要一個View時首先回去Scrap快取裡面找有沒有所需要的View,而這裡面的View已經綁定了需要的資料所以無需適配直接使用。
Recycle Pool(回收池)這裡面回收的View如果再次使用需要重新經過介面卡繫結資料,即呼叫onBindViewHolder()進行繫結資料,當然如果Recycle Pool裡面也沒有View就只有重新建立View。
Detach和Remove
我們可以通過Detach和Remove決定把View快取在Recycle或者Scrap。
使用Detach是把View快取在Scrap,這種快取方式可以方便如果還需要把快取的View新增進來的場景,可以明顯提高效率,可以呼叫detachAndScrapView()方法來實現。
Remove就是把View移除掉,放到Recycle裡面,以備後面的再次利用,呼叫方法removeAndRecycleView()實現。
具體採取何種方法還是要根據你具體的需求來呼叫。
下面寫個具體例子來實現如下效果
當然做法就是寫一個類來繼承RecyclerView.LayoutManager
首先看看幾個重要的方法
generateDefaultLayoutParams()
這是一個必須重寫的方法,當然僅僅實現這個方法不行,雖然能編譯通過。這個方法是給RecyclerView的子View建立一個預設的LayoutParams,實現起來也十分簡單。
onLayoutChildren
這個方法顯然是用於放置子view的位置,十分重要的一個方法。
canScrollVertically()和canScrollHorizontally()
若想要RecyclerView能水平或者豎直滾動這兩個方法需要重寫返回true
scrollVerticallyBy()和scrollHorizontallyBy()
在水平或者豎直滾動時會分別呼叫這兩個方法,dx,dy代表每次的增長值,返回值是真實移動的距離
下面貼出程式碼
package com.lzy.lzy_layoutmanager;
import android.graphics.Rect;
import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by lzy on 2016/10/18.
*/
public class NyLayoutManager extends RecyclerView.LayoutManager {
private static final String TAG = "lzy";
//儲存所有item的偏移資訊
private SparseArrayCompat<Rect> itemFrames = new SparseArrayCompat<>();
//總的高度和寬度
private int mTotalHeight;
private int mTotalWidth;
private int verticalOffset;//豎直方向的偏移
private int horizontalOffset;//水平方向的偏移
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() <= 0) {
return;
}
detachAndScrapAttachedViews(recycler);
int totalHeight = 0;
int totalWidth = 0;
int offsetX = 0;
int offsetY = 0;
//計算每個item的位置資訊,儲存在itemFrames裡面
for (int i = 0; i < getItemCount(); i++) {
//從快取中取出
View view = recycler.getViewForPosition(i);
//新增到RecyclerView中
addView(view);
//測量
measureChildWithMargins(view, 0, 0);
//獲取測量後的寬高
int height = getDecoratedMeasuredHeight(view);
int width = getDecoratedMeasuredWidth(view);
//把每一個子View的寬高加起來獲得總的
totalHeight += height;
totalWidth += width;
//邊界資訊儲存到Rect裡面
Rect rect = itemFrames.get(i);
if (rect == null) {
rect = new Rect();
}
rect.set(offsetX, offsetY, offsetX + width, offsetY + height);
itemFrames.put(i, rect);
//橫豎方向的偏移
offsetX += width;
offsetY += height;
}
mTotalHeight = Math.max(totalHeight, getVerticalSpace());
mTotalWidth = Math.max(totalWidth, getHorizontalSpace());
fill(recycler, state);
}
//回收不必要的view(超出螢幕的),取出需要的顯示出來
private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
//獲得螢幕的邊界資訊
Rect displayFrame = new Rect(horizontalOffset, verticalOffset, horizontalOffset + getHorizontalSpace(),
verticalOffset + getVerticalSpace());
//滑出螢幕回收到快取中
Rect childFrame = new Rect();
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
childFrame.left = getDecoratedLeft(view);
childFrame.top = getDecoratedTop(view);
childFrame.right = getDecoratedRight(view);
childFrame.bottom = getDecoratedBottom(view);
//判斷是否在顯示區域裡面
if (!Rect.intersects(displayFrame, childFrame)) {
removeAndRecycleView(view, recycler);
}
}
//在螢幕上顯示出
for (int i = 0; i < getItemCount(); i++) {
if (Rect.intersects(displayFrame, itemFrames.get(i))) {//判斷是否在螢幕中
View view = recycler.getViewForPosition(i);
measureChildWithMargins(view, 0, 0);
addView(view);
Rect rect = itemFrames.get(i);
layoutDecorated(view, rect.left - horizontalOffset, rect.top - verticalOffset,
rect.right - horizontalOffset, rect.bottom - verticalOffset);
}
}
}
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public boolean canScrollHorizontally() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
if (verticalOffset + dy < 0) {//滑動到最頂部
dy = -verticalOffset;
} else if (verticalOffset + dy > mTotalHeight - getVerticalSpace()) {//滑動到底部
dy = mTotalHeight - getVerticalSpace() - verticalOffset;
}
offsetChildrenVertical(-dy);
fill(recycler, state);
verticalOffset += dy;
return dy;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
if (horizontalOffset + dx < 0) {//滑動到最左邊
dx = -horizontalOffset;
} else if (horizontalOffset + dx > mTotalWidth - getHorizontalSpace()) {//滑動到最右邊
dx = mTotalWidth - getHorizontalSpace() - horizontalOffset;
}
offsetChildrenHorizontal(-dx);
fill(recycler, state);
horizontalOffset += dx;
return dx;
}
//獲取控制元件的豎直高度
private int getVerticalSpace() {
return getHeight() - getPaddingBottom() - getPaddingTop();
}
//獲取控制元件的水平寬度
private int getHorizontalSpace() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
}
程式碼中都有註釋就不多說了。所以我們通過自定義的LayoutManager就可以實現各種我們所想要的效果了!
要實現自定義LayoutManager,首先實現generateDefaultLayoutParams()方法給child新增預設的LayoutParams,在onLayoutChildren這個方法裡面首先detach掉介面上的view快取到scrap裡面,然後重新進行佈局,呼叫getViewForPosition取出快取的view,新增到RecyclerView裡面並測量,接著把它的邊界資訊設定為我們所想要的樣子,通過layoutDecorated()方法把需要顯示的子view佈局到介面上。
如果需要滑動,把canScrollVertically()和canScrollHorizontally()按需返回true,重寫scrollVerticallyBy()或者scrollHorizontallyBy()方法,這兩個方法需要返回真實的偏移距離,返回的dx或者dy可能並不是真實的移動距離,因為當滑動到邊緣的時候真實移動距離可能就不是dx或者dy,所以在這個時候需要判斷處理,移動就呼叫offsetChildrenHorizontal()或者offsetChildrenVertical()實現,然後重新佈局到介面上就可以實現。