Java / Android 网格视图 GridView

和 ListView 一样,GridView 也是一个 ViewGroup,它用来将各种不同的控件整合到一起,按照一个二维可以滚动的网格视图展示出来。同时也遵循 MVC 模式,依靠 Adapter 自动帮我们完成 UI 和数据的绑定。

1. GridView 的特性

GridView 在 Android App 中运用非常广泛,比如我们手机的系统相册将我们的照片及照片名称按照网格的样式排列起来,并且可以上下滚动,这种效果非常适合用 GridView 实现。
为了实现 MVC 模式,更方便的管理数据与 UI,GridView 通过 Adapter 完成数据的填充,Adapter 的使用几乎和 ListView 一样,另外系统提供了几种简单的 Adapter,具体可以参考 23 节和 24 节的内容。

2. GridView 的基本用法

GridView 和 上一节所学的 ListView 极其相似,主要还是从属性、API 及事件监听器三个方面来介绍基本用法。

2.1 GridView 的常用属性

  • android:columnWidth:
    设置网格的列宽
  • android:gravity:
    网格内各个 item 的对齐方式
  • android:horizontalSpacing:
    网格的各个 item 在水平方向上的间距
  • android:verticalSpacing:
    网格的各个 item 在垂直方向上的间距
  • android:numColumns:
    设置列数,可以直接设置距离也可以设置成“auto_fit”让系统自适应
  • android:stretchMode:
    设置网格拉伸模式,可选值如下:
    • none: 不拉伸
    • spacingWidth: 将多余空间分摊给网格的间隔空隙
    • columnWidth: 将多余空间分摊给网格的各个列
    • spacingWidthUniform: 将多余空间分摊给网格的各个列及其间隔空隙

2.2 GridView 的常用 API

  • smoothScrollByOffset:
    平滑滚动一个相对距离
  • smoothScrollToPosition:
    平滑滚动到某个位置

2.3 GridView 的事件监听器

  • setOnItemClickListener:
    设置 GridView 的 item 点击事件回调,此接口与 ListView 完全一样。

3. GridView 的使用示例

为了和 ListView 以及 Adapter 两节的内容作对比,本节依旧实现“水果列表”的例子,只不过本节会把单维列表改成二维的网格样式,代码可基于上一节直接修改。

3.1 布局文件编写

首先修改布局文件,将 ListView 替换成 GridView 并添加一些 GridView 特有的属性,如下:

<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/gridview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:columnWidth="110dp"
    android:numColumns="auto_fit"
    android:verticalSpacing="10dp"
    android:horizontalSpacing="10dp"
    android:stretchMode="columnWidth"
    android:gravity="center" />

3.2 GridView 适配器

以上属性都在 25.2.1 小节有描述,也比较好理解,接着修改 MyAdapter 类,它是继承自 BaseAdapter 实现的自定义适配器,因为二维列表更节省空间,上一节的水果数目已经没法占满一屏,这样会导致列表不能滑动,不利于体验 GridView 的效果。为此有两种解决方法:一个是我们可以手动扩展我们的列表数组,增加一些水果名称和图片;第二种方法是直接修改适配器,不断地循环从之前的列表中取水果数据。为了让大家更好的理解适配器的原理,我们采用第二种方法来扩展列表,我们需要修改两个回调方法:getCountgetView

  • getCount 表示列表的总 item 数,我们直接将水果列表长度乘以10:
     @Override
      public int getCount() {
          return mName.length * 10;
      }
    
  • getView 中我们完成 View 和数据的绑定,我们需要循环取数,所以只需要将 item 的位置对数组大小取模即可:
    @Override
      public View getView(int position, View convertView, ViewGroup parent) {
          convertView = LayoutInflater.from(mContext).inflate(R.layout.list_view, null);
          TextView name = convertView.findViewById(R.id.textView);
          ImageView image = convertView.findViewById(R.id.imageView);
    
          // item 的位置对数组长度取模,实现循环取值
          name.setText(mName[position % mName.length]);    
          image.setImageResource(mResId[position % mResId.length]);
    
          return convertView;
      }
    

完整的 MyAdapter 代码如下:

package com.emercy.myapplication;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;


public class MyAdapter extends BaseAdapter {

    private Context mContext;
    private String[] mName;
    private int[] mResId;

    public MyAdapter(Context context) {
        mContext = context;
    }

    public void setData(String[] name, int[] resId) {
        mName = name;
        mResId = resId;
    }


    @Override
    public int getCount() {
        return mName.length * 10;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView = LayoutInflater.from(mContext).inflate(R.layout.list_view, null);
        TextView name = convertView.findViewById(R.id.textView);
        ImageView image = convertView.findViewById(R.id.imageView);
        name.setText(mName[position % mName.length]);
        image.setImageResource(mResId[position % mResId.length]);
        return convertView;
    }
}

3.3 主 Activity 逻辑

Activity 的逻辑其实和 ListView 中的例子完全一样,只需要将所有的 ListView 类型改成 GridView 即可。这里体现了 MVC 设计思路的灵活性,我们想要替换一个样式其实只需要修改布局文件,主逻辑和数据层完全不需要修改,这就是前面所说的 UI 和数据解耦的强大优势。MainActivity 代码如下:

package com.emercy.myapplication;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.Toast;

public class MainActivity extends Activity {

    GridView mGridView;
    String[] mDataName = {"苹果", "梨", "香蕉", "桃子", "西瓜", "荔枝", "橘子"};
    int[] mDataImage = {R.drawable.apple, R.drawable.pear, R.drawable.banana, R.drawable.peach,
            R.drawable.watermelon, R.drawable.lychee, R.drawable.orange, R.drawable.orange};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mGridView = findViewById(R.id.gridview);

        MyAdapter adapter = new MyAdapter(this);
        adapter.setData(mDataName, mDataImage);
        mGridView.setAdapter(adapter);
        mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Toast.makeText(getApplicationContext(), mDataName[i % mDataName.length], Toast.LENGTH_LONG).show();
            }
        });
    }
}

编译之后,可以发现从一维列表变成了网格列表,水果的样式循环 10 次,效果如下:

GridView 示例

4. 小结

本节介绍了 Adapter 常用的第二个控件,它与 ListView 的用法及其相似,作为网格样式,相比 ListView 有一些二维的属性,其余的 API 和事件回调都和 ListView 完全一样。最后我们完成了和 ListView 类似的一个“水果”列表样式,通过修改 Adapter 可以达到扩展列表的效果。

这里也希望大家能够注意到 Adapter 的几个回调接口具体的含义,因为很多人虽然经常用 Adpater,但其实都是墨守成规的去实现那几个回调,没有真正的理解其内涵,这样在今后的实际开发中往往会遇到很多不可控的问题,希望大家能够做到知其然并知其所以然。