深度剖析!RecyclerView 的 LayoutManager 原理大揭秘
一、RecyclerView 与 LayoutManager 概述
1.1 RecyclerView 简介
RecyclerView 是 Android 提供的一个强大的视图组件,用于在有限的屏幕空间内高效展示大量数据。它替代了传统的 ListView 和 GridView,提供了更灵活的布局方式和更好的性能优化。RecyclerView 通过使用 ViewHolder 模式,减少了视图的创建和销毁次数,从而提高了滚动性能。以下是一个简单的 RecyclerView 使用示例:
java
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private MyAdapter adapter;
private List<String> dataList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化数据列表
dataList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
dataList.add("Item " + i);
}
// 找到 RecyclerView 控件
recyclerView = findViewById(R.id.recyclerView);
// 设置 LayoutManager,这里使用线性布局管理器
recyclerView.setLayoutManager(new LinearLayoutManager(this));
// 创建适配器并设置给 RecyclerView
adapter = new MyAdapter(dataList);
recyclerView.setAdapter(adapter);
}
// 自定义适配器类
private static class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
private List<String> data;
public MyAdapter(List<String> data) {
this.data = data;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 加载布局文件
View view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
return new MyViewHolder(view);
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
// 绑定数据到视图
holder.textView.setText(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
// 自定义 ViewHolder 类
static class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
public MyViewHolder(View itemView) {
super(itemView);
// 找到 TextView 控件
textView = itemView.findViewById(android.R.id.text1);
}
}
}
}
在上述代码中,我们创建了一个包含 20 个数据项的列表,并使用 RecyclerView 进行展示。通过设置 LinearLayoutManager 作为布局管理器,将数据以线性列表的形式展示出来。
1.2 LayoutManager 的作用
LayoutManager 是 RecyclerView 的核心组件之一,它负责 RecyclerView 中 item 视图的布局和排列方式。不同的 LayoutManager 可以实现不同的布局效果,如线性布局、网格布局、瀑布流布局等。LayoutManager 还负责处理滚动事件,决定哪些 item 视图需要显示在屏幕上,哪些需要被回收。以下是 LayoutManager 的主要作用:
- 布局 item 视图:根据不同的布局规则,将 item 视图排列在 RecyclerView 中。
- 管理 item 视图的显示和隐藏:根据滚动事件,动态显示和隐藏 item 视图。
- 处理滚动事件:响应滚动操作,计算滚动距离,更新 item 视图的位置。
1.3 常见的 LayoutManager 类型
RecyclerView 提供了几种常见的 LayoutManager 类型,每种类型都有其独特的布局效果:
- LinearLayoutManager:线性布局管理器,支持水平和垂直方向的线性排列。
- GridLayoutManager:网格布局管理器,将 item 视图以网格的形式排列。
- StaggeredGridLayoutManager:瀑布流布局管理器,支持不规则的网格布局,每个 item 的高度可以不同。
以下是使用不同 LayoutManager 的示例代码:
java
// 使用 LinearLayoutManager 进行垂直排列
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
// 使用 LinearLayoutManager 进行水平排列
recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
// 使用 GridLayoutManager 进行 3 列的网格布局
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
// 使用 StaggeredGridLayoutManager 进行 2 列的瀑布流布局
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
通过设置不同的 LayoutManager,我们可以轻松实现各种不同的布局效果。
二、LayoutManager 的基本工作流程
2.1 初始化过程
当我们为 RecyclerView 设置 LayoutManager 时,会触发 LayoutManager 的初始化过程。以下是 RecyclerView 的 setLayoutManager
方法的源码:
java
/**
* 设置 RecyclerView 的布局管理器
* @param layout 要设置的布局管理器
*/
public void setLayoutManager(LayoutManager layout) {
if (layout == mLayout) {
return;
}
// 停止当前的布局管理器
stopScroll();
if (mLayout != null) {
// 清除当前布局管理器的状态
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler);
mLayout.onDetachedFromWindow(this, mRecycler);
mLayout.setRecyclerView(null);
} else {
// 若之前没有布局管理器,回收所有视图
mRecycler.clear();
}
// 设置新的布局管理器
mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout + " is already attached to a RecyclerView: " + layout.mRecyclerView);
}
// 将 RecyclerView 与新的布局管理器关联
layout.setRecyclerView(this);
layout.onAttachedToWindow(this);
}
// 标记需要重新布局
mLayoutRequested = true;
requestLayout();
}
在 setLayoutManager
方法中,首先会停止当前的滚动操作,然后清除当前布局管理器的状态。接着,设置新的布局管理器,并将其与 RecyclerView 关联。最后,标记需要重新布局,并调用 requestLayout
方法触发布局过程。
2.2 布局过程
布局过程是 LayoutManager 的核心功能之一,它负责将 item 视图排列在 RecyclerView 中。布局过程主要分为以下几个步骤:
- 测量 item 视图:确定每个 item 视图的大小。
- 定位 item 视图:根据布局规则,确定每个 item 视图的位置。
- 添加 item 视图:将测量和定位好的 item 视图添加到 RecyclerView 中。
以下是 LayoutManager 的 onLayoutChildren
方法的源码,该方法是布局过程的入口:
java
/**
* 布局 RecyclerView 的子视图
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param state 当前 RecyclerView 的状态
*/
public abstract void onLayoutChildren(Recycler recycler, State state);
onLayoutChildren
方法是一个抽象方法,具体的布局逻辑由不同的 LayoutManager 实现。例如,LinearLayoutManager 的 onLayoutChildren
方法实现如下:
java
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 如果 RecyclerView 没有数据或者正在执行动画,直接返回
if (state.isPreLayout()) {
return;
}
// 分离并回收所有视图
detachAndScrapAttachedViews(recycler);
// 确定布局方向
ensureLayoutState();
// 计算可用空间
int startOffset = mOrientationHelper.getStartAfterPadding();
int endOffset = mOrientationHelper.getEndAfterPadding();
int available = endOffset - startOffset;
mLayoutState.mAvailable = available;
mLayoutState.mOffset = startOffset;
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
// 进行布局
fill(recycler, mLayoutState, state, false);
// 重新附加和布局视图
reAttachAndLayoutScrap(recycler, state);
}
在 LinearLayoutManager 的 onLayoutChildren
方法中,首先会分离并回收所有视图,然后确定布局方向和计算可用空间。接着,调用 fill
方法进行布局,最后重新附加和布局视图。
2.3 滚动过程
滚动过程是 LayoutManager 的另一个重要功能,它负责处理 RecyclerView 的滚动事件。当用户滚动 RecyclerView 时,LayoutManager 会根据滚动距离更新 item 视图的位置,并动态显示和隐藏 item 视图。以下是 LayoutManager 的 scrollVerticallyBy
方法的源码,用于处理垂直滚动事件:
java
/**
* 处理垂直滚动事件
* @param dy 滚动的垂直距离
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param state 当前 RecyclerView 的状态
* @return 实际滚动的距离
*/
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
scrollVerticallyBy
方法会根据布局方向判断是否支持垂直滚动,如果支持,则调用 scrollBy
方法进行滚动处理。scrollBy
方法的实现如下:
java
/**
* 处理滚动事件
* @param dy 滚动的距离
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param state 当前 RecyclerView 的状态
* @return 实际滚动的距离
*/
private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
// 保存当前的布局状态
mLayoutState.mRecycle = true;
ensureLayoutState();
int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
return 0;
}
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
// 移动所有子视图
offsetChildrenVertical(-scrolled);
// 更新布局状态
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
在 scrollBy
方法中,首先会保存当前的布局状态,然后根据滚动方向和距离更新布局状态。接着,调用 fill
方法进行布局,计算实际滚动的距离。最后,移动所有子视图,并更新布局状态。
三、LinearLayoutManager 原理分析
3.1 布局方向的确定
LinearLayoutManager 支持水平和垂直两种布局方向,通过构造函数或 setOrientation
方法可以设置布局方向。以下是 LinearLayoutManager 的构造函数和 setOrientation
方法的源码:
java
/**
* 构造函数,指定上下文和布局方向
* @param context 上下文对象
* @param orientation 布局方向,LinearLayoutManager.VERTICAL 或 LinearLayoutManager.HORIZONTAL
* @param reverseLayout 是否反转布局
*/
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context);
// 设置布局方向
setOrientation(orientation);
// 设置是否反转布局
setReverseLayout(reverseLayout);
}
/**
* 设置布局方向
* @param orientation 布局方向,LinearLayoutManager.VERTICAL 或 LinearLayoutManager.HORIZONTAL
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
if (orientation == mOrientation) {
return;
}
// 记录旧的布局方向
int oldOrientation = mOrientation;
// 更新布局方向
mOrientation = orientation;
// 重置布局状态
mLayoutState = null;
mOrientationHelper = null;
if (mRecyclerView != null) {
// 重新布局
mRecyclerView.requestLayout();
}
// 发送布局方向改变的事件
dispatchLayoutDirectionChanged(oldOrientation, orientation);
}
在构造函数中,会调用 setOrientation
方法设置布局方向。在 setOrientation
方法中,会检查传入的布局方向是否合法,如果合法,则更新布局方向,并重置布局状态。最后,调用 requestLayout
方法触发重新布局。
3.2 布局过程详解
LinearLayoutManager 的布局过程主要由 onLayoutChildren
方法和 fill
方法完成。以下是 fill
方法的源码:
java
/**
* 填充 RecyclerView 的子视图
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param layoutState 当前的布局状态
* @param state 当前 RecyclerView 的状态
* @param stopOnFocusable 是否在遇到可聚焦的视图时停止填充
* @return 填充过程中消耗的空间
*/
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
// 记录可用空间
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// 处理滚动偏移
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
// 布局一个 item 视图
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null || !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// 剩余空间减去消耗的空间
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
return start - layoutState.mAvailable;
}
在 fill
方法中,会不断调用 layoutChunk
方法布局 item 视图,直到可用空间不足或没有更多的 item 视图。layoutChunk
方法的实现如下:
java
/**
* 布局一个 item 视图
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param state 当前 RecyclerView 的状态
* @param layoutState 当前的布局状态
* @param result 布局结果对象
*/
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 从 Recycler 中获取一个 item 视图
View view = layoutState.next(recycler);
if (view == null) {
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
// 添加视图到 RecyclerView
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
// 测量视图
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
// 布局视图
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
在 layoutChunk
方法中,会从 Recycler 中获取一个 item 视图,然后测量和布局该视图。根据布局方向和布局状态,确定视图的位置,并将其添加到 RecyclerView 中。
3.3 滚动过程详解
LinearLayoutManager 的滚动过程主要由 scrollBy
方法完成。在 scrollBy
方法中,会根据滚动距离更新布局状态,并调用 fill
方法进行布局。以下是 scrollBy
方法中更新布局状态的部分代码:
java
/**
* 处理滚动事件
* @param dy 滚动的距离
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param state 当前 RecyclerView 的状态
* @return 实际滚动的距离
*/
private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
// 保存当前的布局状态
mLayoutState.mRecycle = true;
ensureLayoutState();
int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
int absDy = Math.abs(dy);
// 更新布局状态
updateLayoutState(layoutDirection, absDy, true, state);
int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
return 0;
}
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
// 移动所有子视图
offsetChildrenVertical(-scrolled);
// 更新布局状态
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
/**
* 更新布局状态
* @param layoutDirection 布局方向,LayoutState.LAYOUT_START 或 LayoutState.LAYOUT_END
* @param requiredSpace 需要的空间
* @param canUseExistingSpace 是否可以使用现有的空间
* @param state 当前 RecyclerView 的状态
*/
private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) {
mLayoutState.mLayoutDirection = layoutDirection;
int scrollingOffset;
if (layoutDirection == LayoutState.LAYOUT_END) {
View child = getChildClosestToEnd();
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
} else {
View child = getChildClosestToStart();
mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
scrollingOffset = mOrientationHelper.getStartAfterPadding() - mOrientationHelper.getDecoratedStart(child);
}
mLayoutState.mAvailable = requiredSpace;
if (canUseExistingSpace) {
mLayoutState.mAvailable -= scrollingOffset;
}
mLayoutState.mScrollingOffset = scrollingOffset;
mLayoutState.mItemDirection = layoutDirection == LayoutState.LAYOUT_END ? mShouldReverseLayout ? -1 : 1 : mShouldReverseLayout ? 1 : -1;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
mLayoutState.mCurrentScrap = null;
}
在 updateLayoutState
方法中,会根据滚动方向和需要的空间更新布局状态。首先,确定布局方向和滚动偏移量,然后更新可用空间和当前位置。最后,根据布局方向和是否反转布局,确定 item 视图的排列方向。
四、GridLayoutManager 原理分析
4.1 网格布局的基本概念
GridLayoutManager 用于将 item 视图以网格的形式排列。它通过设置列数或行数来确定网格的布局方式。以下是一个使用 GridLayoutManager 的示例代码:
java
// 创建一个 3 列的网格布局管理器
GridLayoutManager layoutManager = new GridLayoutManager(this, 3);
recyclerView.setLayoutManager(layoutManager);
在上述代码中,我们创建了一个 3 列的网格布局管理器,并将其设置给 RecyclerView。
4.2 布局过程详解
GridLayoutManager 的布局过程与 LinearLayoutManager 类似,但需要考虑网格的列数和行数。以下是 GridLayoutManager 的 onLayoutChildren
方法的源码:
java
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.isPreLayout()) {
return;
}
// 分离并回收所有视图
detachAndScrapAttachedViews(recycler);
// 初始化布局状态
mLayoutState = new LayoutState();
mLayoutState.mAvailable = mOrientationHelper.getTotalSpace();
mLayoutState.mOffset = mOrientationHelper.getStartAfterPadding();
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
mLayoutState.mCurrentPosition = 0;
mLayoutState.mItemDirection = 1;
mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
// 计算布局
calculateLayout(recycler, state);
// 布局子视图
layoutChunk(recycler, state, mLayoutState, new LayoutChunkResult());
// 重新附加和布局视图
reAttachAndLayoutScrap(recycler, state);
}
/**
* 计算布局
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param state 当前 RecyclerView 的状态
*/
private void calculateLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 计算列数
int spanCount = getSpanCount();
// 计算行数
int itemCount = state.getItemCount();
int rowCount = (itemCount + spanCount - 1) / spanCount;
// 初始化布局信息
mSpanLookup.reset();
mSpanLookup.setSpanIndexCacheEnabled(false);
mSpanLookup.setSpanCount(spanCount);
mSpanLookup.setTotalCount(itemCount);
mSpanLookup.setSpanSizeLookup(mSpanSizeLookup);
mSpanLookup.updateSpanAssignments();
// 计算每个 item 视图的位置
for (int i = 0; i < itemCount; i++) {
int spanIndex = mSpanLookup.getSpanIndex(i, spanCount);
int rowIndex = i / spanCount;
// 记录 item 视图的位置信息
mSpanLookup.setSpanGroupIndex(i, rowIndex);
}
}
/**
* 布局一个 item 视图
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param state 当前 RecyclerView 的状态
* @param layoutState 当前的布局状态
* @param result 布局结果对象
*/
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
int spanCount = getSpanCount();
int currentSpan = 0;
while (currentSpan < spanCount && layoutState.hasMore(state)) {
// 从 Recycler 中获取一个 item 视图
View view = layoutState.next(recycler);
if (view == null) {
result.mFinished = true;
break;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
int spanSize = mSpanSizeLookup.getSpanSize(layoutState.mCurrentPosition);
if (currentSpan + spanSize > spanCount) {
// 如果当前列放不下该 item 视图,跳过
layoutState.mCurrentPosition++;
continue;
}
if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
// 添加视图到 RecyclerView
addView(view);
} else {
addView(view, 0);
}
// 测量视图
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight() - currentSpan * mOrientationHelper.getDecoratedMeasurementInOther(view);
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft() + currentSpan * mOrientationHelper.getDecoratedMeasurementInOther(view);
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
top = getPaddingTop() + currentSpan * mOrientationHelper.getDecoratedMeasurementInOther(view);
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
// 布局视图
layoutDecoratedWithMargins(view, left, top, right, bottom);
currentSpan += spanSize;
layoutState.mCurrentPosition++;
}
}
在 onLayoutChildren
方法中,首先会分离并回收所有视图,然后初始化布局状态。接着,调用 calculateLayout
方法计算布局,确定每个 item 视图的位置。最后,调用 layoutChunk
方法布局子视图。
4.3 滚动过程详解
GridLayoutManager 的滚动过程与 LinearLayoutManager 类似,也是通过 scrollBy
方法处理滚动事件。以下是 GridLayoutManager 的 scrollBy
方法的源码:
java
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
/**
* 处理滚动事件
* @param dy 滚动的距离
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param state 当前 RecyclerView 的状态
* @return 实际滚动的距离
*/
private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
// 保存当前的布局状态
mLayoutState.mRecycle = true;
ensureLayoutState();
int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
int absDy = Math.abs(dy);
// 更新布局状态
updateLayoutState(layoutDirection, absDy, true, state);
int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
return 0;
}
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
// 移动所有子视图
offsetChildrenVertical(-scrolled);
// 更新布局状态
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
/**
* 更新布局状态
* @param layoutDirection 布局方向,LayoutState.LAYOUT_START 或 LayoutState.LAYOUT_END
* @param requiredSpace 需要的空间
* @param canUseExistingSpace 是否可以使用现有的空间
* @param state 当前 RecyclerView 的状态
*/
private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) {
mLayoutState.mLayoutDirection = layoutDirection;
int scrollingOffset;
if (layoutDirection == LayoutState.LAYOUT_END) {
View child = getChildClosestToEnd();
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
} else {
View child = getChildClosestToStart();
mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
scrollingOffset = mOrientationHelper.getStartAfterPadding() - mOrientationHelper.getDecoratedStart(child);
}
mLayoutState.mAvailable = requiredSpace;
if (canUseExistingSpace) {
mLayoutState.mAvailable -= scrollingOffset;
}
mLayoutState.mScrollingOffset = scrollingOffset;
mLayoutState.mItemDirection = layoutDirection == LayoutState.LAYOUT_END ? mShouldReverseLayout ? -1 : 1 : mShouldReverseLayout ? 1 : -1;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
mLayoutState.mCurrentScrap = null;
}
在 scrollBy
方法中,会根据滚动距离更新布局状态,并调用 fill
方法进行布局。更新布局状态的逻辑与 LinearLayoutManager 相同。
五、StaggeredGridLayoutManager 原理分析
5.1 瀑布流布局的特点
StaggeredGridLayoutManager 用于实现瀑布流布局,其特点是每个 item 视图的高度可以不同,形成不规则的网格布局。以下是一个使用 StaggeredGridLayoutManager 的示例代码:
java
// 创建一个 2 列的瀑布流布局管理器
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
在上述代码中,我们创建了一个 2 列的瀑布流布局管理器,并将其设置给 RecyclerView。
5.2 布局过程详解
StaggeredGridLayoutManager 的布局过程比 LinearLayoutManager 和 GridLayoutManager 更复杂,需要考虑每个 item 视图的高度和列的剩余空间。以下是 StaggeredGridLayoutManager 的 onLayoutChildren
方法的源码:
java
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.isPreLayout()) {
return;
}
// 分离并回收所有视图
detachAndScrapAttachedViews(recycler);
// 初始化布局状态
mLayoutState = new LayoutState();
mLayoutState.mAvailable = mOrientationHelper.getTotalSpace();
mLayoutState.mOffset = mOrientationHelper.getStartAfterPadding();
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
mLayoutState.mCurrentPosition = 0;
mLayoutState.mItemDirection = 1;
mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
// 初始化列信息
mSpanCount = getSpanCount();
mColumnHeights = new int[mSpanCount];
Arrays.fill(mColumnHeights, mOrientationHelper.getStartAfterPadding());
// 布局子视图
fill(recycler, mLayoutState, state, false);
// 重新附加和布局视图
reAttachAndLayoutScrap(recycler, state);
}
/**
* 填充 RecyclerView 的子视图
* @param recycler 用于获取和回收视图的 Recycler 对象
* @param layoutState 当前的布局状态
* @param state 当前 RecyclerView 的状态
* @param stopOnFocusable 是否在遇到可聚焦的视图时停止填充
* @return 填充过程中消耗的空间
*/
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
// 记录可用空间
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// 处理滚动偏移
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
// 布局一个 item 视图
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if
java
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
// 寻找最优的列来放置当前 item
int selectedColumn = findMinColumn();
layoutState.mCurrentSpan = selectedColumn;
// 布局一个 item 视图
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
break;
}
// 更新对应列的高度
mColumnHeights[selectedColumn] += layoutChunkResult.mConsumed;
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null ||!state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
return start - layoutState.mAvailable;
上述代码中,fill
方法在布局过程中,每次通过 findMinColumn
方法找到当前剩余空间最小(高度最低)的列,将 item 视图放置在该列。findMinColumn
方法实现如下:
java
private int findMinColumn() {
int minColumn = 0;
for (int i = 1; i < mSpanCount; i++) {
if (mColumnHeights[i] < mColumnHeights[minColumn]) {
minColumn = i;
}
}
return minColumn;
}
findMinColumn
方法遍历所有列的高度数组 mColumnHeights
,找到高度最低的列索引并返回。
java
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 从 Recycler 中获取一个 item 视图
View view = layoutState.next(recycler);
if (view == null) {
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
// 添加视图到 RecyclerView
addView(view);
} else {
addView(view, 0);
}
// 测量视图
measureChildWithMargins(view, 0, 0);
int measuredWidth = getDecoratedMeasuredWidth(view);
int measuredHeight = getDecoratedMeasuredHeight(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
int column = layoutState.mCurrentSpan;
left = getPaddingLeft() + column * (measuredWidth + mHorizontalSpacing);
right = left + measuredWidth;
top = mColumnHeights[column];
bottom = top + measuredHeight;
// 更新列的高度
mColumnHeights[column] = bottom;
} else {
// 水平方向布局逻辑(类似垂直方向,此处省略)
}
// 布局视图
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
result.mConsumed = measuredHeight;
}
在 layoutChunk
方法中,获取视图后先进行测量,然后根据布局方向(以垂直方向为例),结合当前列的索引 layoutState.mCurrentSpan
计算视图的 left
、top
、right
、bottom
位置。计算时会考虑列间距 mHorizontalSpacing
,确定位置后调用 layoutDecoratedWithMargins
完成视图布局,并更新对应列的高度 mColumnHeights[column]
。
5.3 滚动过程详解
StaggeredGridLayoutManager 的滚动处理与其他 LayoutManager 有相似之处,但由于其瀑布流特性,在回收和复用视图时需要更细致地处理各列状态。
java
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
if (getChildCount() == 0 || dy == 0) {
return 0;
}
// 保存当前的布局状态
mLayoutState.mRecycle = true;
ensureLayoutState();
int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
int absDy = Math.abs(dy);
// 更新布局状态
updateLayoutState(layoutDirection, absDy, true, state);
int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
return 0;
}
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
// 移动所有子视图
offsetChildrenVertical(-scrolled);
// 更新各列高度以反映滚动后的状态
updateColumnsAfterScroll(scrolled);
// 更新布局状态
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
在 scrollVerticallyBy
方法中,调用 updateColumnsAfterScroll
方法来更新各列高度,以适应滚动后的布局情况:
java
private void updateColumnsAfterScroll(int scrolled) {
for (int i = 0; i < mSpanCount; i++) {
mColumnHeights[i] -= scrolled;
if (mColumnHeights[i] < mOrientationHelper.getStartAfterPadding()) {
mColumnHeights[i] = mOrientationHelper.getStartAfterPadding();
}
}
}
updateColumnsAfterScroll
方法遍历所有列的高度数组 mColumnHeights
,将每列高度减去滚动距离 scrolled
,并确保列高度不会低于 RecyclerView 的起始填充位置 mOrientationHelper.getStartAfterPadding()
。
在滚动过程中,当视图滚出屏幕时,StaggeredGridLayoutManager 会将其回收。回收逻辑在 recycleByLayoutState
方法中实现:
java
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
final int childCount = getChildCount();
if (childCount == 0) {
return;
}
// 根据滚动方向确定回收视图的范围
int start = layoutState.mLayoutDirection == LayoutState.LAYOUT_START? 0 : childCount - 1;
int end = layoutState.mLayoutDirection == LayoutState.LAYOUT_START? childCount : -1;
int step = layoutState.mLayoutDirection == LayoutState.LAYOUT_START? 1 : -1;
for (int i = start; i != end; i += step) {
View view = getChildAt(i);
if (view != null) {
// 判断视图是否在当前可见区域外
if (isViewOutsideBounds(view, layoutState)) {
removeAndRecycleView(view, recycler);
}
}
}
}
recycleByLayoutState
方法根据滚动方向确定从 RecyclerView 的头部还是尾部开始遍历子视图,通过 isViewOutsideBounds
方法判断视图是否在可见区域外:
java
private boolean isViewOutsideBounds(View view, LayoutState layoutState) {
if (mOrientation == VERTICAL) {
int viewTop = mOrientationHelper.getDecoratedStart(view);
int viewBottom = mOrientationHelper.getDecoratedEnd(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
return viewBottom < layoutState.mOffset;
} else {
return viewTop > layoutState.mOffset;
}
} else {
// 水平方向判断逻辑(类似垂直方向,此处省略)
}
}
isViewOutsideBounds
方法通过比较视图的顶部或底部位置与当前滚动偏移量 layoutState.mOffset
,判断视图是否在可见区域外,若在区域外则调用 removeAndRecycleView
方法将视图回收。
六、自定义 LayoutManager
6.1 自定义 LayoutManager 的基本步骤
自定义 LayoutManager 可以实现独特的布局效果,其基本步骤如下:
- 继承 LayoutManager 类 :创建一个新类继承自
LayoutManager
或现有的 LayoutManager 子类(如LinearLayoutManager
)。 - 重写核心方法 :主要重写
onLayoutChildren
、scrollVerticallyBy
(或scrollHorizontallyBy
)等方法,实现自定义的布局和滚动逻辑。 - 处理视图的测量、定位和回收:在重写的方法中,合理处理 item 视图的测量、位置计算以及回收复用。
6.2 示例:自定义简单的水平滚动布局管理器
java
public class CustomHorizontalLayoutManager extends LayoutManager {
private int mChildWidth;
private int mChildHeight;
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(Recycler recycler, State state) {
if (getChildCount() == 0 || state.isPreLayout()) {
return;
}
// 分离并回收所有视图
detachAndScrapAttachedViews(recycler);
int left = getPaddingLeft();
int top = getPaddingTop();
int itemCount = getItemCount();
for (int i = 0; i < itemCount; i++) {
// 从 Recycler 中获取一个 item 视图
View view = recycler.getViewForPosition(i);
addView(view);
// 测量视图
measureChildWithMargins(view, 0, 0);
mChildWidth = getDecoratedMeasuredWidth(view);
mChildHeight = getDecoratedMeasuredHeight(view);
// 布局视图
layoutDecoratedWithMargins(view, left, top, left + mChildWidth, top + mChildHeight);
left += mChildWidth;
}
}
@Override
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
if (getChildCount() == 0) {
return 0;
}
if (dx < 0) {
// 向左滚动
return scrollLeftBy(dx, recycler, state);
} else {
// 向右滚动
return scrollRightBy(dx, recycler, state);
}
}
private int scrollLeftBy(int dx, Recycler recycler, State state) {
int consumed = dx;
int left = getPaddingLeft();
int itemCount = getChildCount();
for (int i = 0; i < itemCount; i++) {
View view = getChildAt(i);
if (left + getDecoratedMeasuredWidth(view) < 0) {
// 视图完全移出屏幕,回收视图
removeAndRecycleView(view, recycler);
} else {
// 移动视图
offsetViewHorizontal(view, consumed);
}
left += getDecoratedMeasuredWidth(view);
}
// 补充新的视图
fillLeft(recycler, state);
return consumed;
}
private int scrollRightBy(int dx, Recycler recycler, State state) {
int consumed = dx;
int right = getWidth() - getPaddingRight();
int itemCount = getChildCount();
for (int i = itemCount - 1; i >= 0; i--) {
View view = getChildAt(i);
if (right - getDecoratedMeasuredWidth(view) > getWidth()) {
// 视图完全移出屏幕,回收视图
removeAndRecycleView(view, recycler);
} else {
// 移动视图
offsetViewHorizontal(view, consumed);
}
right -= getDecoratedMeasuredWidth(view);
}
// 补充新的视图
fillRight(recycler, state);
return consumed;
}
private void fillLeft(Recycler recycler, State state) {
int left = getPaddingLeft();
while (left < 0) {
int position = getPosition(getChildAt(0)) - 1;
if (position < 0) {
break;
}
// 从 Recycler 中获取一个 item 视图
View view = recycler.getViewForPosition(position);
addView(view, 0);
// 测量视图
measureChildWithMargins(view, 0, 0);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
// 布局视图
layoutDecoratedWithMargins(view, left, getPaddingTop(), left + viewWidth, getPaddingTop() + viewHeight);
left += viewWidth;
}
}
private void fillRight(Recycler recycler, State state) {
int right = getWidth() - getPaddingRight();
int itemCount = getItemCount();
while (right < getWidth()) {
int position = getPosition(getChildAt(itemCount - 1)) + 1;
if (position >= getItemCount()) {
break;
}
// 从 Recycler 中获取一个 item 视图
View view = recycler.getViewForPosition(position);
addView(view);
// 测量视图
measureChildWithMargins(view, 0, 0);
int viewWidth = getDecoratedMeasuredWidth(view);
int viewHeight = getDecoratedMeasuredHeight(view);
// 布局视图
layoutDecoratedWithMargins(view, right, getPaddingTop(), right + viewWidth, getPaddingTop() + viewHeight);
right += viewWidth;
}
}
}
在上述自定义 CustomHorizontalLayoutManager
中,generateDefaultLayoutParams
方法生成默认的布局参数;onLayoutChildren
方法实现了初始布局逻辑,将所有 item 视图水平排列;scrollHorizontallyBy
方法根据滚动方向调用 scrollLeftBy
或 scrollRightBy
处理滚动,滚动过程中判断视图是否移出屏幕并进行回收,同时通过 fillLeft
和 fillRight
方法补充新的视图。
七、总结与展望
7.1 总结
RecyclerView 的 LayoutManager 是实现高效、灵活布局的关键组件。不同类型的 LayoutManager(如 LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager)通过各自独特的布局和滚动逻辑,满足了多样化的 UI 需求 。
- 布局过程:所有 LayoutManager 都遵循相似的流程,先分离和回收视图,然后通过测量、定位和添加视图完成布局。不同之处在于具体的计算逻辑,如 LinearLayoutManager 按线性顺序排列,GridLayoutManager 考虑网格行列,StaggeredGridLayoutManager 则基于各列高度动态布局。
- 滚动过程:滚动时,LayoutManager 会根据滚动距离更新布局状态,判断并回收移出屏幕的视图,同时补充新的视图。在这个过程中,通过复用视图减少了视图创建和销毁的开销,提升了 RecyclerView 的性能。
- 自定义扩展:开发者可以通过继承 LayoutManager 类,重写核心方法来自定义布局效果,进一步拓展了 RecyclerView 的应用场景。
7.2 展望
随着 Android 开发技术的不断发展,LayoutManager 也可能迎来更多创新:
- 性能优化:未来可能会出现更高效的布局和滚动算法,进一步减少内存占用和计算开销,提升 RecyclerView 在复杂场景下的性能表现。
- 功能扩展:支持更多样化的布局效果,如 3D 布局、动态变形布局等,满足开发者日益增长的创意需求。
- 与新技术结合:结合 Jetpack Compose 等新的 UI 开发技术,提供更简洁、直观的布局方式,降低开发成本和学习曲线。