9、网格布局实战:用 GridLayoutManager 实现 2 列网格

聊完了线性布局,咱们来看看网格布局。说实话,网格在移动端太常见了——商品列表、相册、应用图标,清一色的网格。我个人觉得,掌握 GridLayoutManager 是 RecyclerView 进阶的必经之路。

9.1 GridLayoutManager 是什么?

GridLayoutManager 是 RecyclerView 自带的布局管理器。它把列表分成若干列,每个 item 占一列。说白了,就是把你手机屏幕横着切几刀,每个格子放一个 item。

它的构造方法很简单:

GridLayoutManager(context, spanCount: Int)
GridLayoutManager(context, spanCount: Int, orientation: Int, reverseLayout: Boolean)
  • spanCount:列数。2 就是两列,3 就是三列。
  • orientation:方向。VERTICAL 是纵向滚动,HORIZONTAL 是横向滚动。
  • reverseLayout:是否反向布局。一般用 false。
小提示:orientation 默认是 VERTICAL。如果你用 HORIZONTAL,那列就变成行了,item 会横向排列。我在项目中很少用横向网格,除非是做横向滚动的标签列表。

9.2 两列网格的完整实现

咱们直接上代码。假设我们要展示一个商品列表,每个商品有图片和名称。

9.2.1 布局文件

先写 item 布局。注意,网格的 item 宽度会被 GridLayoutManager 自动平分,所以不用写死宽高。

<!-- item_product.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="8dp">

    <ImageView
        android:id="@+id/ivProduct"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:scaleType="centerCrop" />

    <TextView
        android:id="@+id/tvName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:textSize="14sp"
        android:maxLines="2"
        android:ellipsize="end" />

</LinearLayout>

9.2.2 适配器

适配器和 LinearLayoutManager 时一模一样,不需要任何改动。这就是 RecyclerView 的优雅之处——布局管理器只管布局,适配器只管数据绑定。

class ProductAdapter(
    private val productList: List<Product>
) : RecyclerView.Adapter<ProductAdapter.ProductViewHolder>() {

    class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val ivProduct: ImageView = itemView.findViewById(R.id.ivProduct)
        val tvName: TextView = itemView.findViewById(R.id.tvName)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_product, parent, false)
        return ProductViewHolder(view)
    }

    override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
        val product = productList[position]
        holder.tvName.text = product.name
        // 图片加载用 Glide 或 Coil,这里省略
    }

    override fun getItemCount() = productList.size
}

9.2.3 在 Activity 或 Fragment 中设置

关键就在这一步。设置 GridLayoutManager,指定列数为 2。

class ProductActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_product)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        val adapter = ProductAdapter(generateMockData())

        // 设置两列网格
        val layoutManager = GridLayoutManager(this, 2)
        recyclerView.layoutManager = layoutManager
        recyclerView.adapter = adapter
    }

    private fun generateMockData(): List<Product> {
        return listOf(
            Product("商品A", R.drawable.img_a),
            Product("商品B", R.drawable.img_b),
            // ... 更多数据
        )
    }
}
核心要点:GridLayoutManager 只负责布局,不关心数据。你只需要把 LinearLayoutManager 换成 GridLayoutManager,其他代码完全不用动。

9.3 网格布局的核心逻辑

我画了一张图,帮你理解 GridLayoutManager 是怎么工作的。

GridLayoutManager 两列网格布局原理 Item 0 Item 1 Item 2 Item 3 第 1 列 第 2 列 spanCount = 2,每行放 2 个 item

你看,每个 item 的宽度是屏幕宽度除以 2。如果屏幕宽度是 360dp,那每个 item 就是 180dp 宽。高度由 item 布局自己决定。

9.4 避坑指南

我在项目中踩过不少网格的坑,分享几个给你。

坑一:item 宽度别写死
有些新手会在 item 布局里写 android:layout_width="180dp"。千万别!GridLayoutManager 会自动分配宽度,你写死了反而会出问题。用 match_parent 就好。
坑二:图片加载注意内存
网格里通常有很多图片。我曾经在一个商品列表里加载了 200 张高清图,结果 OOM 了。建议用 Glide 或 Coil,并设置合适的尺寸:
Glide.with(context).load(url).override(360, 360).into(imageView)
坑三:item 间距别用 margin
如果你在每个 item 上加 margin,你会发现两边的 item 和屏幕边缘间距不一致。我建议用 ItemDecoration 来统一处理间距,后面会专门讲。

9.5 横向网格?也可以

GridLayoutManager 支持横向滚动。你只需要把 orientation 设为 HORIZONTAL:

val layoutManager = GridLayoutManager(this, 2, HORIZONTAL, false)

这时候,spanCount 变成了行数。每行 2 个 item,横向滚动。我一般在做横向分类标签时用这个,比如「热门推荐」那一行。

9.6 总结一下

两列网格其实就这么简单。你只需要记住:

  • GridLayoutManager(context, 2) 设置两列
  • 适配器不用改,和 LinearLayoutManager 一样
  • item 宽度用 match_parent,高度自适应
  • 图片加载注意内存,间距用 ItemDecoration

嗯,网格布局的基础就这些。下一节咱们会聊更高级的——怎么让某些 item 跨列,比如第一个 item 占一整行,后面两个占一行。那个稍微有点绕,但掌握了今天的内容,你已经有基础了。

我的习惯:每次写网格布局,我都会先确认 spanCount 是不是 2 的倍数。如果是奇数,比如 3 列,那 item 宽度就是 33.33%,有时候会出现像素不对齐的问题。两列是最安全的。

好了,去试试吧。把 LinearLayoutManager 换成 GridLayoutManager,看看你的列表瞬间变成网格是什么效果。