Notepad 练习1
这个练习里,你将创建一个简单的笔记列表,用户只能添加而不能编辑笔记。练习展示了:
-
ListActivity
的基础,创建和处理菜单选项。 - 如何使用SQLite数据库存储笔记。
- 如何使用SimpleCursorAdapter将数据从数据库游标绑定到ListView 。
- 屏幕布局基础:如何排版一个list view,如何向activity的菜单里添加项目,以及activity如何处理这些菜单被选中的动作。
步骤1
在Eclipse里面打开工程Notepadv1
。
Notepadv1
是一个工程,作为本次练习的起点。它完成了一些初始的基础工作,如果你已经阅读了Hello, World 教程,应该已经熟悉这些基础工作了。
- 创建一个新工程File > New > Android Project.
- 在创建Android新工程对话框里,选择从已有源码创建工程(Create project from existing source)。
- 点击浏览(Browse),然后找到你拷贝的
NotepadCodeLab
目录(从Notepad介绍下载的),然后选择Notepadv1。
- 工程名和其他属性应该都已经给你填好了。你必须选择编译目标 - 我们建议选择已有最低版本的平台作为编译目标。同时在最低SDK版本一栏填写与选择的目标平台相匹配的API等级数。
- 点击结束。应该能在Eclipse的package explorer看到打开的
Notepadv1
工程了。
AndroidManifest.xml
有错误,或者一些跟Android zip文件有关的问题,右键点击工程,选择
Android Tools >
Fix Project Properties. (工程在错误的位置查找库文件,这个操作将帮你修复这个问题。)
步骤2
访问、修改数据
这个练习里,我们使用SQLite数据库来保存数据。这在只有你的应用会访问、修改数据的时候是有效的。如果你希望其他Activity也能访问、修改数据,你必须通过ContentProvider
来提供数据。
如果你感兴趣,可以了解更多关于content providers 或者整个Data Storage的内容。在SDK的samples/
目录下的NotePad示例也讲述了如何创建一个ContentProvider。
查看一下类NotesDbAdapter
- 这个类用来封装SQLite数据库的数据访问,这个数据库将保存我们的笔记数据并允许我们更新它。
在这个类的最上面,定义了一些常量,用来保存数据库里对应的字段名,应用可以通过这些字段名来查找数据。那儿也同时定义了一条数据库创建语句,在数据库不存在的时候创建一个新的数据库。
我们的数据库名称是data
, 里面只有一个表名字是notes
, 表里面有3个字段:_id
,title
和body。
_id
以下划线惯例命名,这个惯例在Android SDK里面多个地方用到,用来帮助追踪状态。在查询或修改数据库(预先判断哪些列等等)的时候通常必须提供_id
。其他两个字段是简单的文本字段,用来保存数据。
NotesDbAdapter
的构造函数拥有一个Context,它允许NotesDbAdapter
跟Android操作系统的某些部分进行交流。类需要以某种方式跟Android系统接触是很正常的事情。Activity是类Context的一个实现,因此通常在需要Context的时候只要从你的Activity传递this
就行了。
open()
方法创建一个DatabaseHelper的实例,DatabaseHelper是类SQLiteOpenHelper 的一个本地实现。open()
方法方法调用getWritableDatabase()为我们创建并打开一个数据库。
close()
就是关闭数据库,释放跟数据库连接相关的资源。
createNote()
接受两个字符串参数:新笔记的标题和内容,然后在数据库里创建这个笔记。假设创建笔记成功,这个方法会返回新创建笔记的行号_id
。
deleteNote()
接受一个给定笔记的行号rowId ,然后从数据库删除这个笔记。
fetchAllNotes()
生成一个查询来返回数据库里所有笔记的游标。query()
的调用很值得研究。第一个参数是要查询的数据库表名(这里的DATABASE_TABLE
是”notes”)。下一个参数是一系列我们需要返回的列名,这里我们要返回_id
, title
和 body
,将它们放到字符串数组里面。剩下的参数依次是:selection
,selectionArgs
, groupBy
, having
和orderBy
.把它们全部设置为null
意味着我们需要返回全部数据,不要分组,以及使用默认排列顺序。需要更详细的内容请参考SQLiteDatabase。
注意: 返回游标而不是所有的行。这让Android能高效地使用资源 - 游标并不是把大量数据直接堆到内存里,而是只获取、释放需要的数据,这对拥有大量记录的表尤其高效。
fetchNote()
和fetchAllNotes()
相似,不过只获取我们指定行号rowId 对应的笔记。它使用的SQLiteDatabase 的
query()
的版本稍稍有点不同。第一个参数(设置为true)表示我们只对唯一的结果感兴趣。selection 参数(第四个参数)被设置成只搜索行号等于我们传入的行号rowId 的行。因此我们获得了唯一行的游标。
最后,updateNote()
接受三个参数:rowId, title 和body,然后使用一个ContentValues的实例来更新给定行号rowId的笔记。
步骤3
布局和Activity
大部分Activity类都有一个与之关联的布局。布局就是Activity展示给用户看的”脸面”。这里我们的布局占据整个屏幕,展示一系列的笔记。
然而,全屏布局并不是一个Activity唯一的选择。你也可以使用悬浮布局floating layout(例如:一个对话框和提醒dialog or alert),或者你也可能根本不需要一个布局(如果你不为Activity指定某种布局,Acitivity对用户来说就是不可见的。)
打开看看res/layout
下的notepad_list.xml
文件。(你可能需要点击底部的xml标签来查看XML源码。)
这是个几乎为空的布局定义文件。下面是几个你应该知道的关于布局文件的东西:
- 所有的Android布局文件都必须以这个XML头作为开头:
<?xml version="1.0" encoding="utf-8"?>
. - 第一个定义一般(并不总是)是某种布局的定义,这里是
LinearLayout。
- 必须总是在顶层的部件或者布局里面定义Android的XML命名空间,因而在剩下的整个文件里都可以使用
android:
标签:xmlns:android=http://schemas.android.com/apk/res/android
步骤4
我们需要创建一个布局来展示我们的列表。在LinearLayout
元素里面添加代码,结果如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=http://schemas.android.com/apk/res/android android:layout_width="wrap_content" android:layout_height="wrap_content"> <ListView android:id="@android:id/list" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@android:id/empty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/no_notes"/> </LinearLayout>
-
ListView
和TextView
节点里面的id字符串有@符号,表示XML解析器需要解析和扩展id字符串,并使用ID资源。 -
ListView
和TextView
可以被看成是两个交替的View,每次只能显示其中一个。如果有笔记需要显示就使用ListView ,而如果没有任何笔记需要显示就使用TextView (它有一个默认的值"No Notes Yet!",作为一个字符串资源在res/values/strings.xml
里面被定义)。 -
list
和empty
ID是Android平台为我们提供的,因此,我们必须为id
加上前缀android:(例如:
@android:id/list
) - 当
ListAdapter没有数据供给,ListView会自动使用
来更改默认的空View。empty
ID,ListAdapter默认会去查找这个名字。或者,通过在ListView调用setEmptyView(View)
-
更明白地讲,类
android.R里是系统为你预先定义的资源的集合,而你工程里的类
R
是你的工程已定义资源的集合。只要加上android:
命名空间前缀,就可以在XML文件里使用在类android.R
里能找到的资源(正如我们在这里看到的)。
步骤5
资源和类R
Eclipse工程里,res/下的目录存放资源文件。在res/目录下的目录和文件有特定的结构(specific structure).
定义在这些目录和文件里的资源,在类R里面有对应的入口,允许你的应用很方便的访问和使用它们。类R是Eclipse插件根据 res/目录下的内容自动生成的(或者,由aapt生成,如果你使用命令行开发工具)。另外,它们也被打包并部署为应用的一部分。
为了在ListView显示笔记的列表,我们也需要为每一行定义一个View:
- 在
res/layout
下创建一个新文件notes_row.xml
. - 添加以下内容(注意,又一次使用了XML头,在第一个节点定义了Android XML命名空间)
<?xml version="1.0" encoding="utf-8"?> <TextView android:id="@+id/text1" xmlns:android=http://schemas.android.com/apk/res/android android:layout_width="wrap_content" android:layout_height="wrap_content"/>
这就是要用在每条记录标题行的View - 只有一个文本域在里面。
这次我们创建一个新的ID text1
。@符号后面的+符号(加号)表示,这个id应该被看成一个资源,如果不存在,应该自动创建它。
3. 保存文件。
打开看看R.java
文件,应该能看到新定义的notes_row
和text1
(我们新定义的),意味着我们现在就能够在代码里面访问它们了。
步骤6
下面,打开类Notepadv1
。下面的步骤里,我们将修改这个类,将它变成一个ListAdapter来显示我们的笔记,并允许我们添加新的笔记。
Notepadv1
将继承自Activity
的一个子类叫做ListActivity
, 它有额外的能力,像列表一样能容纳一些东西。例如:在屏幕上以行的方式显示任意数量的列表项,在列表项之间移来移去,并允许选中它们。
看一遍类Notepadv1
里面已有的代码。里面有一个现在还没用到的私有成员mNoteNumber
,我们将用它来创建带编号的笔记标题。
也定义了3个重写的方法:onCreate
, onCreateOptionsMenu
和onOptionsItemSelected
; 我们需要重写它们的代码:
- Activity启动的时候会调用
onCreate()
方法 - 它有点像Activity的”main”方法。当Activity运行的时候,我们用它来初始化资源和状态。 - 调用
onCreateOptionsMenu()
来为Activity创建菜单。当用户点击菜单(menu)按钮时会显示菜单,菜单里有一系列选项可供选择(例如”Create Note”)。 -
onOptionsItemSelected()
is the other half of the menu equation, it is used to handle events generated from the menu (e.g., when the user selects the "Create Note" item). -
onOptionsItemSelected()
是菜单等式的另一半,用来处理菜单引发的事件(例如,当用户选择了”Create Note”项)。
步骤7
将Notepadv1
的基类从Activity
改为 ListActivity
:
public class Notepadv1 extends ListActivity
注意:在你完成上面的修改以后,你必须在类Notepadv1里导入ListActivity
,在Windows或Linux用快捷键ctrl-shift-O ,或者在Mac上用cmd-shift-O (组织包的导入)来进行导入。
步骤8
为onCreate()
函数体编写代码。
这里我们将为Activity设置标题(显示在屏幕顶端),利用我们创建的XML布局notepad_list
,创建NotesDbAdapter
实例来访问笔记数据,以及用已有的笔记标题填充列表:
- 在
onCreate()
方法里,调用super.onCreate()
方法,参数是传入的savedInstanceState
。 - 调用
setContentView()
,参数是R.layout.notepad_list
. - 在类的顶部,创建一个类型为
NotesDbAdapter的
私有成员mDbHelper
。 - 回到
onCreate()
方法,构建一个NotesDbAdapter
的实例,并把它赋值给成员mDbHelper
(将this
传递给DBHelper
的构造函数) - 调用
mDbHelper
的open()
方法打开(或创建)数据库。 - 最后,调用一个新方法
fillData()
, 该方法获得数据,并通过mDbHelper
填充ListView - 我们还没定义这个方法呢。
onCreate()
内容如下:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.notepad_list); mDbHelper = new NotesDbAdapter(this); mDbHelper.open(); fillData(); }
同时确认你定义了成员mDbHelper
(就在mNoteNumber 定义的下面):
private NotesDbAdapter mDbHelper;
步骤9
为onCreateOptionsMenu()编写代码。
我们现在创建"Add Item" 按钮,通过点击手机上的menu按钮可以看到它。我们设定它位于菜单第一的位置。
- 在资源文件
strings.xml
(目录res/values
下面)里添加一个新的名为"menu_insert"的字符串,内容设置为Add Item
:<string name="menu_insert">Add Item</string>
保存文件,回到Notepadv1。
- 在类顶部创建一个菜单选项常量:
public static final int INSERT_ID = Menu.FIRST;
- 在方法
onCreateOptionsMenu()
里,修改super
调用返回布尔值result
. 我们在最后返回这个值。 - 然后调用
menu.add()
添加菜单项。
整个函数内容如下:
@Override public boolean onCreateOptionsMenu(Menu menu) { boolean result = super.onCreateOptionsMenu(menu); menu.add(0, INSERT_ID, 0, R.string.menu_insert); return result; }
add()
的参数说明:菜单的分组ID (这里是空),唯一的ID(上面定义的),菜单项的顺序(0表示没有特例),以及菜单项用到的字符串资源。
步骤10
为onOptionsItemSelected()
方法编写代码。
这个函数将处理我们新增加的"Add Note"(译者注:跟前面一致应该是"Add Item")菜单项。当它被选中时,onOptionsItemSelected()
方法将被调用,item.getItemId()
被设置成INSERT_ID
(我们用来定位菜单项的常量)。我们可以检测它的值,并采取适当的行动。
- 将
super.onOptionsItemSelected(item)
方法的调用放在最后面 - 我们需要先捕获我们的事件。 - 对
item.getItemId()
使用switch语句。如果结果是INSERT_ID,就调用一个新方法
createNote()
, 然后返回true,因为我们已经处理了这个事件,并且不想让它传播给整个系统。 - 最后返回基类调用
onOptionsItemSelected()
方法的结果。
整个onOptionsItemSelect()
方法的内容如下:
@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case INSERT_ID: createNote(); return true; } return super.onOptionsItemSelected(item); }
步骤11
添加新方法createNote():
在我们的应用第一版本里,createNote()
方法将不会有什么实际功用。我们只是简单的创建一个笔记,它的标题基于一个计数器生成("Note 1", "Note 2"...) ,内容为空。目前我们还无法编辑笔记的内容,所以我们暂时把笔记的内容都设置成某个默认值。
- 使用"Note"和我们定义的计数器来构建标题:
String noteName = "Note " + mNoteNumber++
- 调用
mDbHelper.createNote()
,将noteName
作为标题,""
作为内容。 - 调用
fillData()
填充笔记列表(低效但是简单) - 我们将在下一步定义这个方法。
整个createNote()
方法内容如下:
private void createNote() { String noteName = "Note " + mNoteNumber++; mDbHelper.createNote(noteName, ""); fillData(); }
步骤12
List适配器
我们的例子使用SimpleCursorAdapter
将数据库游标Cursor
绑定到一个ListView,这是使用ListAdapter
的通常做法。其他的选择如ArrayAdapter
,同样可以将内存数据的List或者Array绑定到一个ListView (译者注:原文是List)。
定义fillData()
方法:
这个方法使用 SimpleCursorAdapter,它持有一个数据库游标
Cursor
,并把它绑定到布局里提供的显示字段。这些字段定义了列表的行元素(这里我们使用布局文件notes_row.xml
里的text1
字段),因此,我们可以很方便的使用数据库记录来填充列表。
为了实现这个目标,我们必须提供从返回的游标里面的title
字段,到我们的text1
TextView的一个映射。通过两个Array来实现此功能:第一个,一个几个列的字符串数组作为映射源(这里只有”title”,来自常量NotesDbAdapter.KEY_TITLE
);第二个,一个保存了View的引用的整型数组作为数据绑定目标(R.id.text1
TextView)。
这是一大段代码,我们实现阅读一下:
private void fillData() { // Get all of the notes from the database and create the item list Cursor c = mDbHelper.fetchAllNotes(); startManagingCursor(c); String[] from = new String[] { NotesDbAdapter.KEY_TITLE }; int[] to = new int[] { R.id.text1 }; // Now create an array adapter and set it to display using our row SimpleCursorAdapter notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to); setListAdapter(notes); }
我们已完成的工作如下:
- 从
mDbHelper.fetchAllNotes()
获得游标后,调用了Activity的方法startManagingCursor()
,让Android系统处理游标的生命周期,而不用我们自己去关心它。(我们将在练习3涉及到生命周期的内幕,但是,暂时只要知道这样做是让Android为我们做一些资源管理的工作。) - 然后我们创建了一个字符串数组,在里面我们我们声明了要映射的列(这里只有标题),和一个整型数组,在里面定义了我们需要绑定列的View(顺序必须和字符串数组保持一致,但这里我们两个数组都只有一个元素)。
- 下面创建SimpleCursorAdapter的一个实例。跟很多Android的类一样,SimpleCursorAdapter 需要一个Context来完成它的工作,因此我们传递
this
作为Context(因为基类Activity 是Context的一个实现)。传递我们创建的notes_row
View作为数据的容器,下一个参数是我们刚刚创建的游标Cursor,最后两个参数是我们的两个数组。
将来,要记住from列和to资源的映射需要保持两个数组内容的顺序一致。如果我们有更多列需要绑定到更多的View上,我们需要按顺序设置它们,例如,我们可能使用{ NotesDbAdapter.KEY_TITLE, NotesDbAdapter.KEY_BODY }
和{ R.id.text1, R.id.text2 }
来绑定两个字段到列表的行(我们也需要在notes_row.xml里定义text2来显示笔记内容)。这就是如何将多个字段绑定到一个列表行的方法(同时也需要定制行的布局)。
如果遇到类未找到的编译错误,按快捷键ctrl-shift-O 或者(在MAC上cmd-shift-O) 来组织导入包。
步骤13
运行应用!
- 右键点击
Notepadv1
工程。 - 在弹出菜单里,选择Run As > Android Application.
- 如果看到有对话框弹出,选择Android Application作为应用运行的方式。
- 点击菜单menu按钮(译者注:模拟器menu按钮或者手机实体menu按钮),选择Add Item 菜单项来添加新笔记。
解决方案和后面的步骤
你可以在zip文件里面的Notepadv1Solution
里面找到这个类的解决方案,把它跟你自己写的比较一下。
一旦你完成上面的练习,请移步到Tutorial Exercise 2 ,增加创建、编辑和删除笔记的功能。