Java / Android 适配器 Adapter

Android 适配器 Adapter

本节将会引入一个全新的概念——适配器,这个名字很形象,和电源适配器的功能类似,从程序设计的角度出发,它可以将不同类型、不同结构的数据适配到一起。
在 Android 中,适配器是 UI 组件和数据之间的桥梁,它帮助我们将数据填充到 UI 组件当中,实现了一个典型的 MVC 模式。我们可以分别编写独立的 UI 样式和数据模型,至于数据如何与 UI 组件绑定都由 Adapter 帮我们完成,这样的好处就是做到 UI 和数据的解耦。Android 系统为我们提供了多种 Adapter,今天就来介绍几种常见同场景下 Adapter 的基本用法。

1. 为什么要用 Adapter

我们首先看看 Android 为什么要引入 Adapter,也就是使用 Adapter 有哪些好处?
在 Android 中Adapter 通常是搭配列表控件使用,我们先看看在没有学习 Adapter 的时候,如何实现一个列表样式,我们可能需要以下几步:

  1. 创建一个 ScrollView(上一节刚学到的,不熟悉的可以参照 22 节);
  2. 在 ScrollView 中放置多个 View / ViewGroup,比如 TextView;
  3. 获取每个 TextView 实例,根据业务需求为 TextView 设置 Text;
  4. 编写额外代码管理所有的 TextView,并且需要分辨点击事件发生在第几行从而定位到相应的 TextView,从而相应列表的点击事件。

读到这里,脑海里已经有实现思路了吗?即使你能捋清思路,代码也很难写的优雅,因为编写 TextView 样式的这些 UI 代码一定会和 TextView 内容的数据代码耦合在一起,这样如何 UI 样式一变,数据也需要做很大的调整,后期的维护成本是相当高的。最好的办法就是能够有一套逻辑专门去管理数据和 UI 代码的绑定关系,用它来将 UI、Data 隔离开,提高代码的简洁性和可维护性。
我们结合一张图来理解一下 Adapter:

Adapter in Android

电源适配器将电器和电源接口适配到一起,好处是可以让手机等电子产品及家用电器厂商在生产过程中完全不需要考虑用户电源接口的类型,可以是 220V 交流电、也可以是 USB 接口,适配工作只需要交给相应的 Adapter 就可以完成。而 Android 适配器是将数据和 UI 适配到一起,好处同样也是我们在做 UI 的时候,完全不用考虑未来填充的数据是什么样的,只需要针对不同的数据类型提供一个 Adapter 即可。

如果你觉得上面的描述都太抽象,后面可以通过几个简单的例子来直观感受一下 Adapter 的用法。

2. Adapter 的类型

就像电源适配器需要根据不同的电源接口类型提供不同的适配器一样,Android 中我们需要根据不同数据类型提供不同的 Adapter,系统已经为我们实现了几种 Adapter:

  1. BaseAdapter:
    所有 Adapter 的基类,通常我们需要实现自定义 Adapter 时,需要实现此抽象类,在实际开发中使用的最多的类型。
  2. ArrayAdapter:
    适用于一个单项列表,并且数据可以以数据形式存放的场景。
  3. SimpleAdapter:
    适用于一个列表项中有多个数据的场景,它可以将一个 map 里的数据映射到 xml 布局文件中的各个控件上。
  4. SimpleCursorAdapter:
    针对数据库使用的 Adapter,使用场景很少。

3. 常见 Adapter 的用法

其实最常用的是 BaseAdapter,在实际开发中稍微复杂一点的列表都需要通过继承 BaseAdapter 来编写一个自定义的 Adapter 。大多数场景是结合 ListView / GridView 来完成,所以 BaseAdapter 的具体用法我们会放到后面 ListView / GridView 的相关章节做详细介绍,这里主要是让大家对 Adapter 的概念有个基本认识即可。

3.1 ArrayAdapter 的用法

ArrayAdpater 的用法非常简单,如上一小节所说,它适合列表是单项列表并且数据可以存在一个数据当中的场景。首先我们创建布局文件,里面只需要存放一个 ListView 控件即可:

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/simpleListView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:divider="#000"
    android:dividerHeight="2dp" />

其中有两个属性大家可能比较陌生:

 android:divider="#000"
 android:dividerHeight="2dp"

这两个属性是用来设置列表项之间的分割线样式的,详细的会在 ListView 章节进行介绍。然后还需要编写列表中每个列表项的布局样式,我们只需要一个 TextView 来显示文本,而文本的内容就是数组的数据,列表项布局代码 list_view.xml 如下:

<?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">

    <TextView
        android:id="@+id/textView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:padding="30dp"
        android:textColor="#000" />
</LinearLayout>

