Android开发:从0到1 (清华开发者书库)
上QQ阅读APP看书,第一时间看更新

8.1 列表类控件

在数据很多的情况下,需要以列表形式展示,Android提供了多种形式的列表类型的控件,常用的有Spinner和ListView。列表类型的控件有三个要素:控件、Adapter(适配器)和数据源。

8.1.1 适配器

列表类型的控件需要将数据源绑定到控件上,才能看到丰富多彩的界面。而系统能够为控件提供的数据源是多种形式的,它们可能来源于数据库、XML、数组对象或集合对象等。Adpater(适配器)是控件和数据源之间的“桥梁”,通过这个“桥梁”可以将不同形式的数据源绑定到控件上,如图8-1所示。

图8-1 适配器作用

Android提供了多种适配器类,适配器类图如8-2所示,CursorAdapter是数据库适配器类,ArrayAdapter是数组适配器类,SimpleAdapter是Map集合适配器类。有时,系统提供的适配器不能满足需要,就需要自定义适配器类了,这些自定义适配器更需要自己实现某些适配器接口或继承某个适配器抽象类。这些适配器类会在后文中逐一介绍。

图8-2 适配器类图

8.1.2 Spinner

Spinner也是一种列表类型的控件,它提供了可以打开和关闭形式的列表控件,在用户需要选择时打开,选择完成时关闭。打开Spinner列表有两种模式:下拉列表风格和对话框风格。图8-3(a)是下拉列表风格,它是默认情形。图8-3(b)是对话框风格。打开Spinner列表模式可以通过XML中的android:spinnerMode属性设置,取值是dropdown(下拉列表)和dialog(对话框风格)。

图8-3 Spinner样式

Spinner对应类是android.widget.Spinner,类图如图8-4所示,从图中可见android. widget.Spinner继承了抽象类android.widget.AdapterView, AdapterView是一种能够由Adapter管理的控件。AdapterView子类还有ListView、GridView和Gallery等。

图8-4 Spinner类图

AdapterView定义了所用列表控件事件处理,AdapterView为列表控件事件处理提供了三个事件监听器接口:

❏ AdapterView.OnItemClickListener。当列表项被单击时触发。

❏ AdapterView.OnItemLongClickListener。当列表项被长按时触发。

❏ AdapterView.OnItemSelectedListener。当列表项被选择时触发。

事件处理者需要实现相应的事件监听器接口。配合上述事件监听接口AdapterView,还提供了注册事件监听器,三个方法如下:

❏ voidsetOnItemClickListener(AdapterView.OnItemClickListener listener)。注册列表项单击事件监听器。

❏ voidsetOnItemLongClickListener(AdapterView.OnItemLongClickListener listener)。注册列表项长按事件监听器。

❏ voidsetOnItemSelectedListener(AdapterView.OnItemSelectedListener listener)。注册列表项选择事件监听器。

注意 列表控件都直接或间接继承了AdapterView,但在AdapterView中定义的三种事件都适合于所用的列表控件。Spinner只能使用AdapterView.OnItemSelectedListener监听接口。虽然ListView可以使用上述三个接口,但是最常用的还是AdapterView. OnItemClickListener监听接口。

8.1.3 实例:使用Spinner进行选择

使用Spinner控件实现图8-3所示的界面。

实现布局文件activity_main.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:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:text="@string/constellation"
              android:textSize="20sp"/>

          <Spinner
              android:id="@+id/spinner"                                            ①
              android:layout_width="match_parent"
              android:layout_height="wrap_content"/>

      </LinearLayout>

上述代码第①行声明了Spinner,其中android:spinnerMode属性是默认值,此代码运行结果为图8-3(a)所示的列表模式。Spinner代码修改如下:

      <Spinner
          android:id="@+id/spinner"
          android:spinnerMode="dialog"                                               ①
          android:layout_width="match_parent"
          android:layout_height="wrap_content"/>

添加代码第①行android:spinnerMode="dialog",运行结果如图8-3(b)所示。

