• 第 22 章 Master-Detail 用户界面


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

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

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

    image

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

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

    image

    图22-2 AVD平板设备选择

    22.1 增加布局灵活性

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

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

    image

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

    image

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

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

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

    image

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

    image

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

    image

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

    image

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

    image

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

    image

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

    image

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

    image

    现在,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)

    image

    最终,在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)

    image

    最后,在CrimeListFragment类中,在启动新的CrimePagerActivity的地方,调用onCrimeSelected(Crime)方法。

    在CrimeListFragment.java中,修改onListItemClick(...)onOptionsItemSelected(MenuItem)方法实现对Callbacks.onCrimeSelected(Crime)方法的调用,如代码清单22-10所示。

    代码清单22-10 调用全部回调方法(CrimeListFragment.java)

    image

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

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

    image

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

    image

    然后,在CrimeFragment.java中,添加回调方法接口以及mCallbacks成员变量并实现onAttach(...)onDetach()方法,如代码清单22-12所示。

    代码清单22-12 新增CrimeFragment回调接口(CrimeFragment.java)

    image

    然后在CrimeListActivity类中实现CrimeFragment.Callbacks接口,在onCrimeUpdated(Crime)方法中重新加载crime列表项,如代码清单22-13。

    代码清单22-13 刷新显示crime列表(CrimeListActivity.java)

    image
    在CrimeFragment.java中,如果Crime对象的标题或问题处理状态发生改变,触发调用onCrimeUpdated(Crime)方法,如代码清单22-14所示。

    代码清单22-14 调用onCrimeUpdated(Crime)方法(CrimeFragment.java)

    image

    onActivityResult(...)方法中,Crime对象的记录日期、现场照片以及嫌疑人都有可能修改,因此,还需在该方法中调用onCrimeUpdated(Crime)方法。当前,crime现场照片以及嫌疑人并没有出现在列表项视图中,但并排的CrimeFragment视图应该显示了这些更新,如代码清单22-15所示。

    代码清单22-15 再次调用onCrimeUpdated(Crime)方法(CrimeFragment.java)

    image

    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

    image

    在平板设备上运行CriminalIntent应用。确认CrimeFragment视图中发生的任何修改,ListView视图都能够更新显示,如图22-7所示。

    image

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

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

  • 相关阅读:
    HDU 4508
    HDU 4503
    HDU 4532
    HDU 4544
    HDU 3167 KMP
    HDU 4542
    HDU 4528
    【70】自然语言处理与词嵌入
    【69】循环神经网络
    【68】一维和三维卷积
  • 原文地址:https://www.cnblogs.com/jlxuqiang/p/4906070.html
Copyright © 2020-2023  润新知