在本练习中,你将学习如何添加第二个Activity至你的记事本应用程序中,该Activity可以让用户创建、编辑以及删除便笺。这个Activity假定通过用户的输入响应创建新便笺,并将其自身打包至一个由intent提供的Bundle返回值中。这个练习的目标是:
用NotepadCodeLab文件夹下Notepadv2目录里的资源创建一个新Android 工程,正如与练习一中做过类似的一样。如果你看到一个有关AndroidManifest.xml,或有关android.zip文件的错误信息,请右击工程选择Android Tools > Fix Project Properties,即可修正错误。
打开Notepadv2工程并查看如下内容:
添加一个删除一条便笺的菜单项:
menu.add(0, DELETE_ID, 0, R.string.menu_delete);
@Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(0, INSERT_ID, 0, R.string.menu_insert); menu.add(0, DELETE_ID, 0, R.string.menu_delete); return true; }
在接口onMenuItemSelected()中,为参数DELETE_ID添加一个新分支语句:
mDbHelper.deleteNote(getListView().getSelectedItemId()); fillData(); return true;
getListView().getSelectedItemId()
. 接口onMenuItemSelected()的完整定义如下:
@Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch(item.getItemId()) { case INSERT_ID: createNote(); return true; case DELETE_ID: mDbHelper.deleteNote(getListView().getSelectedItemId()); fillData(); return true; } return super.onMenuItemSelected(featureId, item); }
在本例中,我们的intent用了一个特定名称的类。我们也可以创建Intents而不必清楚地知道控制此Intent的应用程序究竟是哪个,因为正如我们对类中的starting intents所了解的那样,该类既可以在我们自己的应用程序中也可以在另一应用程序中。
例如,我们可能想打开一个浏览器中的一个页面,这里我们仍将用到一个Intent。但是我们用一个预先定义好的Intent常量和一个用于描述我们目的的URI内容,而非某一特定的类,来控制此Intent。更多信息请参看android.content.Intent一节。
实现接口 createNote()
:
为创建一个便笺(ACTIVITY_CREATE)而用一个名为NoteEdit的类创建一个新Intent。紧接着调用接口startActivityForResult()来启动该Intent。:
Intent i = new Intent(this, NoteEdit.class); startActivityForResult(i, ACTIVITY_CREATE);
这种形式的Intent调用是面向我们Activity中特定类的,本例中该特定类即为NoteEdit。由于Intent类需要通过Android 操作系统来发送请求信息,我们还得提供一个Context(this)。
触发该Intent的接口startActivityForResult()是在一个新Activity完成后,才在我们的Activity中被调用的。 在我们的Activity中用于接收回调的接口名为onActivityResult(),我们将在稍后的步骤中实现它。另一调用Activity的接口名为startActivity(),但这是一种一劳永逸的调用方式(fire-and-forget)——在该调用方式下,当前的Activity并不会收到另一Activity已完成的通知,同时我们无法从被调Activity的接口startActivity()返回有关该Activity的任何结果信息。
不必担心NoteEdit目前还不存在这一事实,我们很快就将完善之。
实现覆写接口onListItemClick():
onListItemClick()是一个我们将要覆写的回调函数。它在用户点击便笺列表中的某一项时调用。它一共有四个参数,分别是:调用该接口的ListView对象,所点击的View(存在于ListView中),所点击的position(该项在列表中的位置),所点击的某一项的行索引(mRowId)。在本例中我们先忽略前两个参数(当前我们只可能有一个ListView),以及参数行索引。我们的着眼点全在用户所选中的某一项在列表中所在的位置position。通过这个参数,我们可以从相应的行得到想要的数据,并将此数据打包发送至NoteEdit Activity。
在该回调函数接口的实现中,我们用类NoteEdit创建一个Intent来修改便笺内容,接着添加数据至一个作为参数传递给被调用Activity的Intent的附加包中。我们用其传递当前正在编辑的便笺的标题、正文以及行索引。最后,它将调用接口startActivityForResult()来启动该Intent。以下是接口onListItemClick()实现的完整代码:
super.onListItemClick(l, v, position, id); Cursor c = mNotesCursor; c.moveToPosition(position); Intent i = new Intent(this, NoteEdit.class); i.putExtra(NotesDbAdapter.KEY_ROWID, id); i.putExtra(NotesDbAdapter.KEY_TITLE, c.getString( c.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE))); i.putExtra(NotesDbAdapter.KEY_BODY, c.getString( c.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY))); startActivityForResult(i, ACTIVITY_EDIT);
注意: 我们在接口定义的开始处就将类成员变量mNotesCursor赋给一个局部变量。这可视为对Android 应用程序代码的优化。因为访问一个局部变量其效率远高于访问Dalvik 虚拟机中的一个类成员变量。所以通过这种方式处理,我们只需访问类成员变量一次,而访问局部变量五次,就可使得常规处理更高效。所以极力推荐尽可能用此方法优化你的Android应用程序代码。
上述接口createNote()和onListItemClick()采用的是异步触发Intent的方式。我们得靠一个句柄来回调,所以以下是接口onActivityResult()的实现部分:
onActivityResult()是一个只在一个Activity有返回结果时才被调用的覆写接口(记住!一个Activity只有在用接口startActivityForResult() 启动时才有一个返回结果)。用于回调的一些参数如下:
requestCode
— 是Intent触发时确定的初始请求类型码(对我们而言,请求类型码无非就是ACTIVITY_CREATE或者ACTIVITY_EDIT这两者之一)。 resultCode
— 调用返回的结果(或错误)类型,如果一切正常则该类型值为零,但若有失败的异常情况则有对应的非零错误类型码表示。有标准的结果类型码可用,同时你也可以自定义一些结果类型码常量,以满足自己特定的问题。intent
— 这是一个由Activity返回的结果创建的Intent。它用于在Intent中的’Extras’返回数据。 接口startActivityForResult()和onActivityResult()的结合可视为一种异步的远程进程调用(RPC: Remote Procedure Call),形成了一个Activity启动另一个Activity并彼此共享服务的推荐方式。
以下为方法 onActivityResult()完整的定义
:
super.onActivityResult(requestCode, resultCode, intent); Bundle extras = intent.getExtras(); switch(requestCode) { case ACTIVITY_CREATE: String title = extras.getString(NotesDbAdapter.KEY_TITLE); String body = extras.getString(NotesDbAdapter.KEY_BODY); mDbHelper.createNote(title, body); fillData(); break; case ACTIVITY_EDIT: Long mRowId = extras.getLong(NotesDbAdapter.KEY_ROWID); if (mRowId != null) { String editTitle = extras.getString(NotesDbAdapter.KEY_TITLE); String editBody = extras.getString(NotesDbAdapter.KEY_BODY); mDbHelper.updateNote(mRowId, editTitle, editBody); } fillData(); break; }
ACTIVITY_CREATE
也控制
ACTIVITY_EDIT
的Activity结果。
fillData()
最后的接口确保每一个都更新完毕。
练习提供的屏幕布局文件note_edit.xml是我们将要建立的应用中最复杂的一个了,但这并不意味着你若想在真正的Android应用中用到一个布局,非得和这个有多接近才行。
创建一个良好的界面是部分艺术、部分科学,剩余的就是孜孜以求了。掌握Android layout 是创建一个赏心悦目的Android应用程序的核心部分。
不妨看看 View Gallery 中的一些例子,了解如何运用它们。范例工程ApiDemos是学习如何创建千姿百态的屏幕布局的极好资源。
打开文件note_edit.xml并浏览一二,这是有关便笺编辑界面方面的代码。
这是目前我们处理过的最复杂的界面布局了,这里给出的文件是为了避免在代码排版中出现淡入淡出的问题。(XML对大小写及结构有非常严格的限制,而这些错误往往又都是屏幕布局问题的根源所在。)
这里用到了一个之前我们从未见过的新参数: android:layout_weight
(在本例中该参数每种情况下都赋值为1)
layout_weight用于给一个线性布局中的诸多视图的重要度赋值。所有的视图都有一个layout_weight值,默认为零,意思是需要显示多大的视图就占据多大的屏幕空间。若赋一个高于零的值,则将父视图中的可用空间分割,分割大小具体取决于每一个视图的layout_weight值以及该值在当前屏幕布局的整体layout_weight值和在其它视图屏幕布局的layout_weight值中所占的比率而定。
举个例子:比如说我们在水平方向上有一个文本标签和两个文本编辑元素。该文本标签并无指定layout_weight值,所以它将占据需要提供的最少空间。 如果两个文本编辑元素每一个的layout_weight值都设置为1,则两者平分在父视图布局剩余的宽度(因为我们声明这两者的重要度相等)。如果两个文本编辑元素其中第一个的layout_weight值设置为1,而第二个的设置为2,则剩余空间的三分之一分给第一个,三分之二分给第二个(因为我们声明第二个有较之第一个更高的重要度)。
这个布局也演示了如何在其它布局中嵌套多个布局以实现更复杂更漂亮的布局。在本例中,一个水平方向上的线性布局嵌套在另一垂直方向上的布局中,以使标题标签和文本字段在水平方向上挨个对齐。
创建一个NoteEdit类以扩展android.app.Activity。
这是我们第一次不用Android Eclipse插件的帮助来创建一个Activity。当按这种方式创建时,方法onCreate()并未自动覆写。很难想象一个没有覆写其onCreate()方法的Activity会是什么样子,所以这是接下来你先得完成的首要任务。
这样方法就出现在你的类中了。
为类NoteEdit的方法onCreate()实现其定义:
该方法将为我们的新Activity设置一个名为”编辑便签”的标题(这是一个在文件strings.xml中定义的字符串),同时通过布局文件note_edit.xml设置便笺内容视图。我们可以得到便笺标题和文本正文视图,以及确认按钮的句柄。如此一来,我们就可以通过这些句柄设置得到便签的标题和正文,并将确认按钮绑定至响应用户按下该按钮的事件。
然后,我们可以解开因传入于所调用的Intent中的附加包中,而已传到Activity里的参数值,并用这些值预填充标题和便签文本正文及视图,这样用户就可以编辑它们了。接下来,我们可以获取和存储便笺行索引(mRowId)这一值,从而可以知道用户当前正在编辑哪一条便签。
setContentView(R.layout.note_edit);
可以通过在R类中声明的与之关联的ID找到这些资源,然后需要将它转换为正确的视图类型 (两个文本视图为EditText 类, 确认按钮为Button类)
mTitleText = (EditText) findViewById(R.id.title); mBodyText = (EditText) findViewById(R.id.body); Button confirmButton = (Button) findViewById(R.id.confirm);
需要注意的是mTitleText和mBodyText这两个是类成员变量(你得先在类中声明才能在此使用)
mRowId = null; Bundle extras = getIntent().getExtras(); if (extras != null) { String title = extras.getString(NotesDbAdapter.KEY_TITLE); String body = extras.getString(NotesDbAdapter.KEY_BODY); mRowId = extras.getLong(NotesDbAdapter.KEY_ROWID); if (title != null) { mTitleText.setText(title); } if (body != null) { mBodyText.setText(body); } }
Listener也许是UI实现中让人倍感困惑的方面之一,但在本例中我们要实现的效果却很简单,就是在用户点确定按钮时调用onClick()方法。用其做一些事,并将便签被编辑后的一些值返回给Intent这一调用者。我们通过名为匿名的内部类来实现这一目的,这个类除非你之前见过,否则第一次看到确实会让人觉得有点晕。但是你确实需要将困惑的心态暂且抛开,因为将来有机会可以参看这一段代码,并将了解到如何创建一个Listener并将其关联至一个按钮(Listener是Java开发中一个常见的术语,尤其是在UI的设计实现中)。下面代码是一个空的Listener:
confirmButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { } });
在我们的Listener中完成方法onClick()的实现部分:
当用户按下确定按钮,这个接口就会运行。我们希望该方法能获取被编辑的便签文本中的标题和文本字段,并且将这些数据放到一个作为返回值的包中,这样这些数据就可以被传递回最初触发这个NoteEdit Activity的Activity中,如果该接口响应的是一个编辑而不是创建操作的话,我们还需将行行索引ID(rowed)也放至作为返回值的包中,这样Notepadv2 类就可以保存对便签所做的编辑。
Bundle bundle = new Bundle(); bundle.putString(NotesDbAdapter.KEY_TITLE, mTitleText.getText().toString()); bundle.putString(NotesDbAdapter.KEY_BODY, mBodyText.getText().toString()); if (mRowId != null) { bundle.putLong(NotesDbAdapter.KEY_ROWID, mRowId); }
Intent mIntent = new Intent(); mIntent.putExtras(bundle); setResult(RESULT_OK, mIntent); finish();
方法onCreate() (另加支持该类的一些字段) 的完整实现如下:
private EditText mTitleText; private EditText mBodyText; private Long mRowId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.note_edit); mTitleText = (EditText) findViewById(R.id.title); mBodyText = (EditText) findViewById(R.id.body); Button confirmButton = (Button) findViewById(R.id.confirm); mRowId = null; Bundle extras = getIntent().getExtras(); if (extras != null) { String title = extras.getString(NotesDbAdapter.KEY_TITLE); String body = extras.getString(NotesDbAdapter.KEY_BODY); mRowId = extras.getLong(NotesDbAdapter.KEY_ROWID); if (title != null) { mTitleText.setText(title); } if (body != null) { mBodyText.setText(body); } } confirmButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { Bundle bundle = new Bundle(); bundle.putString(NotesDbAdapter.KEY_TITLE, mTitleText.getText().toString()); bundle.putString(NotesDbAdapter.KEY_BODY, mBodyText.getText().toString()); if (mRowId != null) { bundle.putLong(NotesDbAdapter.KEY_ROWID, mRowId); } Intent mIntent = new Intent(); mIntent.putExtras(bundle); setResult(RESULT_OK, mIntent); finish(); } }); }
AndroidManifest.xml文件是Android操作系统理解你应用程序的方式。此文件定义了在启动和设置时在何处(或者是否)显示应用程序、该应用程序定义了哪些活动(acivities)、服务(services)和内容提供者(Content Providers)、以及该应用程序能接收哪些Intent 等等诸如此类的应用程序分类目录。
欲知有关该文件的更多信息,请参考文档 AndroidManifest.xml
最后,在manifest文件中定义了新的Activity:
在新Activity被Android操作系统识别之前,它需要在AndroidManifest.xml文件中有自己的Activity 入口(Activity Entry)。这样Android操作系统就知道该Activity在AndroidManifest.xml文件中并可以被其调用。我们也可以确定该Activity在此处实现了哪些IntentFilter,但是我们现在先略过此部分,暂且只让Android操作系统知道该Activity已定义了。
在Eclipse 插件中包括一个可以很方便地编辑AndroidManifest文件的Manifest编辑器,我们即将用到该插件。若你更愿意直接编辑文件或不用Eclipse插件的话,请看下面框中有关如何不用新的Manifest编辑器实现编辑Manifest文件的信息。
如果在对话框顶部你若看到一个单选框(radiobutton)的话,选择顶部为”在应用程序顶层创建一个新元素”的单选框。
Android Manifest编辑器可以让你在文件中添加更复杂的Activity入口,同时也看看其它一些可用的选项(但千万别选择,否则这些选项将添加到你的Manifest文件中)。当你迈向更高级的Android应用时,该编辑器帮助你明白及如何改变AndroidManifest.xml文件。
如果你更愿意直接编辑AndroidManifest.xml文件,只需简单打开该文件并查看其资源(用Eclipse 编辑器中的AndroidManifest.xml选项卡直接查看资源代码),然后按如下方式修改代码:
<activity android:name=".NoteEdit"></activity>
这一句应放在下面这句的后面
</activity>
for the .Notepadv2
activity.
现在开始运行它吧!
你现在应该已可以从菜单添加真正的便签了,也可以删除一个已存在的便签了。但需要注意的是为了按顺序删除某条便签,你必须首先用设备上的方向控制来高亮显示它。还有就是,若从列表中选择一条便签应该同时引出可以让用户编辑的便签编辑器。当结束编辑按确定按钮后,则将编辑后的效果保存至数据库中。
你可以从压缩文件Notepadv2Solution中得到有关本练习的答案并与你自己的实验代码进行对比。
现在试着编辑一条便签,然后点击模拟器上的后退按钮而非确定按钮(后退按钮就在菜单按钮下面)。你将发现突然蹦出一个错误。显然我们的应用程序还有些问题。更糟糕的是,如果你在对便签做了一些编辑操作后点击后退按钮,你再返回到记事本中查看你修改过的便签时,你将发现你所做的所有编辑操作的效果全部丢失了。在下一阶段练习中,我们将修复这些问题。
一旦你准备好了,就继续进行下一个练习——Tutorial Exercise 3 ——你将从中学习通过在便签编辑Activity中引入一个合理的生命周期来解决后退按钮和编辑信息丢失的问题。
Gerald06@163.com Reviewed on 20.Oct.08 1:20 AM.
Thanks for one of the Android@googlegroups.com Localization Translation teammate TOMCai~ 's generous & enthusiastic help to take me out from the mental torture.