一个我们非常熟悉的 TextView,然后就可以在 Java 代码中通过 ArrayAdapter进行数据 / UI 的绑定了,Java 代码如下:

package com.emercy.myapplication;

import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import android.app.Activity;

public class MainActivity extends Activity {
    ListView mList;
    String mNums[] = {"TextView", "EditText", "Button", "ImageButton", "RadioButton", "ToggleButton",
            "ImageView", "ProgressBar", "SeekBar", "RatingBar", "ScrollView", "Adapter"};

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

        ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(this, R.layout.list_view, R.id.textView, mNums);
        mList.setAdapter(arrayAdapter);
    }

}

我们在 OnCreate() 中获取ListView对象,然后创建 ArrayAdapter,传入列表项的布局文件 ID、需要显示内容的 TextView 控件 ID 以及数组形式的数据。最后通过 setAdapter 完成数据及 UI 的绑定,这样系统就会帮我们完成适配工作,效果如下:

ArrayAdapter

我们写在数组中的数据就会按顺序填充到列表中了。

3.2 SimpleAdapter 的用法

SimpleAdapter 相比 ArrayAdapter 会更丰富一点,主要体现在 ArrayAdapter 只能适用于列表中只有一项数据(上一小节中的 TextView)的场景,而如果列表项由多个数据组成,比如文字配图片的形式 ArrayAdapter 就有些力不从心,这时候就需要用到 SimpleAdapter 了。
整个 Activity 的布局文件依旧不变,只需要放置一个 ListView 即可。我们在之前的list_view.xml中增加一个 ImageView,如下:

<?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="wrap_content">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_marginRight="10dp"
        android:padding="5dp" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:padding="30dp"
        android:textColor="#000" />
</RelativeLayout>

从上面的布局文件可以看出,我们现在的列表项由两个部分组成:一个图片和一个文本。接着修改 Java 代码,主要是数据格式的变换,现在数据数组需要包含图片资源和文本内容两个部分,如下:

package com.emercy.myapplication;

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

import java.util.ArrayList;
import java.util.HashMap;

public class MainActivity extends Activity {

    ListView mListView;
    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);
        mListView = findViewById(R.id.simpleListView);

        // 将水果图片和水果名称整合到一个map当中,最后将所有的水果都存放到ArrayList
        ArrayList<HashMap<String, String>> arrayList = new ArrayList<>();
        for (int i = 0; i < mDataName.length; i++) {
            HashMap<String, String> hashMap = new HashMap<>();
            hashMap.put("name", mDataName[i]);
            hashMap.put("image", mDataImage[i] + "");
            arrayList.add(hashMap);
        }
        String[] from = {"name", "image"};
        int[] to = {R.id.textView, R.id.imageView};
        SimpleAdapter simpleAdapter = new SimpleAdapter(this, arrayList, R.layout.list_view, from, to);
        mListView.setAdapter(simpleAdapter);

        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Toast.makeText(getApplicationContext(), mDataName[i], Toast.LENGTH_LONG).show();
            }
        });
    }
}

在这段例子中,我们使用两个数组分别保存水果名称及水果图片,然后再将每个水果的名称和图片存入一个 map,接着把所有的水果 map 都整合到一个 ArrayList 当中,最后创建 SimpleAdapter,这一步也是最关键的。我们来单独看看 SimpleAdapter 的创建语句:

SimpleAdapter simpleAdapter = new SimpleAdapter(this, arrayList, R.layout.list_view, from, to);

SimpleAdapter 构造器参数比较多,我们来仔细分析分析。传入构造器的第二个参数是数据源,也就是存放所有水果 map 的 ArrayList 对象;传入的第三个参数是列表项的布局文件,即 list_view.xml;第四个参数是一个字符串数组,表示水果 map 中的 key,也就是水果名和水果图片的 key,用来与具体的 UI 控件对应;最后一个参数是一个整形数组,用来与第四个参数匹配,告诉系统 map 中的哪些数据需要显示到哪个 View 上。这样一来,就完成了列表、列表项、数据的对应关系,接着直接用setAdapter完成适配,最后通过 ListView 的setOnItemClickListener为每个列表项添加点击事件(具体使用方法会在 ListView 章节详细介绍),效果如下:

SimpleAdapter

4 小结

本节介绍了一个比较新鲜的概念——适配器,大家初期理解它可以当成电源适配器来理解就好。然后介绍了几种常用的使用方法,系统也为我们提供了几种封装好的 Adapter 可以应付一些简单的场景。但是在大家实际的开发过程中能够直接使用系统提供的 Adapter 的场景比较少,大多数情况还是要继承 BaseAdapter 来自己实现一套 Adapter,这个内容会在 ListView / GridView 相关章节做具体的介绍。另外,大家可以思考一下本章节的例子如果使用 ScrollView 要怎么实现,优劣势在哪里?