RecyclerView非常灵活,支持用户自定义布局,本文简单分析下500px的代码,有助于将来实现自己的LayoutManager。
GreedoLayoutManager
两个主要的类
布局类,继承RecyclerView.LayoutManager,实现真正的功能,
public GreedoLayoutManager(SizeCalculatorDelegate sizeCalculatorDelegate) {
mSizeCalculator = new GreedoLayoutSizeCalculator(sizeCalculatorDelegate);
}
尺寸计算类,负责计算每个ITEM的大小,在LayoutManager初始化时一并初始化,
public GreedoLayoutSizeCalculator(SizeCalculatorDelegate sizeCalculatorDelegate) {
//adatper的代理,用于在adapter中取得图片的宽高比
mSizeCalculatorDelegate = sizeCalculatorDelegate;
//存放每个ITEM的size,size是自定义类,里面只有宽高两个变量
mSizeForChildAtPosition = new ArrayList<>();
//存放每行ITEM中第一个ITEM的位置
mFirstChildPositionForRow = new ArrayList<>();
//存放每个ITEM对应的行数
mRowForChildPosition = new ArrayList<>();
}
简单分析实现流程
- 需要实现自己的LayoutParams
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
}
- 初始化时调用或者adapter数据变化时调用,初始化时默认从左上角开始布局,如果是adapter change导致数据变化,需要根据mForceClearOffsets来判断是否需要保留当前的ITEM偏移量。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// We have nothing to show for an empty data set but clear any existing views
if (getItemCount() == 0) {
detachAndScrapAttachedViews(recycler);
return;
}
mSizeCalculator.setContentWidth(getContentWidth());
mSizeCalculator.reset();
int initialTopOffset = 0;
if (getChildCount() == 0) { // First or empty layout
mFirstVisiblePosition = 0;
mFirstVisibleRow = 0;
} else { // Adapter data set changes
// Keep the existing initial position, and save off the current scrolled offset.
final View topChild = getChildAt(0);
if (mForceClearOffsets) {
initialTopOffset = 0;
mForceClearOffsets = false;
} else {
initialTopOffset = getDecoratedTop(topChild);
}
}
//回收当前全部ITEM
detachAndScrapAttachedViews(recycler);
//网格化布局填充
preFillGrid(Direction.NONE, 0, initialTopOffset, recycler, state);
mPendingScrollPositionOffset = 0;
}
- 布局代码,
- 首先通过firstChildPositionForRow获取第一个可视ITEM的位置,后面要根据这个ITEM开始布局。
- 如果第一个可视ITEM发生了变化,说明TOP端有一行上滑隐藏了或者下滑显示了。此时需要重新获取当前viewgroup中第一个child的显示偏移值,以便重新计算startTopOffset。
- 接下来将当前显示的全部ITEM缓存,然后detach掉,这里不是回收。detachView是非常轻量级的操作。
- while循环布局每一个ITEM,直到铺满屏幕或者处理完全部ITEM等
这里先从缓存中读取需要布局位置的ITEM,如果没有在通过recycler获取一个新的。根据当前ITEM的宽度,计算再几个ITEM之后需要换行,更新好对应的偏移量,分别是leftOffset 和topOffset 。若是缓存中获取到的ITEM,说明此ITEM滑动前也是在屏幕中显示的,所以不需要重新BIND数据,attachView就好了,同时将它从缓存中移出。若是recycler获取的ITEM,说明是从屏幕外滑进来的新ITEM,需要addView到Viewgroup中,并重新测量和布局。
分别是measureChildWithMargins和layoutDecorated。 - 最后,如果当前缓存还有ITEM,说明是滑动后被移出屏幕了,需要全部回收掉。recycler.recycleView
private int preFillGrid(Direction direction, int dy, int emptyTop,
RecyclerView.Recycler recycler, RecyclerView.State state) {
int newFirstVisiblePosition = firstChildPositionForRow(mFirstVisibleRow);
// First, detach all existing views from the layout. detachView() is a lightweight operation
// that we can use to quickly reorder views without a full add/remove.
SparseArray viewCache = new SparseArray<>(getChildCount());
int startLeftOffset = getPaddingLeft();
int startTopOffset = getPaddingTop() + emptyTop;
if (getChildCount() != 0) {
startTopOffset = getDecoratedTop(getChildAt(0));
if (mFirstVisiblePosition != newFirstVisiblePosition) {
switch (direction) {
case UP: // new row above may be shown
double previousTopRowHeight = sizeForChildAtPosition(
mFirstVisiblePosition - 1).getHeight();
startTopOffset -= previousTopRowHeight;
break;
case DOWN: // row may have gone off screen
double topRowHeight = sizeForChildAtPosition(
mFirstVisiblePosition).getHeight();
startTopOffset += topRowHeight;
break;
}
}
// Cache all views by their existing position, before updating counts
for (int i = 0; i < getChildCount(); i++) {
int position = mFirstVisiblePosition + i;
final View child = getChildAt(i);
viewCache.put(position, child);
}
// Temporarily detach all cached views. Views we still need will be added back at the proper index
for (int i = 0; i < viewCache.size(); i++) {
final View cachedView = viewCache.valueAt(i);
detachView(cachedView);
}
}
mFirstVisiblePosition = newFirstVisiblePosition;
// Next, supply the grid of items that are deemed visible. If they were previously there,
// they will simply be re-attached. New views that must be created are obtained from
// the Recycler and added.
int leftOffset = startLeftOffset;
int topOffset = startTopOffset + mPendingScrollPositionOffset;
int nextPosition = mFirstVisiblePosition;
int currentRow = 0;
while (nextPosition >= 0 && nextPosition < state.getItemCount()) {
boolean isViewCached = true;
View view = viewCache.get(nextPosition);
if (view == null) {
view = recycler.getViewForPosition(nextPosition);
isViewCached = false;
}
if (mIsFirstViewHeader && nextPosition == HEADER_POSITION) {
measureChildWithMargins(view, 0, 0);
mHeaderViewSize = new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
}
// Overflow to next row if we don't fit
Size viewSize = sizeForChildAtPosition(nextPosition);
if ((leftOffset + viewSize.getWidth()) > getContentWidth()) {
// Break if the rows limit has been hit
if (currentRow + 1 == mRowsLimit) break;
currentRow++;
leftOffset = startLeftOffset;
Size previousViewSize = sizeForChildAtPosition(nextPosition - 1);
topOffset += previousViewSize.getHeight();
}
// These next children would no longer be visible, stop here
boolean isAtEndOfContent;
switch (direction) {
case DOWN: isAtEndOfContent = topOffset >= getContentHeight() + dy; break;
default: isAtEndOfContent = topOffset >= getContentHeight(); break;
}
if (isAtEndOfContent) break;
if (isViewCached) {
// Re-attach the cached view at its new index
attachView(view);
viewCache.remove(nextPosition);
} else {
addView(view);
measureChildWithMargins(view, 0, 0);
int right = leftOffset + viewSize.getWidth();
int bottom = topOffset + viewSize.getHeight();
layoutDecorated(view, leftOffset, topOffset, right, bottom);
}
leftOffset += viewSize.getWidth();
nextPosition++;
}
// Scrap and store views that were not re-attached (no longer visible).
for (int i = 0; i < viewCache.size(); i++) {
final View removingView = viewCache.valueAt(i);
recycler.recycleView(removingView);
}
// Calculate pixels laid out during fill
int pixelsFilled = 0;
if (getChildCount() > 0) {
pixelsFilled = getChildAt(getChildCount() - 1).getBottom();
}
return pixelsFilled;
}
- 滑动支持,允许垂直滑动
public boolean canScrollVertically() {
return true;
}
- 滑动处理
- 首先根据滑动方向及滑动距离,判断是否要重新布局。比如有ITEM滑出屏幕或者滑入屏幕。这会影响到第一个可视ITEM的位置变化,mFirstVisibleRow。
- 前面已经处理完滑动后的重新布局,这里仅仅需要整体移动下全部视图即可。offsetChildrenVertical(-scrolled);
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
final View topLeftView = getChildAt(0);
final View bottomRightView = getChildAt(getChildCount() - 1);
int pixelsFilled = getContentHeight();
// TODO: Split into methods, or a switch case?
if (dy > 0) {
boolean isLastChildVisible = (mFirstVisiblePosition + getChildCount()) >= getItemCount();
if (isLastChildVisible) {
// Is at end of content
pixelsFilled = Math.max(getDecoratedBottom(bottomRightView) - getContentHeight(), 0);
} else if (getDecoratedBottom(topLeftView) - dy <= 0) {
// Top row went offscreen
mFirstVisibleRow++;
pixelsFilled = preFillGrid(Direction.DOWN, Math.abs(dy), 0, recycler, state);
} else if (getDecoratedBottom(bottomRightView) - dy < getContentHeight()) {
// New bottom row came on screen
pixelsFilled = preFillGrid(Direction.DOWN, Math.abs(dy), 0, recycler, state);
}
} else {
if (mFirstVisibleRow == 0 && getDecoratedTop(topLeftView) - dy >= 0) {
// Is scrolled to top
pixelsFilled = -getDecoratedTop(topLeftView);
} else if (getDecoratedTop(topLeftView) - dy >= 0) {
// New top row came on screen
mFirstVisibleRow--;
pixelsFilled = preFillGrid(Direction.UP, Math.abs(dy), 0, recycler, state);
} else if (getDecoratedTop(bottomRightView) - dy > getContentHeight()) {
// Bottom row went offscreen
pixelsFilled = preFillGrid(Direction.UP, Math.abs(dy), 0, recycler, state);
}
}
final int scrolled = Math.abs(dy) > pixelsFilled ? (int) Math.signum(dy) * pixelsFilled : dy;
offsetChildrenVertical(-scrolled);
// Return value determines if a boundary has been reached (for edge effects and flings). If
// returned value does not match original delta (passed in), RecyclerView will draw an
// edge effect.
return scrolled;
}
- ITEM尺寸的获取(宽和高)
- 在获取每行第一个显示ITEM时(firstChildPositionForRow),需要先计算好每个ITEM的宽高(computeChildSizesUpToPosition),才能得到一行能显示多少个。
- ITEM的高可以设置为固定(setFixedHeight),也可以只设最大高度动态判断(setMaxRowHeight)。动态判断是根据adapter中得到的每个图片的宽高比(aspectRatioForIndex)综合计算出来的。
- 高度计算,先用屏幕宽度和本行第一个ITEM的宽高比计算宽和高,如果高度超过最大高度,则加上第二个ITEM的比例,直到高度小于最大高度。确定好高度后则可以确定每个ITEM的宽度,最后一个ITEM的宽度是屏幕剩余宽度。
private void computeChildSizesUpToPosition(int lastPosition) {
if (mContentWidth == INVALID_CONTENT_WIDTH) {
throw new RuntimeException("Invalid content width. Did you forget to set it?");
}
if (mSizeCalculatorDelegate == null) {
throw new RuntimeException("Size calculator delegate is missing. Did you forget to set it?");
}
int firstUncomputedChildPosition = mSizeForChildAtPosition.size();
int row = mRowForChildPosition.size() > 0
? mRowForChildPosition.get(mRowForChildPosition.size() - 1) + 1 : 0;
double currentRowAspectRatio = 0.0;
List itemAspectRatios = new ArrayList<>();
int currentRowHeight = mIsFixedHeight ? mMaxRowHeight : Integer.MAX_VALUE;
int currentRowWidth = 0;
int pos = firstUncomputedChildPosition;
while (pos <= lastPosition || (mIsFixedHeight ? currentRowWidth <= mContentWidth : currentRowHeight > mMaxRowHeight)) {
double posAspectRatio = mSizeCalculatorDelegate.aspectRatioForIndex(pos);
currentRowAspectRatio += posAspectRatio;
itemAspectRatios.add(posAspectRatio);
currentRowWidth = calculateWidth(currentRowHeight, currentRowAspectRatio);
if (!mIsFixedHeight) {
currentRowHeight = calculateHeight(mContentWidth, currentRowAspectRatio);
}
boolean isRowFull = mIsFixedHeight ? currentRowWidth > mContentWidth : currentRowHeight <= mMaxRowHeight;
if (isRowFull) {
int rowChildCount = itemAspectRatios.size();
mFirstChildPositionForRow.add(pos - rowChildCount + 1);
int[] itemSlacks = new int[rowChildCount];
if (mIsFixedHeight) {
itemSlacks = distributeRowSlack(currentRowWidth, rowChildCount, itemAspectRatios);
if (!hasValidItemSlacks(itemSlacks, itemAspectRatios)) {
int lastItemWidth = calculateWidth(currentRowHeight,
itemAspectRatios.get(itemAspectRatios.size() - 1));
currentRowWidth -= lastItemWidth;
rowChildCount -= 1;
itemAspectRatios.remove(itemAspectRatios.size() - 1);
itemSlacks = distributeRowSlack(currentRowWidth, rowChildCount, itemAspectRatios);
}
}
int availableSpace = mContentWidth;
for (int i = 0; i < rowChildCount; i++) {
int itemWidth = calculateWidth(currentRowHeight, itemAspectRatios.get(i)) - itemSlacks[i];
itemWidth = Math.min(availableSpace, itemWidth);
mSizeForChildAtPosition.add(new Size(itemWidth, currentRowHeight));
mRowForChildPosition.add(row);
availableSpace -= itemWidth;
}
itemAspectRatios.clear();
currentRowAspectRatio = 0.0;
row++;
}
pos++;
}
}
- 500px还支持了scrollToPosition函数
public void scrollToPosition(int position) {
if (position >= getItemCount()) {
Log.w(TAG, String.format("Cannot scroll to %d, item count is %d", position, getItemCount()));
return;
}
// Scrolling can only be performed once the layout knows its own sizing
// so defer the scrolling request after the postLayout pass
if (mSizeCalculator.getContentWidth() <= 0) {
mPendingScrollPosition = position;
return;
}
mForceClearOffsets = true; // Ignore current scroll offset
mFirstVisibleRow = rowForChildPosition(position);
mFirstVisiblePosition = firstChildPositionForRow(mFirstVisibleRow);
requestLayout();
}
小结
500px实现的layoutmanager简洁明了,滑动流畅,基本可以根据它来实现想要的各种自定义layout效果。