第 16 章 工具栏

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

复制工程ch12,将工程目录改名为ch16.

在Honeycomb版本系统中,Android引入了全新的操作栏。操作栏不仅取代了用来显示标题和应用图标的传统标题栏(title bar),还带来了更多其他功能,例如,安置菜单选项、配置应用图标作为导航按钮,等等。

本章,我们将为CriminalIntent应用创建一个菜单,并在其中提供可供用户新增crime记录的菜单项,然后让应用的图标支持向上的导航操作,如图16-1所示。

image

  图16-1 创建选项菜单文件

16.1 选项菜单

可显示在工具栏上的菜单被称作选项菜单。新增一条crime记录就是一个很好的例子。而从列表中删除crime记录的操作,使用上下文菜单(context menu)来处理则更合适。因为删除记录的操作需要知道上下文信息,即应该删除哪一条crime记录。第18章,我们将学习如何使用上下文菜单。

本章的选项菜单以及第18章的上下文菜单均需要一些字符串资源。参照代码清单16-1,将这两章所需的字符串资源添加到string.xml文件中。虽然现在可能还不太明白这些新增的字符串资源,但有必要现在就完成添加。这样,在需要它们的时候,就可以直接使用,而无需停下手头的工作。

代码清单16-1 为菜单添加字符串资源(res/values/strings.xml)

image

使用 AppCompatActivity

为了使用Toolbar(工具栏),我们需要将SingleFragmentActivity.java和CrimePagerActivity.java的父类更改为AppCompatActivity.因为AppCompatActivity也是FragmentActivity的子类,所以仅需修改父类即可。

参照代码清单16-1-1修改SingleFragmentActivity.java代码

代码清单16-1-1 修改父类为AppCompatActivity(SingleFragmentActivity.java)

image

参照代码清单16-1-2修改CrimePagerActivity.java代码

代码清单16-1-2修改父类为AppCompatActivity(CrimePagerActivity.java)

image

16.1.1 在XML文件中定义选项菜单

菜单是一种类似于布局的资源。创建一个定义菜单的XML文件,然后将其放置在项目的res/menu目录下。Android会自动生成该XML文件的对应资源ID,以供在代码中生成菜单之用。

在工程中,首先找到res目录下menu子目录。然后右键单击menu子目录,选择New → Menu Resource File菜单项。在弹出的窗口界面,确保选择了Menu文件资源类型,并命名新建文件为fragment_crime_list.xml,如图16-3所示。

image 

16-3 创建选项菜单文件

打开新建的fragment_crime_list.xml, 参照代码清单16-2,添加新的item元素。

代码清单16-2 创建菜单资源(fragment_crime_list.xml)

image

showAsAction属性用于指定菜单选项是显示在Toolbar上,还是隐藏到溢出菜单(overflow menu)中。该属性当前设置为ifRoomwithText的一个组合值。因此,只要空间足够,菜单项图标及其文字描述都会显示在操作栏上。如空间仅够显示菜单项图标,则不会显示文字描述。如空间大小不够任何一项显示,则菜单项会被转移隐藏到溢出菜单中. 

16.1.2 创建选项菜单

在代码中,Activity类提供了管理选项菜单的回调函数onCreateOptionsMenu(Menu)方法。Android会在调用onCreate()-> onResume() 后,第一次显示menu前调用onCreateOptionsMenu(Menu)方法。

然而,按照CriminalIntent应用的设计, 选项菜单相关的回调函数需在fragment而非activity里实现。不用担心,Fragment也有自己的一套选项菜单回调函数。稍后,我们会在CrimeListFragment中实现这些方法。以下为创建选项菜单和响应菜单项选择事件的两个回调方法:

public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)

public boolean onOptionsItemSelected(MenuItem item)

在CrimeListFragment.java中,覆盖onCreateOptionsMenu(Menu, MenuInflater)方法,inflate在fragment_crime_list.xml中定义的菜单,如代码清单16-3所示。

代码清单16-3 Inflating选项菜单(CrimeListFragment.java)

