在RecyclerView之前,对于线性布局和网格布局用的基本上是ListView和GridView,到RecyclerView,就不需要这么麻烦了,RecyclerView对于职责划分的很明确,布局相关的就只需要LayoutManager,继承LayoutManager就可以实现你想要的布局,比如android为我们提供的一下几个布局:
这些布局管理都直接或间接实现了LayoutManager,这也就是LayoutManager的主要作用:布局管理。
LayoutManager是RecyclerView的内部类,RecyclerView把它的测量和布局工作都转交给了LayoutManager,如果你想要的布局android没有给你提供,那么就可以考虑去继承LayoutManager去实现。
前面四个方法好理解,看名字应该就能知道,找到最前最后显示item的位置,其中含有Completely的方法返回的是完全可见的第一个item,而不含的则是真正的第一个item,这个item可能只显示了一部分,这个显示需要注意一点,比如使用的LinearLayoutManager布局,这个可见指的是布局方向上的可见。
onSaveInstanceState():
对于onSaveInstanceState()这个方法肯定很多人都见过,那就是在Activity中就有这个方法,当Activity异常销毁(比如由于内存过大)时就会调用到这个方法,而在LayoutManager中提供的这个方法是一个空方法,在他的子类中提供了实现,这里看下在LinearLayoutManager中的实现:
@Override
public Parcelable onSaveInstanceState() {
if (mPendingSavedState != null) {
return new SavedState(mPendingSavedState);
}
SavedState state = new SavedState();
// RecyclerView中是否含有子view,如果有就会保存子view的状态
if (getChildCount() > 0) {
ensureLayoutState();
boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout;
state.mAnchorLayoutFromEnd = didLayoutFromEnd;
//判断布局是否是倒序
if (didLayoutFromEnd) {
// 倒序布局
final View refChild = getChildClosestToEnd();
state.mAnchorOffset = mOrientationHelper.getEndAfterPadding()
- mOrientationHelper.getDecoratedEnd(refChild);
state.mAnchorPosition = getPosition(refChild);
} else {
// 顺序布局,可以看到,state中主要是保存两个变量,一个是当前显示第一个view所在的position,另一个是当前这个
// view没有显示的偏移量
final View refChild = getChildClosestToStart();
state.mAnchorPosition = getPosition(refChild);
state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild)
- mOrientationHelper.getStartAfterPadding();
}
} else {
state.invalidateAnchor();
}
return state;
}
这里可以看出主要是保存了两个变量,保存的这两个变量有什么用呢?主要是在RecyclerView被remove后再次添加进来时可以恢复上一次显示item的位置,什么意思呢?先来看下下面这个界面:
可以看到这个布局的最外层是一个RecyclerView,里面的子item还有RecyclerView,当我们往上滑的时候,子RecyclerView最终是会被reMove()掉的,子RecyclerView中的子item的显示的位置信息是不会被记录的,当再次往回滑的时候,子RecyclerView显示的总是会是第一个,如果想往回滑的时候,子RecyclerView显示的总是上次滑动到的位置,那么要怎么去是想呢?这时候就可以用到上面提到的onSaveInstanceState(),在这个方法里正好会保存RecyclerView的位置信息,当我们往回滑添加的之前被回收的RecyclerView时,那就可以将上次保存的位置信息取出来再次添加进去就可以了,思路是有了,接下来就是如何在代码中去实现了,首先是回收的时候保存信息,关于item的回收,想到的就是Adapter的onViewRecycled()方法,接下来就是去恢复它的位置信息了,我想到的就是onBindViewHolder()中可以去恢复它的位置信息,毕竟reMove()的item都会调用到这个方法(detach掉的view本身就会保留它本身的信息,不需要我们维护),下面是一些实现的关键代码:
1.确定当前的item当中是否含有RecyclerView:
RecyclerView findNestedRecyclerView(@NonNull View view) {
if (!(view instanceof ViewGroup)) {
return null;
}
if (view instanceof RecyclerView) {
return (RecyclerView) view;
}
final ViewGroup parent = (ViewGroup) view;
final int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
final View child = parent.getChildAt(i);
final RecyclerView descendant = findNestedRecyclerView(child);
if (descendant != null) {
return descendant;
}
}
return null;
}
2.回收时保存当前RecyclerView中item的位置信息:onViewRecycled()
private SparseArray rvState = new SparseArray<>();
@Override
public void onViewRecycled(@NonNull MyViewHolder holder) {
super.onViewRecycled(holder);
RecyclerView nestedRecyclerView = findNestedRecyclerView(holder.itemView);
if (nestedRecyclerView != null) {
Parcelable parcelable = nestedRecyclerView.getLayoutManager().onSaveInstanceState();
// 保存时key为当前item所处的位置position,值为子RecyclerView的子item的显示位置信息
rvState.put(holder.getAdapterPosition(),parcelable);
}
}
3.重新绑定时恢复RecyclerView中item的位置信息,onBindViewHolder():
@Override
public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, int i) {
Log.d(TAG, "onBindViewHolder: position = "+i);
// 这里type=0的item的布局中就有RecyclerView
if (getItemViewType(i) == 0) {
myViewHolder.rv.setLayoutManager(new LinearLayoutManager(TestRVActivity.this,RecyclerView.HORIZONTAL,false));
// 获取当前position之前回收时的位置信息,如果不为null,那么就恢复之前的位置信息
Parcelable parcelable = rvState.get(i);
if (parcelable != null) {
rvState.remove(i);
//
myViewHolder.rv.getLayoutManager().onRestoreInstanceState(parcelable);
}
if (myViewHolder.rv.getAdapter() == null) {
myViewHolder.rv.setAdapter(new MyChildAdapter());
}
return;
}
}
如果需要实现自定义布局,那么可以实现generateDefaultLayoutParams()和onLayoutChildren(),第二个方法主要就是给他进行布局,能不能滑动主要由canScrollHorizontally()和canScrollVertically()进行决定,返回true表示可以滑动,当然,滑动也是需要我们进行处理的,scrollHorizontallyBy()和scrollVerticallyBy()就是让我们来处理滑动的,可以在这两个方法中调用offsetChildrenHorizontal()进行滑动处理。这里看下我写的一个小demo,可以好好理解下:
public class CustomeLayoutManager extends RecyclerView.LayoutManager {
private static final String TAG = "CustomeLayoutManager";
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
@IntDef({VERTICAL, HORIZONTAL})
public @interface OrientationType {
}
@OrientationType
private int mOrientation;
private int mRows;
private int mColms;
private int mOnePageSize;
private int mMoveDistance;
private int mMaxScrollX;
private int mMaxScrollY;
private SparseArray<Rect> mItemFramsList = new SparseArray<>();
public CustomeLayoutManager(@IntRange(from = 1, to = 30) int row, @IntRange(from = 1, to = 30) int col, @OrientationType int orientation) {
this.mRows = row;
this.mColms = col;
mOnePageSize = mRows * mColms;
this.mOrientation = orientation;
}
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public boolean canScrollHorizontally() {
return mOrientation == HORIZONTAL;
}
@Override
public boolean canScrollVertically() {
return mOrientation == VERTICAL;
}
private int getWidthUse() {
int width = getWidth() - getPaddingLeft() - getPaddingRight();
return width;
}
private int getHeightUse() {
return getHeight() - getPaddingTop() - getPaddingBottom();
}
private int getItemWidth() {
return getWidthUse() / mColms;
}
private int getItemHeight() {
return getHeightUse() / mRows;
}
private Rect getItemFrameByPosition(int position) {
Rect rect = mItemFramsList.get(position);
if (rect == null) {
rect = new Rect();
int currentPage = position / mOnePageSize;
//当前页编译量
int offsetX = 0;
int offsetY = 0;
if (canScrollHorizontally()) {
offsetX = getWidthUse() * currentPage;
} else {
offsetY = getHeightUse() * currentPage;
}
int itemIndex = position % mOnePageSize;
int rowIndex = itemIndex / mColms;
int colIndex = itemIndex % mColms;
offsetX += colIndex * getItemWidth();
offsetY += rowIndex * getItemHeight();
rect.left = offsetX;
rect.top = offsetY;
rect.right = offsetX + getItemWidth();
rect.bottom = offsetY + getItemHeight();
}
return rect;
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
int i1 = getItemCount();
int pageCount = getItemCount() / mOnePageSize;
if (getItemCount() % mOnePageSize == 0) {
pageCount--;
}
Log.d(TAG, "onLayoutChildren: i1 = " + i1+" pageCount = "+pageCount+" width = "+getWidthUse());
if (canScrollHorizontally()) {
mMaxScrollX = getWidthUse() * pageCount;
} else {
mMaxScrollY = getHeightUse() * pageCount;
}
for (int i = 0; i < i1; i++) {
View childView = recycler.getViewForPosition(i);
addView(childView);
Rect rect = getItemFrameByPosition(i);
measureChildWithMargins(childView, 0, 0);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) childView.getLayoutParams();
layoutDecorated(childView, rect.left + params.leftMargin, rect.top + params.topMargin,
rect.right - params.rightMargin, rect.bottom - params.bottomMargin);
}
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
Log.d(TAG, "scrollHorizontallyBy: mMoveDistance = " + mMoveDistance + " " + dx+" mMaxScrollX = "+mMaxScrollX);
if (mMoveDistance + dx < 0) {
dx = 0 - mMoveDistance;
}
if (mMoveDistance + dx > mMaxScrollX) {
dx = mMaxScrollX - mMoveDistance;
}
mMoveDistance += dx;
offsetChildrenHorizontal(-dx);
return dx;
}
}
就写到这了,如果还想更深入的学习,可以自己花点时间在研究下源码。如果有不懂的欢迎一起讨论学习。