MainActivity.java代码如下:

        public class MainActivity extends AppCompatActivity{
            static final String TAG ="SpinnerSample";
            static final String[]COLORS= new String[]{"红色", "橙色", "黄色", "绿色", "蓝色", "紫色"};

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

                final ArrayAdapter  CharSequence  adapter = new ArrayAdapter  CharSequence (this,
                        android.R.layout.simple_spinner_item, COLORS);                                    ①
                adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);           ②
 
                Spinner spinner =(Spinner) findViewById(R.id.spinner);                                    ③
                spinner.setAdapter(adapter);                                                              ④

                spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener(){               ⑤
                    @Override
                    public void onItemSelected(AdapterView ? parent, View view, int position, long id){   ⑥
                        Log.i(TAG, "选择:"+ adapter.getItem(position).toString());
                    }

                    @Override
                    public void onNothingSelected(AdapterView ? parent){                                  ⑦
                        Log.i(TAG, "未选中");
                    }
                });
            }
        }

上述代码第①行创建数组适配器ArrayAdapter对象,作为数组类型数据源的适配器,除了提供数组作为数据源以外,还要为Spinner中的列表项提供布局样式。ArrayAdapter构造方法的第一个参数android.R.layout.simple_spinner_item是列表项布局,使用Android框架提供的simple_spinner_item.xml布局文件。ArrayAdapter构造方法的第二个参数COLORS是数据源,ArrayAdapter需要的数据源是数组。

代码第②行是通过Spinner的setDropDownViewResource()方法设置弹出的下拉列表的布局样式,参数android.R.layout.simple_spinner_dropdown_item是使用Android框架提供的simple_spinner_dropdown_item.xml布局文件。

代码第③行获得Spinner控件对象,然后再通过代码第④行spinner.setAdapter(adapter)把适配器与Spinner控件绑定到一起。

代码第⑤行的setOnItemSelectedListener()方法是注册Spinner控件的选择事件监听器,选择事件监听器需要实现AdapterView.OnItemSelectedListener接口,具体实现代码见第⑥行的选择列表项方法onItemSelected和第⑦行未选中方法onNothingSelected。在代码第⑥行onItemSelected中,参数position是选中的列表项位置,id是选项的编号。

8.1.4 ListView

ListView是Android中最为常用的列表类型控件,ListView中的选择列表项样式很丰富,有的是纯文字,有的还可以带有图片等。

ListView对应类是android.widget.ListView,类图如图8-5所示,从图中可见android.widget.ListView继承了抽象类android.widget.AdapterView。

图8-5 ListView类图

8.1.5 实例1:使用ListView实现选择文本

事实上,所有列表类型控件的技术难点是适配器,适配器一方面管理数据源,另一方面管理列表项的布局样式。列表项中若只是显示文本,可以使用ArrayAdapter、SimpleAdapter或CursorAdapter适配器,如果这些适配器不能满足需要,可以自定义来实现。

本节先介绍在ListView中显示文本实例,实例的运行效果如图8-6所示。

图8-6 实例运行效果

实现布局文件activity_main.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="horizontal">

          <ListView
              android:id="@+id/ListView01"
              android:layout_width="match_parent"
              android:layout_height="match_parent"/>
      </LinearLayout>

上述代码中声明了ListView控件,属性设置非常简单。

MainActivity.java代码如下:

        public class MainActivity extends AppCompatActivity{
            static final String TAG ="ListViewSample";
            private String[] mStrings ={
              "北京市","天津市","上海","重庆","乌鲁木齐"…};

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

                ArrayAdapter  String  adapter = new ArrayAdapter  String (this,
                        android.R.layout.simple_list_item_1,mStrings);                              ①

                ListView listview=(ListView) findViewById(R.id.ListView01);                         ②
                listview.setAdapter(adapter);                                                       ③
                listview.setOnItemClickListener(new AdapterView.OnItemClickListener(){              ④
                    @Override
                    public void onItemClick(AdapterView ? parent,View view,int position,long id){
                                                                                                    ⑤
                        Log.i(TAG,"选择:"+ mStrings[position]);
                    }
                });
            }
        }

