全面总结之 ListView 篇

ListView

ListView 是一个显示一列可滚动项目的视图控件,其组成部分可以大致分为:数据源,Adapter 和 ListView 显示部分。

其中:

  • 数据源为 ListView 提供统一的数据维护和管理,它可以是一个数组或者一个集合甚至是数据库
  • Adapter 是沟通数据源和 ListView 显示界面的桥梁,将布局文件 inflate 出 View 对象,并将集合中的当前元素的数据填入布局文件的组件中
  • ListView 则作为显示界面,将 Adapter 解析出来的界面显示出来

事实上,我觉得 ListView 的重点并不是 ListView 本身,而是 Adapter,下面讲解一下 Adapter。

Adapter

Adapter 本身只是一个接口,它常用的实现类主要有以下几个:

  • ArrayAdapter,通常用于将数组或者集合的多个值包装成列表项
  • SimpleAdapter,用于将集合包装成列表项,功能较 ArrayAdapter 丰富
  • SimpleCursorAdapter,和 SimpleAdapter 功能类似,只是用于包装 Cursor 数据
  • BaseAdapter,通常用于被扩展,用于定制列表项

使用 ArrayAdapter
ArrayAdapter 比较简单,它只能用于显示文本,它接收三个参数:

  • 第一个是 Context
  • 第二个是每个 Item 的布局 id
  • 第三个是数据源
public class MainActivity extends Activity {

    private ListView listView;
    private String[] arr = new String[]{"天津", "上海", "北京", "深圳"};
    private ArrayAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, arr);
        listView = (ListView) findViewById(R.id.listview);
        listView.setAdapter(adapter);
    }
}

显示如下:

SimpleAdapter
名字叫 Simple 但它却并不 Simple,它可以帮我们实现更复杂的界面。SimpleAdapter 接收五个参数:

  • 第一个参数是 Context
  • 第二个参数是一个 List<? extends Map<String,?>> 集合,是数据源
  • 第三个参数是布局 id
  • 第四个参数是一个 String 数组,该参数决定提取第二个参数中哪些 key 对应的 value 来生成列表项
  • 第五个参数是一个 int 型数组,该参数决定填充哪些组件

示例:

MainActivity

public class MainActivity extends Activity {

    private ListView listView;
    private SimpleAdapter adapter;

    private String[] data = {"橘子", "苹果", "香蕉", "西瓜", "樱桃", "猕猴桃", "木瓜", "山竹",
            "桃", "葡萄", "山楂", "杏", "核桃", "哈密瓜", "菠萝", "柚子", "无花果", "柠檬", "李子"};

    private List<Map<String, Object>> dataList = new ArrayList<Map<String, Object>>();
    ;

    private String[] from = {"FruitImage", "FruitName", "FruitDesc"};
    private int[] to = {R.id.iv_fruit_img, R.id.tv_fruit_name, R.id.tv_fruit_desc};


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        listView = (ListView) findViewById(R.id.listview);
        adapter = new SimpleAdapter(MainActivity.this, getData(), R.layout.listview_item, from, to);
        listView.setAdapter(adapter);

    }


    private List<Map<String, Object>> getData() {

        for (int i = 0; i < data.length; i++) {
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("FruitImage", R.mipmap.ic_launcher);
            map.put("FruitName", data[i]);
            map.put("FruitDesc", "甜甜的,酸酸的,有营养,味道好");
            dataList.add(map);
        }
        return dataList;
    }
}

listview_item

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_fruit_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="10dp" />

    <TextView
        android:id="@+id/tv_fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/iv_fruit_img"
        android:paddingLeft="20dp"
        android:paddingTop="5dp" />

    <TextView
        android:id="@+id/tv_fruit_desc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_fruit_name"
        android:layout_toRightOf="@id/iv_fruit_img"
        android:paddingLeft="20dp"
        android:textColor="#f20404" />

</RelativeLayout>

显示如下:

SimpleCusorAdapter
是不过是数据源从 List 换成了 Cursor,用法基本类似,这里就省略了。

BaseAdapter
BaseAdapter 通常用于扩展,扩展 BaseAdapter 可以对各项列表进行最大限度的定制。

