深度剖析!RecyclerView 的 LayoutManager 原理大揭秘

深度剖析!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 中。布局过程主要分为以下几个步骤:

  1. 测量 item 视图:确定每个 item 视图的大小。
  2. 定位 item 视图:根据布局规则,确定每个 item 视图的位置。
  3. 添加 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 计算视图的 lefttoprightbottom 位置。计算时会考虑列间距 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 可以实现独特的布局效果,其基本步骤如下:

  1. 继承 LayoutManager 类 :创建一个新类继承自 LayoutManager 或现有的 LayoutManager 子类(如 LinearLayoutManager)。
  2. 重写核心方法 :主要重写 onLayoutChildrenscrollVerticallyBy(或 scrollHorizontallyBy)等方法,实现自定义的布局和滚动逻辑。
  3. 处理视图的测量、定位和回收:在重写的方法中,合理处理 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 方法根据滚动方向调用 scrollLeftByscrollRightBy 处理滚动,滚动过程中判断视图是否移出屏幕并进行回收,同时通过 fillLeftfillRight 方法补充新的视图。

七、总结与展望

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 开发技术,提供更简洁、直观的布局方式,降低开发成本和学习曲线。
相关推荐
恋猫de小郭19 分钟前
Flutter Widget IDE 预览新进展,开始推进落地发布
android·前端·flutter
南客先生1 小时前
马架构的Netty、MQTT、CoAP面试之旅
java·mqtt·面试·netty·coap
百锦再1 小时前
Java与Kotlin在Android开发中的全面对比分析
android·java·google·kotlin·app·效率·趋势
Ya-Jun5 小时前
常用第三方库:flutter_boost混合开发
android·flutter·ios
_一条咸鱼_7 小时前
深度剖析:Android NestedScrollView 惯性滑动原理大揭秘
android·面试·android jetpack
_一条咸鱼_7 小时前
深度揭秘!Android NestedScrollView 绘制原理全解析
android·面试·android jetpack
_一条咸鱼_7 小时前
揭秘 Android CoordinatorLayout:从源码深度解析其协同工作原理
android·面试·android jetpack
_一条咸鱼_7 小时前
揭秘 Android View 的 TranslationY 位移原理:源码深度剖析
android·面试·android jetpack
_一条咸鱼_7 小时前
揭秘 Android NestedScrollView 滑动原理:源码深度剖析
android·面试·android jetpack
_一条咸鱼_7 小时前
深度揭秘:Android NestedScrollView 拖动原理全解析
android·面试·android jetpack