image 

在以上方法中,调用MenuInflater.inflate(int, Menu)方法并传入菜单文件的资源ID,我们将文件中定义的菜单项目填充到Menu实例中。

注意,我们调用了超类的onCreateOptionsMenu( )方法。虽然不是必须的,但作为一种约定的开发规范,我们推荐这么做。通过该超类方法的调用,任何超类定义的选项菜单功能在子类方法中也能获得应用。不过,这里的超类方法调用仅仅是遵循约定而已,因为Fragment超类的onCreateOptionsMenu()方法什么也没做。

FragmentonCreateOptionsMenu(Menu, MenuInflater)方法是由FragmentManager负责调用的。因此,当activity接收到来自操作系统的onCreateOptionsMenu()方法回调请求时,我们必须明确告诉FragmentManager:其管理的fragment应接收onCreateOptionsMenu()方法的调用指令。

要通知FragmentManager,需调用以下方法:

public void setHasOptionsMenu(boolean hasMenu)

CrimeListFragment.onCreate()方法中,让FragmentManager知道CrimeListFragment需接收选项菜单方法回调的方法,如代码清单16-4所示。

代码清单16-4 调用SethasOptionsMenu方法(CrimeListFragment.java)

image 

运行CriminalIntent应用,查看新创建的选项菜单,如图16-5所示。

image 

16-5 显示在操作栏上的菜单项图标

菜单项的标题“新手记”怎么没有显示?大多数设备在竖直模式下屏幕空间都有限,因此,应用的操作栏上只够显示菜单项图标。“长按”Toolbar上的菜单图标,可弹出菜单标题,如图16-6所示。

image 

16-6 长按操作栏上的图标,显示菜单项标题

水平模式下,操作栏上会有足够的空间同时显示菜单图标和菜单项标题,如图16-7所示。

image 

16-7 同时显示在操作栏上的菜单图标和菜单标题

16.1.3 响应菜单项选择

为响应用户点击[新手记]菜单项,需实现新方法以添加新的Crime到crime数组列表中。在CrimeLab.java中,新增以下方法,实现添加Crime到数组列表中,如代码清单16-5所示。

代码清单16-5 添加新的crime(CrimeLab.java)

image 

既然可以手动添加crime记录,也就没必要再让程序自动生成100条crime记录了。在CrimeLab.java中,删除生成随机crime记录的代码,如代码清单16-6所示。

代码清单16-6 再见,随机crime记录!(CrimeLab.java)

image 

用户点击选项菜单中的菜单项时,fragment会收到onOptionsItemSelected(MenuItem)方法的回调请求。该方法接受的传入参数是一个描述用户选择的MenuItem实例。

尽管当前的选项菜单只包含一个菜单项,但通常菜单可包含多个菜单项。通过检查菜单项ID,可确定被选中的是哪一个菜单项,然后做出相应的响应。代码中使用的菜单项ID实际就是在菜单XML定义文件中赋予菜单项的资源ID。

在CrimeListFragment.java中,实现onOptionsItemSelected(MenuItem)方法响应菜单项的选择事件。在该方法中,创建一个新的Crime实例,并将其添加到CrimeLab中,然后启动一个CrimePagerActivity实例,让用户可以编辑新创建的Crime记录,如代码清单16-7所示。

代码清单16-7 响应菜单项选择事件(CrimeListFragment.java)

image 

注意,onOptionsItemSelected(MenuItem)方法返回的是布尔值。一旦完成菜单项事件处理,应返回true值以表明已完成菜单项选择需要处理的全部任务。另外,case表达式中,如果菜单项ID不存在,默认的超类版本方法会被调用。

运行CriminalIntent应用,尝试使用选项菜单,添加一些crime记录并对它们进行编辑。

16.2 实现层级式导航

目前为止,CriminalIntent应用主要依靠手机提供的“后退键”在应用内返回。使用后退键的导航又称为临时性导航,只能返回到上一次的用户界面。而 Ancestral navigation,有时也称为层级式导航(hierarchical navigation),可逐级向上在应用内导航。

