请参考教材,全面理解和完成本章节内容... ...
复制工程ch8,将工程目录改名为ch9。
我们经常会在应用程序中使用列表的形式来展现一些内容,所以学好ListView控件是非常必要的。ListView功能强大、用法灵活,需要我们花些时间才能掌握,不过本章将是一个非常好的的开始。
当前,「陋习手记」应用的模型层仅包含一个Crime实例。本章,我们将更新「陋习手记」应用以包含一个crime列表(通过ListView控件实现的),如图9-1所示。列表显示每一个Crime实例的标题、发生日期以及处理状态。(本章功能需求)
图9-1crime列表
图9-2展示了本章「陋习手记」应用的整体规划设计。
图9-2 「陋习手记」应用对象图
应用的模型层将新增一个CrimeLab对象,该对象是一个数据集中存储池,用来存储Crime对象。
为了显示crime的列表,应用还需在的控制层新增一个activity和一个fragment,即CrimeListActivity和CrimeListFragment。
其中CrimeListFragment是ListFragment的子类,ListFragment是Fragment的子类, Fragment内置的功能支持列表显示。
9.1 更新 CriminalIntent 应用的模型层
首先,我们来更新应用的模型层,从原来的单个Crime
对象变为可容纳多个Crime
对象的ArrayList
。
单例与数据集中存储
在「陋习手记」应用中,crime数组对象将存储在一个单例里。
提示:
java中单例模式是一种常见的设计模式,单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。如,同一个手机仅能登录一个微信。
为什么将crime数组对象存储在一个单例对象里?应用能够在内存里存在多久,单例对象就能存在多久,因此将对象列表保存在单例里可保持crime数据的一直存在,不管activity、fragment及它们的生命周期发生什么变化。
要创建单例对象,需创建一个带有私有构造方法及get()
方法的类,其中get()
方法返回实例。如实例已存在,get()
方法则直接返回它;如实例还未存在,get()
方法会调用构造方法来创建它。
右键单击com.
jet
.criminalintent
类包,选择New → Class菜单项。在随后出现的对话框中,命名类为CrimeLab
,然后单击OK按钮完成。
在打开的CrimeLab.java文件中,编码实现CrimeLab
类为带有私有构造方法和get(Context)
方法的单例,如代码清单9-1所示。
代码清单9-1 创建单例(CrimeLab.java)
CrimeLab
类的构造方法需要一个Context
参数。这在Android开发里很常见,使用Context
参数,单例可完成启动activity、获取项目资源,查找应用的私有存储空间等任务。
下面,我们将一些Crime
对象保存到CrimeLab
中去。在CrimeLab
的构造方法里,创建一个空的用来保存Crime
对象的ArrayList
。此外,再添加两个方法,即getCrimes()
方法和getCrime(UUID)
方法。前者返回数组列表(多个crime),后者返回带有指定ID的Crime
对象。具体代码如代码清单9-2所示。
代码清单9-2 创建可容纳Crime
对象的ArrayList
(CrimeLab.java)
新建的ArrayList
将内含用户自建的Crime
,用户既可以存入Crime
,也可以从中调取Crime
。但实现用户手工存入Crime
还需要许多工作,我们放的后续章节完成。现在,我们暂时先往数组列表中批量存入100个Crime
对象,如代码清单9-3所示。
代码清单9-3 生成100个crime(CrimeLab.java)
现在我们拥有了一个满是数据的模型层,和100个可在屏幕上显示的crime。
9.2 创建 ListFragment
创建一个名为CrimeListFragment的类, 使用ListFragment类作为她的超类,导入支持库中的android.support.v4.app.ListFragment类包,避免不同系统版本的兼容性问题。
在CrimeListFragment.java中,覆盖onCreate(Bundle)方法,设置托管CrimeListFragment的activity标题,如代码清单9-4所示。
代码清单9-4 为新Fragment添加onCreate(Bundle)方法(CrimeListFragment.java)(原翻译为“为新Activity”不对)
注意查看getActivity()方法,该方法不仅可以便利地返回托管activity,且允许fragment处理更多的与activity相关的事务。这里,我们就用它调用Activity.setTitle(int)方法,将显示在操作栏上的标题文字替换为传入的字符串资源中设定的文字。
现在,无需覆盖onCreateView()方法或为CrimeListFragment生成布局。ListFragment类默认实现方法已生成了一个全屏ListView布局。我们先暂时使用该布局,后续章节中我们会覆盖CrimeListFragment.onCreateView()方法,从而添加更多的高级功能。
在strings.xml文件中,为列表activity标题添加字符串资源,如代码清单9-5所示。
代码清单9-5 为新的activity标题添加字符串资源(strings.xml)
CrimeListFragment需要获取存储在CrimeLab中的crime列表。在CrimeListFragment.onCreate()方法中,先获取CrimeLab单例,再获取其中的crime列表,如代码清单9-6所示。
代码清单9-6 在CrimeListFragment中获取crime(CrimeListFragment.java)
9.3 使用抽象 activity 托管 fragment
下面我们来创建一个用于托管CrimeListFragment
的CrimeListActivity
类。首先为CrimeListActivity
创建一个视图。
9.3.1 通用的fragment托管布局
对于CrimeListActivity
,我们仍可使用定义在activity_crime.xml文件中的布局。该布局提供了一个用来放置fragment的FrameLayout
容器视图,其中的fragment可在activity中使用代码获取,如代码清单9-7所示。
代码清单9-7 通用的布局定义文件activity_crime.xml
因为activity_crime.xml布局文件并没有指定给一个特定的fragment,因此你可以使用它托管任一个fragment。为反映出该布局的通用性,我们把它重命名为activity_fragment.xml。
首先,如果已打开了activity_crime.xml文件,请先关闭它。接下来,右键单击activity_crime.xml文件。在弹出的菜单里,选择Refactor → Rename菜单项将activity_crime.xml改名为activity_fragment.xml,点击Refactor确认。重命名资源时,IDE会自动更新资源文件的所有引用。如代码清单9-8所示。(太幸福了,这就是重构好处!)
代码清单9-8 为CrimeActivity
更新布局文件引用(CrimeActivity.java)
9.3.2 抽象 activity 类
我们可以通过复用CrimeActivity
的代码来创建CrimeListActivity
类。
回顾一下前面写的CrimeActivity
类代码,该代码简单且几近通用。事实上,CrimeActivity
类代码唯一不通用的地方是实例化CrimeFragment
类哪行代码(用下划线标识的部分),如代码清单9-9所示。
代码清单9-9 几近通用的CrimeActivity
类(CrimeActivity.java)
本书中几乎每一个创建的activity都需要同样的代码。为避免不必要的重复性输入,我们将这些重复代码置入一个抽象类,以供使用。
首先,在CriminalIntent
类包里创建一个名为SingleFragmentActivity
的新类,单击OK按钮完成创建,如图9-3所示。
图9-3 创建SingleFragmentActivity抽象类
接下来,使用FragmentActivity
类作为它的超类。
然后,使用abstract关键字,让SingleFragmentActivity
类成为一个抽象类,添加代码清单9-10所示代码到SingleFragmentActivity.java文件。可以看到,除了两处加下划线的代码,其余代码和原来的CrimeActivity
代码完全一样。
代码清单9-10 添加一个通用超类(SingleFragmentActivity.java)
在以上代码里,我们设置从activity_fragment.xml布局里生成activity视图。然后在容器中寻找FragmentManager
里的fragment。如fragment不存在,则创建一个新的fragment并将其添加到容器中。
代码清单9-10与原来的CrimeActivity
代码唯一的区别就是,新增了一个名为createFragment()
的抽象方法,我们可使用它实例化新的fragment。SingleFragmentActivity
的子类会实现该方法返回一个由activity托管的fragment实例。
1. 使用抽象类
下面我们来测试一下抽象类的使用。首先创建一个名为CrimeListActivity
的新类,如图9-4所示。
图9-4CrimeListActivity类
新建的CrimeListActivity
类是SingleFragmentActivity类的子类。
首先,参考代码清单9-11完成CrimeListActivity
类的创建。
代码清单9-11 代码实现CrimeListActivity
(CrimeListActivity.java)
如代码清单9-11所示,代码实现了父类的抽象方法createFragment()
。该方法返回一个新的CrimeListFragment
实例。
如果CrimeActivity
类也可以继承通用类来实现,那就再好不过了。返回到CrimeActivity.java文件中。参照代码清单9-12,删除CrimeActivity
类的现有代码,重新编写代码,使其成为SingleFragmentActivity
的子类。
代码清单9-12 清理CrimeActivity
类(CrimeActivity.java)
在本书的后续章节中,我们会发现使用SingleFragmentActivity
抽象类可大大减少代码输入量,节约开发时间。现在,世界清静了,我们的activity代码看起来简练又整洁。
2. 在配置文件中声明CrimeListActivity
CrimeListActivity
创建完成后,记得在配置文件中声明它(为什么呢?)。
为实现CriminalIntent应用启动后,用户看到的主界面是crime列表,我们还需配置CrimeListActivity
为启动activity。
如代码清单9-13所示,在manifest配置文件中,首先声明CrimeListActivity
,然后删除CrimeActivity
的启动activity配置,改配CrimeListActivity
为启动activity。
代码清单9-13 声明CrimeListActivity
为启动activity(AndroidManifest.xml)
现在,CrimeListActivity
被配置为启动activity。运行CriminalIntent应用,会看到CrimeListActivity
的FrameLayout
托管了一个无内容的CrimeListFragment
,如图9-5所示。
图9-5 没有内容的CrimeListActivity用户界面
当ListView
没有内容可以显示时,ListFragment
会通过内置的ListView
显示一个圆形进度条。CrimeListFragment
已经被赋予了访问Crime
数组的能力,接下来,我们要将crime列表通过ListView
显示在屏幕上。
9.4 ListFragment、ListView 及 ArrayAdapter
我们需要通过CrimeListFragment
的ListView
将列表项展示给用户,而不是什么圆形进度条。每一个列表项都应该包含一个Crime
实例的数据。
ListView
是ViewGroup
的子类,每一个列表项都是作为ListView
的一个View
子对象显示的。这些View
子对象既可以是复杂的View
对象,也可以是简单的View
对象,这取决于我们对列表显示复杂度的需要。
简单起见,我们先实现一个简单形式的列表项显示,即每个列表项只显示Crime
的标题,并且View
对象是一个简单的TextView
,如图9-6所示。(你可能有疑义,这些陋习项目不是逐条添加的吗?Good Idear!)
图9-6 带有子TextView的ListView
上图中,我们可以看到6个TextView
。要是能滚动截图屏幕的话,ListView
还可显示出更多的TextView
,如 陋习 7 、陋习 8等。
这些View
对象来自哪里?ListView
会提前准备好所有要显示的View
对象吗?倘若这样,效率可就太低了。其实View
对象只有在屏幕上显示时才有必要存在。列表的数据量非常大,为整个列表创建并储存所有视图对象不仅没有必要,而且会导致严重的系统性能及内存占用问题。
因此,比较聪明的做法是在需要显示的时候才创建视图对象。即当ListView
需要显示某个列表项时,它才会去申请一个可用的视图对象。
ListView
找谁去申请视图对象呢?答案是adapter。
adapter是一个控制器对象,从模型层获取数据,并将之提供给ListView
显示,起到沟通桥梁的作用。adapter负责:
- 创建必要的视图对象;
- 用模型层数据填充视图对象;
- 将准备好的视图对象返回给ListView。
ListView
需要显示视图对象时,会与其adapter展开会话沟通(自动)。图9-8展示了ListView
向其数组adapter启动会话的例子。
图9-8 模拟ListView与Adapter会话过程
首先,通过调用adapter的getCount()
方法,ListView
询问数组列表中包含多少个对象。(为避免出现数组越界的错误,获取对象数目信息非常重要。)
紧接着,ListView
就调用adapter
的getView(int, View, ViewGroup)
方法。该方法的第一个参数是ListView
要查找的列表项在数组列表中的位置。
在getView()
方法的内部实现里,adapter使用数组列表中指定位置的列表项创建一个视图对象,并将该对象返回给ListView
。ListView
继而将其设置为自己的子视图,并刷新显示在屏幕上。
稍后,通过覆盖getView()
方法创建定制列表项的学习,我们将会了解到更多有关它的实现机制。
9.4.1 创建ArrayAdapter<T>
类实例
首先,使用以下构造方法为CrimeListFragment
创建一个默认的ArrayAdapter<T>
类实现:
publicArrayAdapter(Context context,int textViewResourceId, T[] objects)
在数组adapter构造方法中:
- 第一个参数是一个Context对象,是上下文,就是当前的Activity;
- 第二个参数是android sdk中自己内置的一个布局,它里面只有一个TextView,这个参数是表明我们数组中每一条数据的布局是这个view,就是将每一条数据都显示在这个view上面;
- 第三个参数是数据集(Crime数组对象),就是我们要显示的数据。
listView会根据这三个参数,遍历adapter里面的每一条数据,读出一条,显示到第二个参数对应的布局中,这样就形成了我们看到的listView。
在CrimeListFragment.java中,创建一个ArrayAdapter<T>
实例,并设置其为CrimeListFragment
中ListView
的adapter,如代码清单9-14所示。
代码清单9-14 创建ArrayAdapter
(CrimeListFragment.java)
setListAdapter(
ListAdapter)
是ListFragment
类内含的一个易用的方法,使用此方法可为ListFragment
类内置的ListView
设置(或绑定)adapter(这个ListView由CrimeListFragment
管理)。
(ListFragment类默认实现方法生成一个全屏ListView布局)
我们在adapter的构造方法中指定的布局(android.R.layout.simple_list_item_1)是Android SDK 提供的预定义布局资源(稍后我们用自己的布局替换它)。该布局拥有一个TextView
根元素。布局的源码如代码清单9-15所示。
代码清单9-15 android.R.layout.simple_list_item_1资源的源码
也可在adapter的构造方法中指定其他布局,只要满足布局的根元素是TextView
条件即可。
得益于ListFragment
的默认行为,我们现在可以运行应用了。ListView
随即会被实例化,并显示在屏幕上,然后开启与adapter间的会话。
运行CriminalIntent应用。这次屏幕上出现的是列表项,而不再是圆形进度条了。不过,视图上显示的内容对用户来说莫名其妙(为什么?),如图9-9所示。
图9-9 混合了类名和内存地址的列表项
默认的ArrayAdapter<T>.getView()
实现方法依赖于toString()
方法。它首先生成布局视图,然后找到指定位置的Crime
对象并对其调用toString()
方法,最后得到字符串信息并传递给TextView
。Crime
类当前并没有覆盖toString()
方法,因此,它默认使用了java.lang.Object
类的实现方法,直接返回了混和对象类名和内存地址的字符串信息。这就是视图上显示“莫名其妙”内容的原因!
为让adapter针对Crime
对象创建更实用的视图,可打开Crime.java文件,覆盖toString()
方法返回crime标题,如代码清单9-16所示。
代码清单9-16 覆盖Crime.toString()
方法(Crime.java)
再次运行CriminalIntent应用。上下滚动列表,查看更多的Crime
对象,如图9-10所示。
图9-10 显示crime标题的简单列表项
在我们上下滚动列表时,ListView
调用adapter的getView()
方法,按需获得要显示的视图。
9.4.2 响应列表项的点击事件
要响应用户对列表项的点击事件,我们仅需要覆盖ListFragment
类的另一方法:
public void onListItemClick(ListView l, View v, int position, long id)
无论用户是单击或是手指的触摸,都会触发onListItemClick()
方法。
OK,我们开始处理列表项的点击事件,首先,在CrimeListFragment.java中,覆盖onListItemClick()
方法,使adapter返回被点击的列表项所对应的Crime
对象;然后,日志会记录Crime
对象的标题,如代码清单9-17所示。
代码清单9-17 覆盖onListItemClick()
方法,日志记录Crime对象的标题(CrimeListFragment.java)
getListAdapter()
方法是ListFragment
类中的一个便利的方法,该方法可返回设置在ListFragment
列表视图上的adapter。然后,使用onListItemClick()
方法的position
参数调用adapter的getItem(int)
方法,最后把结果转换成Crime对象。
再次运行CriminalIntent应用。点击某个列表项,查看日志,确认Crime
对象已被正确获取。
9.5 定制列表项
截至目前,每个列表项只是显示了Crime
的标题(Crime.toString()
方法的返回结果)。
如不满足于此,也可以创建定制列表项。实现显示定制列表项需完成以下任务:
- 创建定义列表项视图的XML布局文件;
- 创建
ArrayAdapter<T>
的子类,用来创建、填充并返回定义在新布局中的视图。
9.5.1 创建列表项布局
在CriminalIntent应用中,每个列表项的视图布局应包含crime的三项内容,即标题、创建日期,以及是否已解决的状态,如图9-11所示。这要求该视图布局包含两个TextView
和一个CheckBox
。
图9-11 一些定制的列表项
如同创建activity或fragment视图一样,为列表项创建一个新的布局视图。在项目导航视图中,右键单击reslayout目录,选择New -> XML-> Layout XML File。在随后出现的对话框中,命名布局文件为list_item_crime.xml,设置其根元素为RelativeLayout
,最后单击Finish按钮完成。
在RelativeLayout
里,子视图相对于根布局以及子视图相对于子视图的布置排列,需使用一些布局参数加以布置控制。对于列表项新布局,需布置CheckBox
对齐RelativeLayout
布局的右手边,布置两个TextView
相对于CheckBox
左对齐。
图9-12展示了定制列表项布局的全部组件。CheckBox
子视图须首先被定义,因为虽然它出现在布局的最右边,但TextView
需使用CheckBox
的资源ID作为属性值。
现在是利用图形布局工具的时候了,依据图9-12完成布局文件list_item_crime.xml。
图9-12 定制列表项的布局(list_item_crime.xml)
基于同样的理由,显示标题的TextView
也必须定义在显示日期的TextView
之前。总而言之,在布局文件里,一个组件必须首先被定义,这样,其他组件才能在定义时使用它的资源ID。定制列表项的布局如图9-12所示。
注意,在其他组件的定义中使用某个组件的ID时,符号+不应该包括在内(只是应用不是定义)。符号+是在组件首次出现在布局文件中时,用来创建资源ID的,一般出现在android:id属性值里。
另外要注意的是,我们在布局定义中使用的是固定字符串,而不是android:text属性的字符串资源。这些字符串是用作开发和测试的示例文字。adapter会提供用户想看到的信息。由于这些字符串不会显示给用户,所以也就没有必要为它们创建字符串资源了。
定制列表项布局创建就完成了。接下来,我们继续学习创建定制adapter。
9.5.2 创建adapter子类
定制布局用来显示特定Crime
对象信息的列表项。列表项要显示的数据信息必须使用Crime
类的getter方法才能获取,因此,我们需创建一个知道如何与Crime
对象交互的adapter。
在CrimeListFragment.java中,创建一个ArrayAdapter
的子类作为CrimeListFragment
的内部类,如代码清单9-18所示。
代码清单9-18 添加定制的adapter内部类(CrimeListFragment.java)
这里需调用超类的构造方法来绑定Crime
对象的数组列表。由于不打算使用预定义布局,我们传入0作为布局ID参数。
创建并返回定制列表项是在ArrayAdapter<T>
的方法getView()里实现的,如下所示:
public View getView(int position, View convertView, ViewGroup parent)
convertView
参数是一个已存在的列表项,adapter可重新配置并返回它,因此我们无需再创建全新的视图对象。复用视图对象可避免反复创建、销毁同一类对象的开销,应用性能因此得到了提高。ListView
一次性需显示大量列表项,因此,没有理由产生大量暂不使用的视图对象来耗尽宝贵的内存资源。
在CrimeAdapter
内部类中覆盖getView()
方法, 返回产生于定制布局的视图对象,并填充对应的Crime数据如代码清单9-19所示。
注意:重写的是CrimeAdapter内部类中的getView()方法,不是CrimeListFragment类的方法;
提示:
ListView需要显示视图对象时,会与其adapter自动展开会话沟通,如自动调用getView()方法,类似购买火车票,系统会自动查询余额
代码清单9-19 覆盖getView()
方法(CrimeListFragment.java)
在getView()
实现方法里,首先检查传入的视图对象是否是复用对象。如不是,则从定制布局里产生一个新的视图对象。
无论是新对象还是复用对象,都应调用Adapter
的getItem(int)
方法获取列表中当前position
的Crime
对象。
获取Crime
对象后,引用视图对象中的各个组件,并以Crime
的数据信息对应配置视图对象。最后,把视图对象返回给ListView
。
现在可以在CrimeListFragment
中绑定定制的adapter了。在CrimeListFragment.java文件头部,参照代码清单9-20,更新onCreate()
和onListItemClick()
实现方法以使用CrimeAdapter
。
代码清单9-20 使用CrimeAdapter
(CrimeListFragment.java)
既然已转换为CrimeAdapter
类,自然也获得了类型检查的能力。CrimeAdapter
只能容纳Crime
对象,因此Crime
类的强制类型转换也就不需要了。
通常情况下,现在就可以准备运行应用了。但由于列表项中存在着一个CheckBox
,因此还需进行一处调整。CheckBox
默认是可聚焦的。这意味着,点击列表项会被解读为切换CheckBox
的状态,自然也就无法触发onListItemClick()
方法了。
由于ListView
的这种内部特点,出现在列表项布局内的任何可聚焦组件(如CheckBox
或Button
)都应设置为非聚焦状态,从而保证用户在点击列表项后能够获得预期效果。
当前CheckBox
没有绑定应用逻辑,只是用来显示Crime
信息的,因此,解决方法很简单。只需更新list_item_crime.xml布局文件,将CheckBox
定义为非聚焦状态组件即可,如代码清单9-21所示。
代码清单9-21 配置CheckBox
为非聚焦状态(list_item_crime.xml)
运行CriminalIntent应用。滚动查看定制列表项,如图9-13所示。点击某个列表项并查看日志,确认CrimeAdapter
返回了正确的crime信息。如应用可运行但布局显示不正确,请返回list_item_crime.xml布局文件,检查是否存在输入或拼写等错误。
图9-13 具有定制列表项的用户界面