[译文]Notepad Exercise 1 – Android SDK Tutorials系列

时间:2022-08-31 19:56:57

Notepad 练习1

这个练习里,你将创建一个简单的笔记列表,用户只能添加而不能编辑笔记。练习展示了:

  •  ListActivity的基础,创建和处理菜单选项。
  • 如何使用SQLite数据库存储笔记。
  • 如何使用SimpleCursorAdapter将数据从数据库游标绑定到ListView 。
  • 屏幕布局基础:如何排版一个list view,如何向activity的菜单里添加项目,以及activity如何处理这些菜单被选中的动作。

步骤1

在Eclipse里面打开工程Notepadv1

Notepadv1 是一个工程,作为本次练习的起点。它完成了一些初始的基础工作,如果你已经阅读了Hello, World 教程,应该已经熟悉这些基础工作了。

  1. 创建一个新工程File > New > Android Project.
  2. 在创建Android新工程对话框里,选择从已有源码创建工程(Create project from existing source)
  3. 点击浏览(Browse),然后找到你拷贝的NotepadCodeLab 目录(从Notepad介绍下载的),然后选择Notepadv1。
  4. 工程名和其他属性应该都已经给你填好了。你必须选择编译目标 - 我们建议选择已有最低版本的平台作为编译目标。同时在最低SDK版本一栏填写与选择的目标平台相匹配的API等级数。
  5. 点击结束。应该能在Eclipse的package explorer看到打开的Notepadv1 工程了。
如果发现 AndroidManifest.xml有错误,或者一些跟Android zip文件有关的问题,右键点击工程,选择 Android Tools > Fix Project Properties. (工程在错误的位置查找库文件,这个操作将帮你修复这个问题。)

步骤2

查看一下类NotesDbAdapter - 这个类用来封装SQLite数据库的数据访问,这个数据库将保存我们的笔记数据并允许我们更新它。

在这个类的最上面,定义了一些常量,用来保存数据库里对应的字段名,应用可以通过这些字段名来查找数据。那儿也同时定义了一条数据库创建语句,在数据库不存在的时候创建一个新的数据库。

我们的数据库名称是data, 里面只有一个表名字是notes, 表里面有3个字段:_id,titlebody。_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, titlebody ,将它们放到字符串数组里面。剩下的参数依次是:selection,selectionArgs, groupBy, havingorderBy.把它们全部设置为null 意味着我们需要返回全部数据,不要分组,以及使用默认排列顺序。需要更详细的内容请参考SQLiteDatabase

注意: 返回游标而不是所有的行。这让Android能高效地使用资源 - 游标并不是把大量数据直接堆到内存里,而是只获取、释放需要的数据,这对拥有大量记录的表尤其高效。

fetchNote()fetchAllNotes() 相似,不过只获取我们指定行号rowId 对应的笔记。它使用的SQLiteDatabase query() 的版本稍稍有点不同。第一个参数(设置为true)表示我们只对唯一的结果感兴趣。selection 参数(第四个参数)被设置成只搜索行号等于我们传入的行号rowId 的行。因此我们获得了唯一行的游标。

最后,updateNote() 接受三个参数:rowId, titlebody,然后使用一个ContentValues的实例来更新给定行号rowId的笔记。

步骤3

打开看看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>
  • ListViewTextView 节点里面的id字符串有@符号,表示XML解析器需要解析和扩展id字符串,并使用ID资源。
  • ListViewTextView 可以被看成是两个交替的View,每次只能显示其中一个。如果有笔记需要显示就使用ListView ,而如果没有任何笔记需要显示就使用TextView (它有一个默认的值"No Notes Yet!",作为一个字符串资源在res/values/strings.xml里面被定义)。
  • listempty ID是Android平台为我们提供的,因此,我们必须为id 加上前缀android:(例如:@android:id/list)
  • ListAdapter没有数据供给,ListView会自动使用empty ID,ListAdapter默认会去查找这个名字。或者,通过在ListView调用setEmptyView(View) 来更改默认的空View。
  • 更明白地讲,类android.R里是系统为你预先定义的资源的集合,而你工程里的类R 是你的工程已定义资源的集合。只要加上android: 命名空间前缀,就可以在XML文件里使用在类android.R 里能找到的资源(正如我们在这里看到的)。

步骤5

