第 22 章 Master-Detail 用户界面

请参考教材,全面理解和完成本章节内容... ...

复制工程ch21,将工程目录改名为ch22.

本章将为「陋习手记」应用打造适应平板设备的用户界面,让用户能同时查看到列表和明细界面并与它们进行交互。图22-1展示了这样的列表明细界面。通常我们也称为主从用户界面(master-detail interface)。

第 22 章 Master-Detail 用户界面_第1张图片

图22-1 同时显示列表和明细的用户界面

本章的测试需要一台平板设备(我用大手机也行)或AVD。要创建平板AVD,首先启动Android Virtual Device Manager,然后在分类Category里单击Tablet,选择一个平板AVD设备。最后设置AVD的系统目标版本为API级别第17级。如图22-2所示:

第 22 章 Master-Detail 用户界面_第2张图片

图22-2 AVD平板设备选择

22.1 增加布局灵活性

在手机设备上,CrimeListActivity生成的是单版面(single-pane)布局。而在平板设备上,为同时显示主从视图,我们需要它生成双版面(two-pane)布局。

在双版面布局中,CrimeListActivity将同时托管CrimeListFragmentCrimeFragment(如图22-3所示)。

第 22 章 Master-Detail 用户界面_第3张图片

图22-3 不同类型的布局

要实现双版面布局,需执行如下操作步骤:

  • 修改SingleFragmentActivity,不再硬编码实例化布局;
  • 创建包含两个fragment容器的布局;
  • 修改CrimeListActivity,实现在手机设备上实例化单版面布局,而在平板设备上实例化双版面布局。
22.1.1 修改SingleFragmentActivity

CrimeListActivitySingleFragmentActivity的子类。当前,SingleFragmentActivity只能实例化activity_fragment.xml布局。为使SingleFragmentActivity类更加抽象、灵活,我们让它的子类自己提供布局资源ID。

在SingleFragmentActivity.java中,添加一个protected方法,返回activity需要的布局资源ID,如代码清单22-1所示。

代码清单22-1 增加SingleFragmentActivity类的灵活性(SingleFragmentActivity.java)

第 22 章 Master-Detail 用户界面_第4张图片

现在,虽然SingleFragmentActivity抽象类的功能和以前一样,但是它的子类可以选择覆盖getLayoutResId()方法返回所需布局,而不用再使用固定不变的activity_fragment.xml布局。

22.1.2 创建包含两个fragment容器的布局

在包浏览器中,右键单击res/layout/目录,选择创建一个新的布局文件,并将文件命名为activity_twopane.xml,然后选择LinearLayout作为根元素。参照图22-4,完成双版面布局的XML内容定义。

第 22 章 Master-Detail 用户界面_第5张图片

图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)

第 22 章 Master-Detail 用户界面_第6张图片

在平板设备或AVD上运行「陋习手记」应用,确认可以看到如图22-5所示的用户界面。注意,右边的明细版面什么也没显示,点击任意列表项,也无法显示对应的陋习明细信息。本章稍后将完成crime明细fragment容器的编码及设置工作。

第 22 章 Master-Detail 用户界面_第7张图片

图22-5 平板设备上的双版面布局

当前,无论是在手机还是在平板设备上,CrimeListActivity都会生成双版面的用户界面。下一节将使用别名资源来解决这个问题。

22.1.3 使用别名资源

别名资源是一种指向其他资源的特殊资源。它存放在res\values\目录下,并按照约定定义在refs.xml文件中。

本小节将分别创建用于手机指向activity_fragment.xml布局的别名资源,以及用于平板指向activity_twopane.xml布局的别名资源。

在项目中,右键单击res\values\目录,创建一个新的XML文件, New->Values resource file,并将文件命名为refs.xml,参照代码清单22-3,在新建的refs.xml中添加item节点定义。

代码清单22-3 创建默认的别名资源值(res/values/refs.xml)

第 22 章 Master-Detail 用户界面_第8张图片

别名资源指向了单版面布局资源文件。别名资源自身也具有资源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)

第 22 章 Master-Detail 用户界面_第9张图片

运行「陋习手记」应用,验证别名资源是否可以正常工作。一切正常的话,CrimeListActivity应该再次生成了单版面布局。

第 22 章 Master-Detail 用户界面_第10张图片

图22-5-1 再次生成了单版面布局

创建平板设备专用可选资源

存放在values目录下的别名资源是系统默认的别名资源,所以,CrimeListActivity生成了默认的单版面布局。

现在,创建一个可选别名资源,以实现在平板等大屏幕设备上,activity_masterdetail别名资源可以指向activity_twopane.xml双版面布局资源。

在包浏览器中,右键单击res目录,新建一个名为values-sw600dp的目录。将res\values\refs.xml文件复制到 res\values-sw600dp目录下,然后参照代码清单22-5,修改别名资源指向双版面布局。

注:如果在工程里看不见新建的values-sw600dp的目录,在工程外部将refs.xml文件复制到 res\values-sw600dp目录下。

代码清单22-5 用于大屏幕设备的可选资源(res/values-sw600dp/refs.xml)

第 22 章 Master-Detail 用户界面_第11张图片

配置修饰符-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\refs.xml资源文件复制到新建的values-xlarge目录中。现在我们有了另一个如代码清单22-6所示的资源文件。

代码清单22-6 用于Android 3.2之前版本的可选资源(res/values-xlarge/refs.xml)

@layout/activity_twopane

配置修饰符-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)

第 22 章 Master-Detail 用户界面_第12张图片

现在,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。因为,布局文件名随时可能更改,并且我们也不关心布局是从哪个文件实例化产生。我们只需知道,布局文件是否包含可以放入CrimeFragmentdetailFragmentContainer

如果证实布局包含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)

第 22 章 Master-Detail 用户界面_第13张图片

onOptionsItemSelected(...)方法中调用回调方法时,只要新增一项crime记录,就会立即重新加载crime列表。这很有必要,因为在平板设备上,新增crime记录后,crime列表依然可见。而在手机设备上,crime明细界面会在列表界面之前出现,列表项的刷新可以很灵活地处理。

在平板设备上运行CriminalIntent应用。新添加一项crime记录,可以看到,一个CrimeFragment视图立即被添加并显示在detailFragmentContainer容器中。然后,尝试查看其他旧记录以观察CrimeFragment视图的切换,如图22-6所示。

第 22 章 Master-Detail 用户界面_第14张图片

图22-6 已关联的主界面和明细界面

然而,如果修改crime明细内容,列表项并不会以最新数据刷新显示。当前,在CrimeListFragment.onResume()方法中,只有新添加一项crime记录,我们才能立即重新刷新显示列表项界面。但是,在平板设备上,CrimeListFragmentCrimeFragment将会同时可见。因此,当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)

第 22 章 Master-Detail 用户界面_第15张图片

然后在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)

第 22 章 Master-Detail 用户界面_第16张图片

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 章 Master-Detail 用户界面_第17张图片

图22-7 列表刷新显示了明细界面的修改

至此,CriminalIntent应用的开发全部完成了。

转载于:https://www.cnblogs.com/jlxuqiang/p/4906070.html

你可能感兴趣的:(第 22 章 Master-Detail 用户界面)