我们需要继承 BaseAdapter,然后实现其一系列方法:

  • getCount:返回 ListView 需要显示的数据量
  • getItem:返回指定索引(position)所对应的数据项
  • getItemId:返回对应的索引
  • getView:返回每一项所显示的内容

BaseAdapter 最重要的就是这个 getView 方法,该方法接收三个参数:

  • int position:ListView 需要展示很多个 Item,这个参数表示第几个
  • View convertView:这个参数就是用来重用的对象,假如我们的 ListView 有 10000 个 Item,那么展示这么多 Item 总不能 inflate 个 view 吧,那样的话,就太浪费资源了,convertView 的作用就在这里,滑出屏幕的converView就在下面新进来的item中重新使用,只是修改下他展示的值
  • ViewGroup parent:该方法返回的 View 最终被附加到的父布局

MainActivity:

public class MainActivity extends Activity {

    private ListView listView;
    private MyAdapter adapter;

    private String[] data = {"橘子", "苹果", "香蕉", "西瓜", "樱桃", "猕猴桃", "木瓜", "山竹",
            "桃", "葡萄", "山楂", "杏", "核桃", "哈密瓜", "菠萝", "柚子", "无花果", "柠檬", "李子"};

    private List<Map<String, Object>> dataList = new ArrayList<Map<String, Object>>();

    private String[] from = {"FruitImage", "FruitName", "FruitDesc"};
    private int[] to = {R.id.iv_fruit_image, R.id.tv_fruit_name, R.id.tv_fruit_desc};


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        listView = (ListView) findViewById(R.id.listview);
        adapter = new MyAdapter(this, getData());
        listView.setAdapter(adapter);

    }


    private List<Map<String, Object>> getData() {

        for (int i = 0; i < data.length; i++) {
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("FruitImage",R.mipmap.ic_launcher);
            map.put("FruitName", data[i]);
            map.put("FruitDesc","酸酸的,甜甜的,有营养,味道好");
            dataList.add(map);
        }
        return dataList;
    }
}

MyAdapter

public class MyAdapter extends BaseAdapter {

    private List<Map<String, Object>> list;
    private LayoutInflater inflater;

    public MyAdapter(Context context, List<Map<String, Object>> list) {
        this.list = list;
        inflater = LayoutInflater.from(context);
    }

    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View view = inflater.inflate(R.layout.listview_item, null);

        ImageView fruitImage = (ImageView) view.findViewById(R.id.iv_fruit_image);
        TextView fruitName = (TextView) view.findViewById(R.id.tv_fruit_name);
        TextView fruitDesc = (TextView) view.findViewById(R.id.tv_fruit_desc);

        fruitImage.setImageResource((int)list.get(position).get("FruitImage"));
        fruitName.setText((String) list.get(position).get("FruitName"));
        fruitDesc.setText((String) list.get(position).get("FruitDesc"));

        return view;
    }
}

listview_item

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_fruit_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="30dp"
        android:layout_marginTop="5dp" />

    <TextView
        android:id="@+id/tv_fruit_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_toRightOf="@id/iv_fruit_image"/>

    <TextView
        android:id="@+id/tv_fruit_desc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_fruit_name"
        android:layout_toRightOf="@id/iv_fruit_image" />

</RelativeLayout>

显示就不上了,这里的显示和 SimpleAdapter 差不多,只是没有设置文字颜色。

Adapter 中的缓存
上一个例子中,我们都是直接 inflate 布局,然后再填充数据的,这种情况下,如果我们的 ListView 很多个 Item 的话,就需要执行很多次的 inflate,浪费时间耗费资源,上面讲过了 getView 方法中的 convertView 参数是用来重用的,所以我们可以在 getView 方法中判断一下,如果我们要显示的 View 是否存在,如果不存在,才创建,如果存在,就直接返回,这样就避免了重复创建的麻烦,所以上面的 getView 方法可以这样修改一下:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if(convertView == null){
            convertView = inflater.inflate(R.layout.listview_item,null);
        }

        ImageView fruitImage = (ImageView) convertView.findViewById(R.id.iv_fruit_image);
        TextView fruitName = (TextView) convertView.findViewById(R.id.tv_fruit_name);
        TextView fruitDesc = (TextView) convertView.findViewById(R.id.tv_fruit_desc);

        fruitImage.setImageResource((int) list.get(position).get("FruitImage"));
        fruitName.setText((String) list.get(position).get("FruitName"));
        fruitDesc.setText((String) list.get(position).get("FruitDesc"));

        return convertView;
    }

