请参考教材,全面理解和完成本章节内容... ...
复制工程ch21,将工程目录改名为ch22.
本章将为「陋习手记」应用打造适应平板设备的用户界面,让用户能同时查看到列表和明细界面并与它们进行交互。图22-1展示了这样的列表明细界面。通常我们也称为主从用户界面(master-detail interface)。
图22-1 同时显示列表和明细的用户界面
本章的测试需要一台平板设备(我用大手机也行)或AVD。要创建平板AVD,首先启动Android Virtual Device Manager,然后在分类Category里单击Tablet,选择一个平板AVD设备。最后设置AVD的系统目标版本为API级别第17级。如图22-2所示:
图22-2 AVD平板设备选择
22.1 增加布局灵活性
在手机设备上,CrimeListActivity
生成的是单版面(single-pane)布局。而在平板设备上,为同时显示主从视图,我们需要它生成双版面(two-pane)布局。
在双版面布局中,CrimeListActivity
将同时托管CrimeListFragment
和CrimeFragment
(如图22-3所示)。
图22-3 不同类型的布局
要实现双版面布局,需执行如下操作步骤:
- 修改
SingleFragmentActivity
,不再硬编码实例化布局; - 创建包含两个fragment容器的布局;
- 修改
CrimeListActivity
,实现在手机设备上实例化单版面布局,而在平板设备上实例化双版面布局。
22.1.1 修改SingleFragmentActivity
CrimeListActivity
是SingleFragmentActivity
的子类。当前,SingleFragmentActivity
只能实例化activity_fragment.xml布局。为使SingleFragmentActivity
类更加抽象、灵活,我们让它的子类自己提供布局资源ID。
在SingleFragmentActivity.java中,添加一个protected方法,返回activity需要的布局资源ID,如代码清单22-1所示。
代码清单22-1 增加SingleFragmentActivity
类的灵活性(SingleFragmentActivity.java)
现在,虽然SingleFragmentActivity
抽象类的功能和以前一样,但是它的子类可以选择覆盖getLayoutResId()
方法返回所需布局,而不用再使用固定不变的activity_fragment.xml布局。
22.1.2 创建包含两个fragment容器的布局
在包浏览器中,右键单击res/layout/目录,选择创建一个新的布局文件,并将文件命名为activity_twopane.xml,然后选择LinearLayout
作为根元素。参照图22-4,完成双版面布局的XML内容定义。
图22-4 包含两个fragment容器的布局(layout/activity_twopane.xml)
注意,布局定义的第一个FrameLayout
也有一个fragmentContainer
布局资源ID,因此SingleFragmentActivity.onCreate(...)
方法的相关代码能够像以前一样工作。activity创建后,createFragment()
方法返回的fragment将会出现在屏幕左侧的版面中。
要测试新建布局,在CrimeListActivity
类中覆盖getLayoutResId()
方法,返回R.layout.activity_twopane
资源ID,如代码清单22-2所示。
代码清单22-2 使用双版面布局(CrimeListActivity.java)
在平板设备或AVD上运行「陋习手记」应用,确认可以看到如图22-5所示的用户界面。注意,右边的明细版面什么也没显示,点击任意列表项,也无法显示对应的陋习明细信息。本章稍后将完成crime明细fragment容器的编码及设置工作。
图22-5 平板设备上的双版面布局
当前,无论是在手机还是在平板设备上,CrimeListActivity
都会生成双版面的用户界面。下一节将使用别名资源来解决这个问题。
22.1.3 使用别名资源
别名资源是一种指向其他资源的特殊资源。它存放在resvalues目录下,并按照约定定义在refs.xml文件中。
本小节将分别创建用于手机指向activity_fragment.xml布局的别名资源,以及用于平板指向activity_twopane.xml布局的别名资源。
在项目中,右键单击resvalues目录,创建一个新的XML文件, New->Values resource file,并将文件命名为refs.xml,参照代码清单22-3,在新建的refs.xml中添加item节点定义。
代码清单22-3 创建默认的别名资源值(res/values/refs.xml)
别名资源指向了单版面布局资源文件。别名资源自身也具有资源ID:R.layout.activity_masterdetail
。注意,别名的type
属性决定了资源ID属于什么内部类。即使别名资源自身存放在res/values/目录中,它的资源ID依然归属于R.layout
内部类。
修改CrimeListActivity
类的相应代码,以R.layout.activity_masterdetail
资源ID替换R.layout.activity_fragment
,如代码清单22-4所示。
代码清单22-4 再次切换布局(CrimeListActivity.java)
运行「陋习手记」应用,验证别名资源是否可以正常工作。一切正常的话,CrimeListActivity
应该再次生成了单版面布局。
图22-5-1 再次生成了单版面布局
创建平板设备专用可选资源
存放在values目录下的别名资源是系统默认的别名资源,所以,CrimeListActivity
生成了默认的单版面布局。
现在,创建一个可选别名资源,以实现在平板等大屏幕设备上,activity_masterdetail
别名资源可以指向activity_twopane.xml双版面布局资源。
在包浏览器中,右键单击res目录,新建一个名为values-sw600dp的目录。将resvalues efs.xml文件复制到 resvalues-sw600dp目录下,然后参照代码清单22-5,修改别名资源指向双版面布局。
注:如果在工程里看不见新建的values-sw600dp的目录,在工程外部将refs.xml文件复制到 resvalues-sw600dp目录下。
代码清单22-5 用于大屏幕设备的可选资源(res/values-sw600dp/refs.xml)
配置修饰符-sw600dp
是什么意思?SW
是smallest width(最小宽度)的缩写,虽然字面上是宽度的含义,但它实际指的是屏幕的最小尺寸(dimension),因而SW
与设备的当前方向无关。
在确定可选资源时,-sw600dp
配置修饰符表明:对任何最小尺寸为600dp或更高dp的设备,都使用该资源。对于指定平板的屏幕尺寸规格来说,这是一种非常好的做法。
需要说明的是, Android 3.2中才引入了最小宽度配置修饰符。这意味着,运行Android 3.0或Android 3.1系统的平板设备无法识别它。
为解决该问题,可以增加另一种使用-xlarge
(仅适用于Android 3.2以前的版本)屏幕尺寸修饰符的可选资源。
单击res目录,新建一个名为values-xlarge的目录。然后将values-sw600dp efs.xml资源文件复制到新建的values-xlarge目录中。现在我们有了另一个如代码清单22-6所示的资源文件。
代码清单22-6 用于Android 3.2之前版本的可选资源(res/values-xlarge/refs.xml)
<resources>
<item name="activity_masterdetail" type="layout">@layout/activity_twopane</item>
</resources>
配置修饰符-xlarge
包含的资源适用于最低尺寸为720×960dp的设备。该修饰符仅适用于运行Android 3.2之前版本的设备。Android 3.2及之后的系统版本会自动找到并使用-sw600dp
修饰符目录下的资源。
分别在手机和平板上运行CriminalIntent应用。确认单双版面的布局达到预期效果。
22.2 Activity:fragment 的托管者
既然单双版面的布局显示已处理完成,我们来着手添加CrimeFragment
给crime明细fragment容器,让CrimeListActivity
可以展示一个完整的双版面用户界面。
为保持fragment的独立性,我们可以在fragment中定义回调接口,委托托管activity来完成那些不应由fragment处理的任务。托管activity将实现回调接口,履行托管fragment的任务。
fragment回调接口
为委托工作任务给托管activity,通常的做法是由fragment定义名为Callbacks
的回调接口。回调接口定义了fragment委托给托管activity处理的工作任务。任何打算托管目标fragment的activity必须实现这些定义的接口。
有了回调接口,无需知道自己的托管者是谁,fragment可以直接调用托管activity的方法。
实现CrimeListFragment.Callbacks
回调接口
要实现一个Callbacks
接口,首先定义一个成员变量存放实现Callbacks
接口的对象。然后将托管activity强制类型转换为Callbacks
对象并赋值给Callbacks
类型变量。
在CrimeListFragment.java中,添加一个Callbacks
接口。另外再添加一个mCallbacks
变量并覆盖onAttach(Activity)
和onDetach()
方法,完成变量的赋值与清空,如代码清单22-7所示。
代码清单22-7 添加回调接口(CrimeListFragment.java)
现在,CrimeListFragment
有了调用托管activity方法的途径。另外,它也不关心托管activity是谁。只要托管activity实现了CrimeListFragment.Callbacks
接口,而CrimeListFragment
中一切代码行为都保持不变。
注意,未经类安全性检查,CrimeListFragment
就将托管activity强制转换为CrimeListFragment.Callbacks
对象。这意味着,托管activity必须实现CrimeListFragment.Callbacks
接口。这并非是不良的依赖关系,但记录下它非常重要。
接下来,在CrimeListActivity
类中,实现CrimeListFragment.Callbacks
接口,如代码清单22-8所示。暂时不用理会onCrimeSelected(Crime)
空方法,稍后,我们再来处理。
代码清单22-8 实现回调接口(CrimeListActivity.java)
最终,在onListItemClick(...)
方法里以及在用户创建新crime
时,CrimeListFragment
将调用onCrimeSelected(Crime)
方法。现在,我们先来思考如何实现CrimeListActivity.onCrimeSelected(Crime)
方法。
onCrimeSelected(Crime)
方法被调用时,CrimeListActivity
需要完成以下二选一的任务:
- 如使用手机用户界面布局,启动新的
CrimePagerActivity
; - 如使用平板用户界面布局,将
CrimeFragment
放入detailFragmentContainer
中。
为确定需实例化手机还是平板界面布局,可以检查布局ID
。但最好最准确的检查方式是检查布局是否包含detailFragmentContainer
。因为,布局文件名随时可能更改,并且我们也不关心布局是从哪个文件实例化产生。我们只需知道,布局文件是否包含可以放入CrimeFragment
的detailFragmentContainer
。
如果证实布局包含detailFragmentContainer
,那么我们就会创建一个fragment事务,将我们需要的CrimeFragment添加到detailFragmentContainer
中。如果之前就有CrimeFragment
存在,首先应从detailFragmentContainer
中移除它。
在CrimeListActivity.java中,实现onCrimeSelected(Crime)
方法,按照布局界面的不同,响应crime
的选择,如代码清单22-9所示。
代码清单22-9 有条件的CrimeFragment
启动(CrimeListActivity.java)
最后,在CrimeListFragment
类中,在启动新的CrimePagerActivity
的地方,调用onCrimeSelected(Crime)
方法。
在CrimeListFragment.java中,修改onListItemClick(...)
和onOptionsItemSelected(MenuItem)
方法实现对Callbacks.onCrimeSelected(Crime)
方法的调用,如代码清单22-10所示。
代码清单22-10 调用全部回调方法(CrimeListFragment.java)
在onOptionsItemSelected(...)
方法中调用回调方法时,只要新增一项crime
记录,就会立即重新加载crime
列表。这很有必要,因为在平板设备上,新增crime
记录后,crime
列表依然可见。而在手机设备上,crime
明细界面会在列表界面之前出现,列表项的刷新可以很灵活地处理。
在平板设备上运行CriminalIntent应用。新添加一项crime
记录,可以看到,一个CrimeFragment
视图立即被添加并显示在detailFragmentContainer
容器中。然后,尝试查看其他旧记录以观察CrimeFragment
视图的切换,如图22-6所示。
图22-6 已关联的主界面和明细界面
然而,如果修改crime明细内容,列表项并不会以最新数据刷新显示。当前,在CrimeListFragment.onResume()
方法中,只有新添加一项crime记录,我们才能立即重新刷新显示列表项界面。但是,在平板设备上,CrimeListFragment
和CrimeFragment
将会同时可见。因此,当CrimeFragment
出现时,CrimeListFragment
不会暂停,自然也就永远不会从暂停状态恢复了。这就是crime列表项不能重新加载刷新的根本原因。
下面,我们将在CrimeFragment中添加另一个回调接口来修正该问题。
CrimeFragment.Callbacks回调接口的实现
CrimeFragment
类中定义的接口如下:
public interface Callbacks {void onCrimeUpdated(Crime crime);}
保存crime
记录的修改时,CrimeFragment
类都将调用托管activity的onCrimeUpdated(Crime)
方法。CrimeListActivity
类将会实现onCrimeUpdated(Crime)
方法,从而重新加载CrimeListFragment
的列表。
实现CrimeFragment
的接口之前,首先在CrimeListFragment
类中新增一个方法,用来重新加载刷新CrimeListFragment
列表,如代码清单22-11所示。
代码清单22-11 新增updateUI()方法(CrimeListFragment.java)
然后,在CrimeFragment.java中,添加回调方法接口以及mCallbacks
成员变量并实现onAttach(...)
和onDetach()
方法,如代码清单22-12所示。
代码清单22-12 新增CrimeFragment回调接口(CrimeFragment.java)
然后在CrimeListActivity
类中实现CrimeFragment.Callbacks
接口,在onCrimeUpdated(Crime)
方法中重新加载crime
列表项,如代码清单22-13。
代码清单22-13 刷新显示crime列表(CrimeListActivity.java)
在CrimeFragment.java中,如果Crime对象的标题或问题处理状态发生改变,触发调用onCrimeUpdated(Crime)
方法,如代码清单22-14所示。
代码清单22-14 调用onCrimeUpdated(Crime)
方法(CrimeFragment.java)
在onActivityResult(...)
方法中,Crime
对象的记录日期、现场照片以及嫌疑人都有可能修改,因此,还需在该方法中调用onCrimeUpdated(Crime)
方法。当前,crime
现场照片以及嫌疑人并没有出现在列表项视图中,但并排的CrimeFragment
视图应该显示了这些更新,如代码清单22-15所示。
代码清单22-15 再次调用onCrimeUpdated(Crime)
方法(CrimeFragment.java)
CrimeListActivity
现在有了CrimeFragment.Callbacks
接口的一个良好实现。然而,如果在手机设备上运行CriminalIntent应用,它将会崩溃。记住,任何托管CrimeFragment
的activity都必须实现CrimeFragment.Callbacks
接口。因此,我们还需要在CrimePagerActivity
类中实现CrimeFragment.Callbacks
接口。
对于CrimePagerActivity
类,onCrimeUpdated(Crime)
方法什么都不用做,因此直接实现一个空方法即可(如代码清单22-16所示)。CrimePagerActivity
类托管CrimeFragment
时,必需的列表加载刷新已经在OnResume()
方法中完成了。
代码清单22-16 CrimeFragment.Callbacks接口的空实现(CrimePagerActivity.java
)
在平板设备上运行CriminalIntent应用。确认CrimeFragment
视图中发生的任何修改,ListView
视图都能够更新显示,如图22-7所示。
图22-7 列表刷新显示了明细界面的修改
至此,CriminalIntent应用的开发全部完成了。