本节中,针对显示在CrimePagerActivity操作栏上的应用图标,我们将编码使其具有向上按钮的功能。点击该图标,可回退至crime列表界面。

16.2.1 启用应用图标的导航功能

通常,应用图标一旦启用了向上导航按钮的功能,在应用图标的左边会显示一个如图16-9所示的向左指向图标。

image

16-9 带有向上导航按钮的操作栏

为启用应用图标向上导航按钮的功能,并在fragment视图上显示向左的图标,须调用以下方法设置fragment的DisplayHomeAsUpEnabled属性:

public abstract void setDisplayHomeAsUpEnabled(boolean showHomeAsUp)

该方法来自于API 11级,因此需进行系统版本判断保证应用向下兼容,并使用@TargetApi(11)注解阻止Android Lint报告兼容性问题。

CrimeFragment.onCreateView(...)中,调用setDisplayHomeAsUpEnabled(true)方法,如代码清单16-8所示。

代码清单16-8 启用向上导航按钮(CrimeFragment.java)

image 

16.2.2 响应向上按钮

如同响应选项菜单项那样,可通过覆盖onOptionsItemSelected(MenuItem)方法的方式,响应已启用向上按钮功能的应用图标。因此,首先应通知FragmentManager:CrimeFragment将代表其托管activity实现选项菜单相关的回调方法。如同前面对CrimeListFragment的处理一样,在CrimeFragment.onCreate(...)方法中,调用setHasOptionsMenu(true)方法,如代码清单16-9所示。

代码清单16-9 开启选项菜单处理(CrimeFragment.java)

image 

无需在XML文件中定义或生成应用图标菜单项。它已具有现成的资源ID:android.R.id.home。在CrimeFragment.java中,覆盖onOptionsItemSelected(MenuItem)方法,响应用户对该菜单项的点击事件,如代码清单16-10所示。

代码清单16-10 响应应用图标(Home键)菜单项(CrimeFragment.java)

image

使用NavUtils便利类与manifest配置文件中的元数据,实现点击向上按钮返回至crime列表界面功能。

先来处理元数据。打开AndroidManifest.xml文件,在CrimePagerActivity声明中添加新的meta-data属性,指定CrimePagerActivity的父类为CrimeListActivity,如代码清单16-11所示。

代码清单16-11 添加父activity元数据属性(AndroidManifest.xml)

image

把元数据标签想象为张贴在activity上的一个便利贴。类似这样的便利贴信息都保存在系统的PackageManager中。只要知道便利贴的名字,任何人都可以获取它的内容。也可创建自己的名-值(name-value)对以便在需要的时候获取它们。这种特别的名-值对由NavUtils类定义,这样它就能知道谁是指定activity的父类,配合以下NavUtils类方法一起使用尤其有用:

public static void navigateUpFromSameTask(Activity sourceActivity)

CrimeFragment.onOptionsItemSelected(...)方法中,首先通过调用NavUtils.getParentActivityName(Activity)方法,检查元数据中是否指定了父activity。如指定有父activity,则调用navigateUpFromSameTask(Activity)方法,导航至父activity界面。如代码清单16-12所示。

代码清单16-12 使用NavUtils类(CrimeFragment.java)

image 

如元数据中未指定父activity,则为避免误导用户,无需再显示向左的箭头图标。回到onCreateView(...)方法中,在调用setDisplayHomeAsUpEnabled(true)方法前,先检查父activity是否存在,如代码清单16-13所示。

代码清单16-13 控制导航图标的显示(CrimeFragment.java)

image

为什么使用NavUtils类要好于手动启动activity?首先,NavUtils类的实现代码既简洁又优雅。其次,使用NavUtils类也可实现在manifest配置文件中统一管理activity间的关系。如果activity间的关系发生改变,无需费力地去修改Java代码,我们只要简单修改配置文件中的一行代码即可。