这样就避免了重复创建 view,但是还有一个问题,每个 Item 的显示,还需要每次执行 findViewById,这个也是十分耗费资源的,这里就可以使用 View 对象的 setTag 和 getTag 方法了,我们创建一个新对象,其属性就是我们 View 要展示的内容,然后使用 setTag 将 View 和 这个新对象关联起来,假如 convertView 为空,则创建 View,并设置 tag,如果不为空,则直接通过 getTag 来取出这个对象,然后通过这个对象去设置展示的内容值。

所以 getView 方法还可以这样修改一下:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        FruitItem item = null;

        if(convertView == null){
            item = new FruitItem();
            convertView = inflater.inflate(R.layout.listview_item,null);
            item.fruitImage = (ImageView) convertView.findViewById(R.id.iv_fruit_image);
            item.fruitName = (TextView) convertView.findViewById(R.id.tv_fruit_name);
            item.fruitDesc = (TextView) convertView.findViewById(R.id.tv_fruit_desc);
            convertView.setTag(item);
        }else{
            item = (FruitItem) convertView.getTag();
        }

        item.fruitImage.setImageResource((int)list.get(position).get("FruitImage"));
        item.fruitName.setText((String)list.get(position).get("FruitName"));
        item.fruitDesc.setText((String)list.get(position).get("FruitDesc"));


        return convertView;
    }

我们创建的缓存对象:

class FruitItem{
    ImageView fruitImage;
    TextView fruitName;
    TextView fruitDesc;
}

ListView 的其他方法和属性

点击监听
监听 ListView 的 Item 的点击,只需要调用 ListView 的 setOnItemClickListener 和 setOnItemLongClickListener,前者监听普通的 Item 点击事件,后者处理 Item 的长按事件。

滑动监听

设置 Item 分割线
为 ListView 设置 Item 分割线和给布局设置分割线一样,都是使用 android:dividerandroid:dividerHeight 两个属性,当 divider 属性为 @null 的时候,分割线会变成透明的

隐藏 ListView 的滚动条
默认的 ListView 滚动的时候,屏幕右侧会显示滚动条,将 android:scrollbars 属性设置为 node,会隐藏右侧的滚动条。

设置 ListView 显示第几项
ListView 默认从第一个 Item 开始显示,通过调用 ListView 对象的 setSelection 方法,可以设置从第几项显示

动态更新 ListView
当 ListView 中的数据发生变化的时候,我们不用重新为 ListView 设置 Adapter,那样的话太没有效率,我们仅仅需要调用 adapter 的 notifyDataSetChanged() 方法即可。

调用 notifyDataSetChanged 方法需要注意的是,传入 Adapter 的数据 List 是同一个 List 对象,否则将无法实现更新。

public class MainActivity extends Activity {

    private ListView listView;
    private List<String> data = new ArrayList<String>();
    private Button bt;
    private ArrayAdapter adapter;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        data.add("橘子");
        data.add("苹果");
        data.add("香蕉");
        data.add("西瓜");
        data.add("樱桃");
        data.add("猕猴桃");

        listView = (ListView) findViewById(R.id.listview);
        bt = (Button) findViewById(R.id.bt);
        adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, data);
        listView.setAdapter(adapter);

        bt.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                data.add("柚子");
                adapter.notifyDataSetChanged();
            }
        });
    }
}

获取 ListView 的子 Item
ListView 包含若干个 Item,自然也就有相应的有关子 Item 的方法了两个获取相关子 Item 的方法:

  • getChildAt:获取指定索引的子 Item
  • getChildCount:获取屏幕显示的子 Item 的数量

需要注意的是,getChildCount 方法获取到的只是显示在屏幕上的 Item 的数量,如果要获取全部的 Item,需要调用 ListView 的 getCount 方法。