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 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()实现,然后重新布局到界面上就可以实现。
Demo下载