GridView 是 Android 提供的一个网格列表控件,继承自 AbsListView,能够按照固定列数在垂直方向上排列子项。九宫格菜单本质上是一个三行三列的等宽网格,使用 GridView 实现可以省去手工计算入口坐标的麻烦,只需要配置好列数、间距和适配器,就能快速得到一个可滚动的菜单区域。

GridView 基础配置与九宫格结构
九宫格菜单最常见的布局参数是固定三列,每一列宽度自动等分。布局文件中通过 android:numColumns 设置列数为 3,再配合 android:stretchMode 让列宽自动拉伸,就能保证不同屏幕宽度下每列基本等宽。verticalSpacing 和 horizontalSpacing 分别控制行间距与列间距,padding 用于设置整个网格与父容器边缘的留白。
listSelector 属性建议设置为透明,否则点击条目时会出现系统默认的橙色背景,破坏菜单的整体视觉效果。下面是一个基础配置示例,根节点通常使用垂直方向的 LinearLayout,GridView 的高度可以根据需求设置为 match_parent 或 wrap_content。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<GridView
android:id="@+id/grid_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:numColumns="3"
android:verticalSpacing="8dp"
android:horizontalSpacing="8dp"
android:padding="12dp"
android:stretchMode="columnWidth"
android:listSelector="@android:color/transparent" />
</LinearLayout>
如果只配置 numColumns 而不设置 stretchMode,GridView 默认会按列宽值排列,某些屏幕下可能无法填满整行。stretchMode 设置为 columnWidth 后,多余空间会平均分配给每一列,这是九宫格菜单最常用的做法。对于固定九宫格需求,不建议使用 auto_fit 模式,因为 auto_fit 会根据 columnWidth 动态计算列数,屏幕较宽时可能出现四列甚至五列,破坏三行三列的布局结构。
适配器与数据模型设计
九宫格菜单至少需要展示图标和文字两个信息,因此先定义一个简单的数据模型类 MenuItem。模型里包含图标资源 id 和标题字符串,构造方法负责初始化这两个字段。实际项目中还可以扩展跳转类型、角标数量等字段,但核心字段保持简洁即可。
public class MenuItem {
private int iconRes;
private String title;
public MenuItem(int iconRes, String title) {
this.iconRes = iconRes;
this.title = title;
}
public int getIconRes() {
return iconRes;
}
public String getTitle() {
return title;
}
}
接下来是 item 布局文件。每个九宫格入口通常由上方图标和下方文字组成,根节点可以使用垂直方向的 LinearLayout,ImageView 负责显示图标,TextView 负责显示标题。图标建议使用固定 dp 值,例如 48dp,文字大小控制在 12sp 到 14sp,这样在多数屏幕密度下都不会显得拥挤。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<ImageView
android:id="@+id/iv_menu_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="菜单图标" />
<TextView
android:id="@+id/tv_menu_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textColor="#333333"
android:textSize="13sp" />
</LinearLayout>
适配器使用 BaseAdapter 实现,通过 LayoutInflater 加载 item 布局,并在 getView 方法中使用 ViewHolder 复用视图,避免快速滚动时频繁 findViewById 造成的性能浪费。数据集使用 List 保存多个 MenuItem,getCount 返回列表大小,getItem 返回对应位置的数据对象。
public class GridMenuAdapter extends BaseAdapter {
private Context context;
private List<MenuItem> data;
public GridMenuAdapter(Context context, List<MenuItem> data) {
this.context = context;
this.data = data;
}
@Override
public int getCount() {
return data == null ? 0 : data.size();
}
@Override
public Object getItem(int position) {
return data.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_grid_menu, parent, false);
holder = new ViewHolder();
holder.icon = convertView.findViewById(R.id.iv_menu_icon);
holder.title = convertView.findViewById(R.id.tv_menu_title);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
MenuItem item = data.get(position);
holder.icon.setImageResource(item.getIconRes());
holder.title.setText(item.getTitle());
return convertView;
}
static class ViewHolder {
ImageView icon;
TextView title;
}
}
在实际项目中,九宫格菜单的数据往往来自接口配置,而不是在 Activity 里写死。此时可以在 Adapter 里增加 setData 方法,刷新数据后调用 notifyDataSetChanged,实现动态菜单。图标也可以通过图片加载框架加载网络图片,但需要提前设置占位图,避免图标区域出现空白。
屏幕适配与间距细节优化
九宫格菜单的列间距和行间距直接影响视觉效果。如果有 8dp 的 horizontalSpacing,三列之间会有两段间距,再加上左右 padding,这些数值相加后需要小于屏幕总宽度,否则会出现列宽为 0 或条目被压缩的情况。一般情况下,间距总和控制在屏幕宽度的 10% 以内比较合理。
如果设计师给出了固定列宽要求,也可以通过 android:columnWidth 指定每列最小宽度,但要注意固定三列时,实际列宽由 GridView 根据可用空间重新计算。比如屏幕宽度为 360dp,左右 padding 各 12dp,列间距各 8dp,那么每列可用宽度大约为 (360 - 24 - 16) / 3,约等于 106dp。这个计算过程由 GridView 自动完成,只需保证 stretchMode 设置正确即可。
对于图标和文字的比例,不建议把 ImageView 的高度设置成 match_parent,否则文字可能被挤出屏幕。更好的做法是让图标使用固定 dp,文字使用 wrap_content,item 整体高度由内容撑开。如果希望九宫格整体高度固定,可以给 item 根节点设置固定高度,但必须考虑不同屏幕密度下的显示效果,最好结合 dimens 资源适配。
点击事件与嵌套滚动注意事项
GridView 通过 setOnItemClickListener 监听条目点击,回调中的 position 与数据集合下标一致,直接取出对应 MenuItem 即可执行页面跳转或功能调用。如果菜单项数量不是 9 个,比如只有 5 个,GridView 也会按照三列排成两行,剩余位置留空,这在实际业务中很常见。
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
MenuItem item = (MenuItem) parent.getAdapter().getItem(position);
Toast.makeText(MainActivity.this, "点击了" + item.getTitle(), Toast.LENGTH_SHORT).show();
}
});
当 GridView 嵌套在 ScrollView 内部时,会出现只显示第一行的问题。这是因为 GridView 继承自 AbsListView,默认会按滚动内容测量高度,而 ScrollView 给子控件的高度约束是无限大,导致 GridView 无法正确展开。解决办法是自定义一个 GridView,重写 onMeasure 方法,将高度测量模式改为 AT_MOST,让 GridView 根据实际内容高度来测量。
public class NoScrollGridView extends GridView {
public NoScrollGridView(Context context) {
super(context);
}
public NoScrollGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
不过九宫格菜单通常放在页面顶部,本身高度固定,如果外层容器不是 ScrollView,直接使用普通 GridView 即可。只有当菜单和其他内容需要一起滚动时,才需要上面的 NoScrollGridView。使用时把布局文件中的 GridView 标签替换为自定义类全路径,或者直接用自定义控件包一层。
综合来看,GridView 实现九宫格菜单的核心步骤并不复杂:固定三列,配置好间距,编写 BaseAdapter 绑定图标和文字,再处理点击事件即可。遇到嵌套滚动时重写测量方法,遇到动态数据时更新集合并刷新,就能得到一个稳定的九宫格入口。