为了在ListView显示笔记的列表,我们也需要为每一行定义一个View:

  1. res/layout 下创建一个新文件notes_row.xml.
  2. 添加以下内容(注意,又一次使用了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_rowtext1 (我们新定义的),意味着我们现在就能够在代码里面访问它们了。

步骤6

下面,打开类Notepadv1 。下面的步骤里,我们将修改这个类,将它变成一个ListAdapter来显示我们的笔记,并允许我们添加新的笔记。

Notepadv1 将继承自Activity 的一个子类叫做ListActivity, 它有额外的能力,像列表一样能容纳一些东西。例如:在屏幕上以行的方式显示任意数量的列表项,在列表项之间移来移去,并允许选中它们。

看一遍类Notepadv1 里面已有的代码。里面有一个现在还没用到的私有成员mNoteNumber ,我们将用它来创建带编号的笔记标题。

也定义了3个重写的方法:onCreate, onCreateOptionsMenuonOptionsItemSelected; 我们需要重写它们的代码:

  • 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 实例来访问笔记数据,以及用已有的笔记标题填充列表:

  1. onCreate() 方法里,调用super.onCreate() 方法,参数是传入的savedInstanceState
  2. 调用setContentView() ,参数是R.layout.notepad_list.
  3. 在类的顶部,创建一个类型为NotesDbAdapter私有成员mDbHelper
  4. 回到onCreate() 方法,构建一个NotesDbAdapter 的实例,并把它赋值给成员mDbHelper (将this 传递给DBHelper的构造函数)
  5. 调用mDbHelperopen() 方法打开(或创建)数据库。
  6. 最后,调用一个新方法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按钮可以看到它。我们设定它位于菜单第一的位置。

  1. 在资源文件strings.xml (目录res/values下面)里添加一个新的名为"menu_insert"的字符串,内容设置为Add Item:
    <string name="menu_insert">Add Item</string>
    保存文件,回到Notepadv1。
  2. 在类顶部创建一个菜单选项常量:
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 (我们用来定位菜单项的常量)。我们可以检测它的值,并采取适当的行动。

  1. super.onOptionsItemSelected(item) 方法的调用放在最后面 - 我们需要先捕获我们的事件。
  2. item.getItemId()使用switch语句。

    如果结果是INSERT_ID,就调用一个新方法createNote(), 然后返回true,因为我们已经处理了这个事件,并且不想让它传播给整个系统。

  3. 最后返回基类调用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"...) ,内容为空。目前我们还无法编辑笔记的内容,所以我们暂时把笔记的内容都设置成某个默认值。

  1. 使用"Note"和我们定义的计数器来构建标题:String noteName = "Note " + mNoteNumber++
  2. 调用mDbHelper.createNote() ,将noteName 作为标题,"" 作为内容。
  3. 调用fillData() 填充笔记列表(低效但是简单) - 我们将在下一步定义这个方法。

整个createNote() 方法内容如下:

    private void createNote() {

         String noteName = "Note " + mNoteNumber++;

         mDbHelper.createNote(noteName, "");

         fillData();

     }

步骤12

定义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);     }

我们已完成的工作如下:

  1. mDbHelper.fetchAllNotes()获得游标后,调用了Activity的方法startManagingCursor() ,让Android系统处理游标的生命周期,而不用我们自己去关心它。(我们将在练习3涉及到生命周期的内幕,但是,暂时只要知道这样做是让Android为我们做一些资源管理的工作。)
  2. 然后我们创建了一个字符串数组,在里面我们我们声明了要映射的列(这里只有标题),和一个整型数组,在里面定义了我们需要绑定列的View(顺序必须和字符串数组保持一致,但这里我们两个数组都只有一个元素)。
  3. 下面创建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

运行应用!

  1. 右键点击Notepadv1 工程。
  2. 在弹出菜单里,选择Run As > Android Application.
  3. 如果看到有对话框弹出,选择Android Application作为应用运行的方式。
  4. 点击菜单menu按钮(译者注:模拟器menu按钮或者手机实体menu按钮),选择Add Item 菜单项来添加新笔记。

解决方案和后面的步骤

你可以在zip文件里面的Notepadv1Solution 里面找到这个类的解决方案,把它跟你自己写的比较一下。

一旦你完成上面的练习,请移步到Tutorial Exercise 2 ,增加创建、编辑和删除笔记的功能。