上述代码第①行创建数组适配器ArrayAdapter对象,构造方法ArrayAdapter参数android.R.layout.simple_list_item_1是使用Android框架提供的布局simple_list_item_1.xml文件,该布局文件中只有一个TextView控件,每一个列表项只能显示文本内容。

构造方法ArrayAdapter参数mStrings是数组数据源。

提示 Android系统本身提供了很多这样的布局文件,但是有的适合于ListView控件,有的适合于Spinner控件,有的适合于它的列表控件,这是使用时需要注意的。例如,8.1.3节的实例Spinner使用了Android框架提供的布局文件simple_spinner_item. xml,该文件就不适合在ListView中使用。

代码第②行获得ListView控件对象,然后再通过代码第③行listview.setAdapter(adapter)把适配器与ListView控件绑定到一起。

代码第④行的setOnItemClickListener()方法是注册ListView控件的选择事件监听器,选择事件监听器需要实现AdapterView.OnItemClickListener接口,具体实现代码为第⑤行所示的方法,参数position是选中列表项的位置,id是选项的编号。

8.1.6 实例2:使用ListView实现选择文本+图片

本节介绍如何自定义适配器实现ListView中显示文本与图片。自定义适配器主要是通过继承BaseAdapter抽象类来实现,本实例的运行效果如图8-7所示。

图8-7 实例运行效果

该实例布局文件有两个,一个是屏幕布局文件activity_main.xml;另一个是列表控件中每一个列表项的布局文件listview_item.xml。

主屏幕布局文件activity_main.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="horizontal">

          <ListView
              android:id="@+id/ListView01"
              android:layout_width="match_parent"
              android:layout_height="match_parent"/>
      </LinearLayout>

列表项的布局文件listview_item.xml代码如下:

      <?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/icon"
              android:layout_width="48dp"
              android:layout_height="48dp"
              android:layout_marginLeft="5dp"/>

          <TextView
              android:id="@+id/textview"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_toEndOf="@id/icon"                                  ①
              android:layout_marginLeft="15dp"
              android:layout_marginTop="10dp"
              android:textSize="20sp"/>
      </RelativeLayout>

列表项的布局采用相对布局,代码第①行设定了TextView在ImageView后面。

在该实例中,Java源代码文件有两个:屏幕Activity类MainActivity.java和自定义适配器类EfficientAdapter.java。

MainActivity.java代码如下:

        public class MainActivity extends AppCompatActivity{

            static final String TAG ="ListViewSample"; 
            String[] DATA ={"北京市", "天津市", "上海", "重庆", "哈尔滨", …};                  ①
            int[] icons ={R.mipmap.beij ing, R.mipmap.tianj ing, R.mipmap.shanghai,
                    R.mipmap.chongqing, R.mipmap.haerbing, …};                            ②

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

                EfficientAdapter adapter = new EfficientAdapter(this,
                        R.layout.listview_item, DATA, icons);                             ③

                ListView listview=(ListView) findViewById(R.id.ListView01);
                listview.setAdapter(adapter);

                listview.setOnItemClickListener(new AdapterView.OnItemClickListener(){
                    @Override
                    public void onItemClick(AdapterView ? parent, View view, int position, long id){
                        Log.i(TAG, "选择:"+ DATA[position]);
                    }
                });
            }
        }

上述代码第①行是定义数据源中城市名称数组。代码第②行是与城市名称数组对应的城市图标数组,该数组是int类型,保存放置在res/mipmap目录图标id。

注意 DATA和icons两个数组元素是一一对应的,即DATA第一个元素对应icons第一个元素,以此类推,所以它们两个数组的长度也是相等的。如果读者感觉两个相互关联的数组不好管理,可以使用Map数据结构保存城市名称和城市图标数据。

上述代码第③行是实例化定义的适配器EfficientAdapter类,构造方法需要提供4个参数。