运行CriminalIntent应用。创建新的crime记录,然后点击应用图标,返回至crime列表界面。

16.3 可选菜单项

添加一个菜单项实现显示或隐藏CrimeListActivity操作栏的子标题。

16.3.1 创建可选菜单XML文件

参照代码清单16-14,新增一个显示为Show Subtitle的菜单项。如空间足够,它将显示在操作栏上。

代码清单16-14 添加Show Subtitle菜单项(res/menu/fragment_crime_list.xml)

image 

我们计划在点击“显示子标题”菜单项后,显示当前“陋习”的条数。为此,新建一个 updateSubtitle()方法,它将设置子标题的内容。如代码清单16-15所示。

代码清单16-15 新建一个 updateSubtitle()方法(CrimeListFragment.java)

image

接下来,添加响应点击“显示子标题”事件的代码。如代码清单16-15-1所示。

代码清单16-15-1 在子标题处显示“陋习”数量(CrimeListFragment.java)

image

运行CriminalIntent应用,点击“显示子标题”,你将看到当前“陋习”数量显示在子标题处.

16.3.2 切换菜单项标题

操作栏上的子标题显示后,菜单项标题依然显示为“显示子标题”。如果菜单项标题的切换与子标题的显示或隐藏能够联动,用户体验会更好。

实现子标题显示和隐藏的联动功能比较好的方法是,修改onCreateOptionsMenu(…)的“显示子标题”菜单项以及当用户点击“显示子标题”后重新设置Toolbar。

首先,新建一个成员变量跟踪子标题的显示状态,如代码清单16-16-1所示。

代码清单16-16-1 跟踪子标题的显示状态(CrimeListFragment.java)

image

接下来,修改onCreateOptionsMenu(…)的“显示子标题”菜单项以及当用户点击“显示子标题”后重新设置Toolbar,如代码清单16-16-2所示。

代码清单16-16-2 修改onCreateOptionsMenu(…),重新设置Toolbar(CrimeListFragment.java)

image

在代码清单16-16-2中,我们调用了Activity的invalidateOptionsMenu()方法,这样会触发Activity再次调用onCreateOptionMenu()方法重新生成 Toolbar,从而更新“子标题”显示或隐藏。

最后,根据mSubtitleVisible的值,显示或隐藏子标题. 如代码清单16-16-3所示

代码清单16-16-3 修改onCreateOptionsMenu(…),重新设置Toolbar(CrimeListFragment.java)

image

运行CriminalIntent应用,点击“显示子标题”和“隐藏子标题。

16.3.3 还有两个问题

第一个问题,当你新建一个“陋习”,然后点击“后退键”返回到CrimeListActivity后,子标题消失了。

第二个问题,是经典的设备旋转问题。子标题显示后,旋转设备,这时因为用户界面的重新生成,显示的子标题会消失。

我们把这两个问题放到一起解决。

首先修改mSubtitleVisible定义方式,改为静态变量,这样返回到CrimeListActivity后,不会再将mSubtitleVisible设为默认值false(这种方法不太好,不清晰,需要优化),如代码清单16-17所示:

代码清单16-17 跟踪子标题的显示状态(CrimeListFragment.java)

image

之前我们讨论过,如何利用setRetainInstance(true)来处理设备变化时保存对象,现在我们使用它解决旋转问题,如代码清单16-18所示。

代码清单16-18 调用setRetainInstance(true)来保存对象(CrimeListFragment.java

image

接下来,需要查看设备旋转后是否应该显示子标题。在CrimeListFragment.java中,覆盖onCreateView(...)方法,根据变量mSubtitleVisible的值确定是否要设置子标题,如代码清单16-19所示。

代码清单16-19 根据变量mSubtitleVisible的值设置子标题(CrimeListFragment.java)

image

运行CriminalIntent应用。显示子标题并旋转设备,可以看到子标题在重新创建的视图中依然能正确显示。

posted @ 2015-08-25 20:47  jlxuqiang  阅读(597)  评论(0编辑  收藏  举报