第 18 章 上下文菜单与上下文操作模式
请参考教材,全面理解和完成本章节内容... ...
我们将为应用实现长按列表项删除crime记录的功能。删除一条crime记录是一种上下文操作(contextual action),所谓“上下文”是指操作与某个列表项相关的,而非整个屏幕相关联的。
目前,上下文操作主要是通过上下文操作栏呈现的,它位于activity的Toolbar之上,上下文操作栏为用户提供了各种操作,如图18-1所示。
图18-1 长按列表项删除一条crime记录
第16章我们已看到,对于选项菜单而言,处理不同API级别的兼容性问题很简单:只需定义一种菜单资源并实现一组菜单相关的回调方法,不同设备上的操作系统会自行决定菜单项的显示方式。
而对于上下文操作,事情就没那么简单了。虽然还是定义一种菜单资源,但我们必须实现两组不同的回调方法,一组用于上下文操作栏,一组用于浮动上下文菜单。
本章,我们将在运行API 11级及以上系统版本的设备上实施一个上下文操作,然后,再在Froyo及Gingerbread设备上实施一个浮动上下文菜单。
18.1 定义上下文菜单资源
在res/menu/目录中,以menu为根元素,新建名为crime_list_item_context.xml的菜单资源文件。然后参照代码清单18-1添加需要的菜单项。
代码清单18-1 用于crime列表的上文菜单(crime_list_item_context.xml)
以上定义的菜单资源将用于上下文操作栏和浮动上下文菜单的实施。
18.2 实施浮动上下文菜单
首先,我们来创建浮动上下文菜单。Fragment
的回调方法类似于第16章中用于选项菜单的回调方法。要实例化生成一个上下文菜单,可使用以下方法:
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)
要响应用户的上下文菜单选择,可实现以下Fragment
方法:
public boolean onContextItemSelected(MenuItem item)
18.2.1 创建上下文菜单
在CrimeListFragment.java中,实现onCreateContextMenu(...)
方法,实例化菜单资源,并用它填充上下文菜单,如代码清单18-2所示。
代码清单18-2 创建上下文菜单(CrimeListFragment.java)
不像onCreateOptionsMenu(...)
方法,以上菜单回调方法不接受MenuInflater
实例参数,因此,我们应首先获得与CrimeListActivity
关联的MenuInflater
。然后调用MenuInflater.inflate(...)
方法,传入菜单资源ID和ContextMenu
实例,用菜单资源文件中定义的菜单项填充菜单实例,从而完成上下文菜单的创建。
当前我们只定义了一个上下文菜单资源,因此,无论用户长按的是哪个视图,菜单都是以该资源实例化生成的。假如定义了多个上下文菜单资源,通过检查传入onCreateContextMenu(...)
方法的View
视图ID,我们可以自由决定使用哪个资源来生成上下文菜单。
18.2.2 为上下文菜单登记视图
默认情况下,长按视图不会触发上下文菜单的创建。要触发菜单的创建,必须调用以下Fragment
方法为浮动上下文菜单登记一个视图:
public void registerForContextMenu(View view)
该方法需传入触发上下文菜单的视图。
在CriminalIntent应用里,我们希望点击任意列表项,都能弹出上下文菜单。这岂不是意味着需要逐个登记列表项视图吗?不用那么麻烦,直接登记ListView
视图即可,然后它会自动登记各个列表项视图。
在CrimeListFragment.onCreateView(...)
方法中,引用并登记ListView
,如代码清单18-3所示。
代码清单18-3 为上下文菜单登记ListView
(CrimeListFragment.java)
在onCreateView(...)
方法中,使用android.R.id.list
资源ID获取ListFragment
管理着的ListView
。ListFragment
也有一个getListView()
方法,但在onCreateView(...)
方法中却无法使用。这是因为,在onCreateView()
方法完成调用并返回视图之前,getListView()
方法返回的永远是null值。
运行CriminalIntent应用,长按任意列表项,可弹出含“删除”菜单项的浮动菜单,如图18-2所示。
图18-2 长按列表项弹出上下文菜单项
18.2.3 响应菜单项选择
Delete菜单项要可用,需要一个能够从模型层删除crime数据的方法。在CrimeLab.java中,新增deleteCrime(Crime)
方法,如代码清单18-4所示。
代码清单18-4 新增删除crime的方法(CrimeLab.java)
然后,在onContextItemSelected(MenuItem)
方法中处理菜单项选择事件。MenuItem
有一个资源ID可用于识别选中的菜单项。除此之外,还需明确具体要删除的crime对象,才能确定用户想要删除crime数据的意图。
可调用MenuItem
的getMenuInfo()
方法,获取要删除的crime对象的信息。该方法返回一个实现了ContextMenu.ContextMenuInfo
接口的类实例。
在CrimeListFragment
中,新增onContextItemSelected(MenuItem)
实现方法,使用menu信息和adapter,确定被长按的Crime
对象,然后从模型层数据中删除它,如代码清单18-5所示。
注:需手工 import android.widget.AdapterView.AdapterContextMenuInfo;
代码清单18-5 监听上下文菜单项选择事件(CrimeListFragment.java)
以上代码中,因为ListView
是AdapterView
的子类,所以getMenuInfo()
方法返回了一个AdapterView.AdapterContextMenuInfo
实例。然后,将getMenuInfo()
方法的返 回结果进行类型转换,获取选中列表项在数据集中的位置信息。最后,使用列表项的位置,获取要删除的Crime对象。
运行CriminalIntent应用,新增一条crime记录,然后长按删除它。(要在模拟器上模拟长按动作,可按下鼠标左键不放直到菜单弹出。)
18.3 实施上下文操作模式
通过浮动上下文菜单删除crime记录的实现代码,可在任何Android设备上运行。例如,图18-2为Jelly Bean系统设备上弹出的浮动菜单。
然而,在新系统设备上,长按视图进入上下文操作模式是提供上下文操作的主流方式。屏幕进入上下文操作模式时,上下文菜单中定义的菜单项会出现在覆盖着操作栏的上下文操作栏上,如图 18-3所示。相比浮动菜单,上下文操作栏不会遮挡屏幕,因此是更好的菜单展现方式。
图18-3 长按列表项出现上下文操作栏
上下文操作栏的实现代码不同于浮动上下文菜单。此外,上下文操作栏实现代码所使用的类和方法不支持Froyo或Gingerbread等老系统,因此必须保证仅支持新系统的代码在老系统上不会被调用。
18.3.1 实现列表视图的多选操作
列表视图进入上下文操作模式时,可开启它的多选模式。多选模式下,上下文操作栏上的任何操作都将同时应用于所有已选视图。
在CrimeListFragment.onCreateView(...)
方法中,设置列表视图的选择模式为CHOICE_MODE_MULTIPLE_MODAL,如代码清单18-6所示。最后,为处理兼容性问题,记得使用编译版本常量,将登记ListView
的代码与设置选择模式的代码区分开来。
代码清单18-6 设置列表视图的选择模式(CrimeListFragment.java)
18.3.2 列表视图中的操作模式回调方法
接下来,为ListView
设置一个实现AbsListView.MultiChoiceModeListener
接口的监听器。该接口包含以下回调方法,视图在选中或撤销选中时会触发它:
public abstract void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked)
MultiChoiceModeListener
实现了另一个接口,即ActionMode.Callback
。用户屏幕进入上下文操作模式时,会创建一个ActionMode
类实例。随后在其生命周期内,ActionMode.Callback
接口的回调方法会在不同时点被调用。以下为ActionMode.Callback
接口中必须实现的四个方法:
public abstract boolean onCreateActionMode(ActionMode mode, Menu menu)
在ActionMode
对象创建后调用。也是实例化上下文菜单资源,并显示在上下文操作栏上的任务完成的地方。
public abstract boolean onPrepareActionMode(ActionMode mode, Menu menu)
在onCreateActionMode(...)
方法之后,以及当前上下文操作栏需要刷新显示新数据时调用。
public abstract boolean onActionItemClicked(ActionMode mode, MenuItem item)
在用户选中某个菜单项操作时调用。是响应上下文菜单项操作的地方。
public abstract void onDestroyActionMode(ActionMode mode)
在用户退出上下文操作模式或所选菜单项操作已被响应,从而导致ActionMode
对象将要销毁时调用。默认的实现会导致已选视图被反选。这里,也可完成在上下文操作模式下,响应菜单项操作而引发的相应fragment更新。
在CrimeListFragment.onCreateView()
方法中,为列表视图设置实现MultiChoiceModeListener
接口的监听器。这里,只需实现onCreateActionMode()
和onActionItemClicked(ActionMode, MenuItem)
方法即可,如代码清单18-7所示。注: import android.view.ActionMode;
代码清单18-7 设置MultiChoiceModeListener
监听器(CrimeListFragment.java)
注意,如使用代码自动补全来创建MultiChoiceModeListener
接口,系统自动产生的onCreateActionMode(...)
存根方法会返回false值。记得将其改为返回true值,因为返回false值会导致操作模式创建失败。
另外要注意的是,在onCreateActionMode(...)
方法中,我们是从操作模式,而非activity中获取MenuInflater
的。操作模式负责对上下文操作栏进行配置。例如,可调用ActionMode.setTitle(...)
方法为上下文操作栏设置标题,而activity的MenuInflater
则做不到这一点。
接下来,在onActionItemClicked(...)
方法中,响应菜单项删除操作,从CrimeLab
中删除一个或多个Crime
对象,然后重新加载显示列表。最后,调用ActionMode.finish()
方法准备销毁操作模式。
运行CriminalIntent应用。长按选择任意列表项,进入上下文操作模式。此时,还要选择其他列表项的话,直接点击即可。而再次点击已选中的列表项则撤销选择。点击删除图标将结束操作模式并返回到刷新后的列表项界面。也可以点击上下文操作栏最左边的取消图标,这将取消操作模式并返回到没有任何变化的列表项界面。
如图18-4所示,尽管功能使用上没有什么问题,但用户的使用体验很糟糕,因为很难看出哪些列表项被选中了。不过,该问题可通过改变已选中列表项背景的方式解决。
图18-4 第二crime记录已被选中
18.3.3 改变已激活视图的显示背景
依据自身的不同状态,有时需要差别化地显示某个视图。CriminalIntent应用中,在列表项处于激活状态时,我们希望能够改变其显示背景。视图处于激活状态,是指该视图已被用户标记为关注处理对象。
基于视图的状态,可使用state list drawable资源来改变其显示背景。state list drawable是一种以XML定义的资源。该资源定义中,我们指定一个drawable(位图或彩图),并列出该drawable对应的状态。(可查阅StateListDrawable
参考手册页,了解更多视图相关状态。)
右键点击res\drawable目录,选择New-> Drawable resource file,创建名为background_activated的资源文件文件。参照代码清单18-8完成内容的添加。
代码清单18-8 简单的state list drawable资源(res/drawable/background_activated.xml)
以上XML文件告诉我们:当引用该drawable资源的视图处于激活状态时,则使用android:drawable
属性值指定的资源;反之,则不采取任何操作。如android:state_activated
的属性值设置为false,则只要视图未处于激活状态,android:drawable
指定的资源都会被使用。
修改res/layout/list_item_crime.xml文件,引用drawable目录下background_activated.xml定义的资源,如代码清单18-9所示。
代码清单18-9 改变列表项的显示背景(res/layout/list_item_crime.xml)
重新运行CriminalIntent应用。这次,已选列表项一眼便能看出了,如图18-5所示。
图18-5 醒目的第二列表项