下面看看EfficientAdapter.java代码:

        public class EfficientAdapter extends BaseAdapter{                                  ①

            private LayoutInflater mInflater;            //布局填充器                         ②
            private String[] mDataSource;               //数据源数组
            private int[] mIcons;                      //与数据源数组对应的图标id
            private int mResource;                      //列表项布局文件
            private Context mContext;                   //所在上下文                          ③

            public EfficientAdapter(Context context, int resource,
                                  String[] dataSource, int[] icons){
                mContext = context;
                mResource = resource;
                mDataSource = dataSource;
                mIcons = icons;
                //通过上下文对象创建布局填充器
                mInflater = LayoutInflater.from(context);                                   ④
            }
            //返回总数据源中总的记录数
            @Override
            public int getCount(){
                return mDataSource.length;
            }
            //根据选择列表项位置,返回列表项所需数据
            @Override
            public Object getItem(int position){
                return mDataSource[position];
            }
            //根据选择列表项位置,返回列表项id
            @Override
            public long getItemId(int position){
                return position;
            }
            //返回列表项所在视图对象
            @Override
            public View getView(int position, View convertView, ViewGroup parent){

                ViewHolder holder;                                                          ⑤
                if(convertView== null){                                                     ⑥
                    convertView= mInflater.inflate(mResource, null);                        ⑦
                    holder = new ViewHolder();
                    holder.textView
                            =(TextView) convertView.findViewById(R.id.textview);
                    holder.imageView
                            =(ImageView) convertView.findViewById(R.id.icon);
                    convertView.setTag(holder);                                             ⑧
                }else{
                    holder =(ViewHolder) convertView.getTag();                              ⑨
                }
                holder.textView.setText(mDataSource[position]);
                Bitmap icon= BitmapFactory
                        .decodeResource(mContext.getResources(), mIcons[position]);         ⑩
                holder.imageView.setImageBitmap(icon);
                return convertView;
            }
            //保存列表项中控件的封装类
            static class ViewHolder{                                                        ⑪
                TextView textView;                      //列表项中Textview
                ImageView imageView;                   //列表项中ImageView
            }
        }

上述代码第①行声明继承抽象类BaseAdapter。代码第②行是定义成员变量mInflater,它是LayoutInflater类型,LayoutInflater是布局填充器,通过布局填充器类可以从XML文件创建视图。代码第③行是定义成员变量mContext,它是Context类型,Context类称为“上下文”,上下文描述了当前组件的信息,Context是抽象类,它的子类有Activity、Service和广播接收器等,在本例中就是当前的Activity对象。

代码第④行LayoutInflater.from(context)是通过上下文对象创建布局填充器,这是一种工厂设计模式。

继承BaseAdapter重写getView()方法比较麻烦。getView()方法是ListView的每个列表项显示到屏幕上时被调用的,getView()方法返回值View是列表项显示的视图。

注意 getView()方法的convertView参数非常重要!当用户向上滑动屏幕翻动列表时,屏幕上的列表项会退出屏幕,屏幕下面原来不可见的列表项会进入屏幕,列表项在屏幕中显示时会调用getView()方法获得列表项视图,如果每次都实例化列表项视图,那么必然会导致大量对象的创建,消耗大量的内存。参数convertView就是为了解决这个问题而设计的,它是一个可重用的列表项视图对象。如果convertView为空值(一般是刚进入屏幕),则实例化convertView(见代码第⑥行)。如果convertView不为空值,直接返回convertView。实例化convertView对象,见代码第⑦行,它通过布局填充器的inflate方法从布局文件创建,mResource是布局文件id。

代码第⑤行是声明ViewHolder类型的变量holder, ViewHolder是代码第⑪行声明的内部类用来保存列表项中控件的封装类。holder保存在convertView的tag属性中,见代码⑧行。每一个View都有tag属性,属性类型是Obj ect,因此tag属性可以保存任何对象。如果convertView不为空,可以通过代码第⑨行的convertView.getTag()方法取出holder对象,但是这个holder是个旧的对象,保存了上次显示列表项所需的内容。所以,要通过holder.textView.setText(mDataSource[position])和holder.imageView.setImageBitmap(icon)语句重新设置本次要显示列表项所需内容。

代码第⑩行是通过BitmapFactory工厂类decodeResource创建Bitmap图片对象,decodeResource方法可以通过图片资源id获得图片对象。