记事本练习 1
在这个练习中,你将要创建一个简单的记事本列表,用户可以添加新的记事但是不能编辑他们。这个练习示范了如下内容:
列表活动创建并操作菜单项的基本知识。怎样在SQLite数据库中存取记事。怎样将数据绑定到一个使用了数组适配器的列表视图(最简单的一种绑定到列表视图的方式)。关于屏幕布局,包括怎样布局一个列表视图,怎样将一个项目加入活动的菜单,活动是怎样操作那些菜单选择,这些内容的基础知识。 [Exercise 1] [Exercise 2] [Exercise 3] [Extra Credit]
Step 1
第一步 在Eclipse中打开Notepadv1项目。
Notepadv1项目是一个起点,它关注一些样板性的工作,如果你完成了Hello Android教程,那么你就曾经见过它们。
右击包资源管理器,选择把.../General/Existing Projects导入到工作区。按浏览键,转到你拷贝的那三个练习所在的文件夹,选择Notepadv1然后点OK。你应该看到Notepadv1列在项目表中,并且有个检查标记在它旁边,点完成。练习项目应该已经打开。如果AndroidManisfest.xml 或者Android zip 文件有什么错误,右击项目然后选择Android Tools->Fix Project Properties(项目在错误的路径里查找库文件,上面的操作会替你修复它)。
Step 2
在这个练习中,我们仅打算直接用SQLite 数据库来存储我们的数据,但是在一个实际的应用程序中,也许写一个合适的内容提供器(Content Provider)来封装这个行为会更好。
如果你感兴趣,你可以找到更多关于内容提供器或者介绍存储,查找,显示数据的整个主题。
看一下DBHelper类-它用来封装对SQLite数据库的数据访问,这个数据库将保存我们的记事本数据并且允许我们更新它。
通常你将利用一个内容提供器来实现,事实上在SDK中包含的那个完整的记事本程序确实实现了这样一个 ContentProvider。不管怎样,没理由不让你直接用自己的SQLite数据,像我们在这里做的一样。最主要的事是注意这个类是怎样为我们存储,查找,更新SQLite数据库中的数据。对于查找行,基于行id查找,创建新行,删除已存在的行和更新一行,这里有一些方法。如果你想迅速了解如何在你的程序中使用SQLite数据库的基础知识,或者看一下这个例子中的类,或者更好的,看一下完整的记事本应用程序。它在SDK的samples/目录中,作为一个使用ContentProvider的例子。
Step 3
布局和活动
大多数活动会有一个相联系的布局。布局是活动对用户的直观表现。在这个例子中,我们的布局将占据整个屏幕并提供一个记事列表。
全屏的布局并不是活动的唯一选择。你也许想知道如何用浮动布局(例如,对话框或者警告),或者也许你完全不需要一个布局(这类活动对用户将是不可见的,除非你为它指定了某种布局)
打开res/layout中的notepad_list.xml文件,看一下:
这是一个布局定义文件,其中包含了默认的起始项。我们提供这些,是为了便于你迅速开始
- a.所有的Android布局文件必须以XML头为开始:<?xml version="1.0" encoding="utf-8"?>
- b.同样,下一条定义通常(但不完全)是一个某种类的布局定义,在这个例子中是LinearLayout.
- c.Android的XML名字空间应该总是定义在xml文件中组件或布局的最上级,这样标签"android:"才能用在文件的其余部分:
xmlns:android="http://schemas.android.com/apk/res/android"
Step 4
我们需要创建一个布局来容纳我们的列表。在LinearLayout标签中添加代码,使得这个文件看起来像下面的样子:(你得敲Source tab按钮来编辑XML文件)
<?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 id="@id/android:list" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView id="@id/android:empty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/no_notes"/> </LinearLayout>
- ListView 和TextView可以看做是两个替代的视图,同一时间只有一个可以被显示。ListView可以用在有记事要显示的时候,而TextView(有一个“No Notes Yet!”的字符串资源作为默认值会在没有任何记事的时候显示)。
- ListView和TextView的id字串中有一个“@”符号,它的意思是XML解释器应当解析并且展开id字串的剩余部分并且利用ID资源。
- android:list和android:empty是Android平台已经提供给我们的ID,android:empty在list adapter中没有数据时自动提供。List Adapter默认的搜索这些名字。作为替代,你也可以用setEmptyView();更广泛的,Android.R类是平台提供的预定义资源的集合,你项目中的R类是在你项目中预定义的资源集合。在资源类android.R中出现的资源可以通过android:名字空间前缀在XML文件中使用。
Step 5
在Eclipse项目res/下的文件夹很特殊。在这个文件夹下的子文件夹和文件有特定的结构。
尤其,定义在这些文件夹和文件里的资源会在R类中有相应的入口。这样你的程序就可以轻松的访问到这些资源。更进一步的,它们会被绑定,作为程序的一部分。
要产生一个列表视图,我们需要定义表中各行的显示:
- 在res/layout下创建一个名为notes_row.xml的文件
- 添加如下的内容(注:xml头又一次被用到了,并且第一个节点定义了Android xml名字空间)
<?xml version="1.0" encoding="utf-8"?> <TextView id="@+id/text1" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
- 每一行的标题将用到这个视图-它仅有一个文字区域。
- 在这个例子中,我们创建了一个叫text1的新id。在id字串中+号跟在@号之后,显示了这个id如果不存在,则应当自动创建。因此我们定义了一个悬空的text1,然后使用它。
- 保存文件后,打开项目中的R.java类,你应该看到notes_row和text1的新定义(我们的新定义),这意味着,我们可以从我们的代码中获得访问。
Step 6
然后,打开源码中的Notepadv1类。我们将要替换这个类,成为列表适配器来显示我们的记事,同时允许我们添加新的记事。
Notepadv1将成为Activity的一个子类,称为ListActivity,它拥有一些适合做与列表相关的额外功能,例如:在屏幕上按行显示列表项的数字,在列表项中移动,允许他们被选择。
看一下Notepadv1类中的现有代码。在最上面有一些常量定义,接着的私有区域里我们将创建数字形式的记事标题,并且还有一些从超类重载的方法。
Step 7
将Notepadv1的父类从Activity改为ListActivity:
public class Notepadv1 extends ListActivity
注:你必须用Eclipse把ListActivity导入到Notepadv1类中,在Windows或Linux按crtl-shift-O,或者在Mac下按cmd-shift-O。
Step 8
这里定义了三个重载方法:onCreate,onCreateOptionsMenu和onOptionsItemSelected,我们把需要的内容详细说明一下:
- onCreate()在活动开始的时候被调用-它有一点像activity的"main"方法。我们用这个函数在活动运行的时候为它设置资源和状态。
- onCreateOptionsMenu()用来为活动产生一个菜单。当使用者点击menu按钮的时候,它会显示,并且有一列可以选择的选项(像“Create Note”)
- onOptionsItemSelected()是菜单等式中(菜单处理过程)的另一半,它用来处理由菜单产生的事件(例如.当用户选择了“Create Note”选项)
Step 9
详细说明onCreate()方法的主体。
在这儿,我们将要为活动设定一个名称(显示在屏幕的最上面),利用我们先前为活动创建的notepad_list布局来显示内容,建立DBHelper实例来访问记事数据,然后产生带有有效记事标题的列表:
- 在我们的方法中,调用带有icicle参数的super()函数
- setContentView到R.layout.notepad_list
- 在类中新建一个叫dbHelper的私有域,它是DBHelper的实例(在OnCreate 方法之前)
- 回到onCreate方法,新建一个DBHelper实例-assign到dbHelper区域(注,你必须为DBHelper的构造函数传递一个this参数)
- 最后,调用一个新方法-fillData()-利用helper获取并产生数据,我们现在还没有定义这个函数。
- onCreate()函数应该像这样:
@Override
public void onCreate(Bundle icicle)
{
super.onCreate(icicle);
setContentView(R.layout.notepad_list);
dbHelper = new DBHelper(this);
fillData();
}
记住添加DBHelper的定义(在noteNumber定义的下面):
private DBHelper dbHelper;
Step 10
我们构建的记事本程序仅仅涉及了菜单的表面。
你可以为菜单项添加快捷键,创造子菜单甚至将菜单添加到别的应用程序中onCreateOptionMenu()方法的主体
我们现在要添加一个菜单项,“Add Item”,它会用到我们在strings.xml中创建的字符串,并且用到一个常量定义,这个常量我们在类的顶部创建来标志Add项的操作。
- 在strings.xml资源中(在res/values下),为menu_insert添加一个新的字符创“Add Item”<string name="menu_insert">Add Item</string>,然后保存文件
- 同时,你需要在Notepadv1类的顶部定义一个菜单位置常量(在KEY_BODY的定义下面)public static final int INSERT_ID=Menu.FIRST;
- 在onCreateOptionsMenu()方法中,添加一个菜单项。同时注意super调用的返回值结果。整个方法应该看起来像下面的样子:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
boolean result = super.onCreateOptionsMenu(menu);
menu.add(0, INSERT_ID, R.string.menu_insert);
return result;
}
Step 11
onOptionsItemSelected()方法:
这个方法将要处理我们新的“添加记事”菜单项。当选中这个菜单项时,onOptionsItemSelected()方
法就将被调用,同时item.getId()的值会被设为INSERT_ID(我们用这个常量来标志菜单项)。我们能
检查这个,然后选择适当的动作:
- super.onOptionsItemSelected(item)方法出现在这个方法的最后-我们想要先抓取我们的事件!
- 在item.getId()上的选择语句。
- INSERT_ID项。
- 调用新方法createNote()
- 在case的结尾用break
- 在结束的时候返回超类onOptionsItemSelected()方法的返回值
- 整个onOptionsItemSelect()方法看起来应该如下:
@Override
public boolean onOptionsItemSelected(Item item) {
switch (item.getId()) {
case INSERT_ID:
createNote();
break;
}
return super.onOptionsItemSelected(item);
}
Step 12
添加一个createNote()方法:在我们应用程序的这个第一版里,createNote()不会很有用。我们只是简单的创建一个带有标题的记事,
这个标题被赋值为(“Note1”,“Note2”。。。)这类基于计数的东东,但是记事的内容为空。现在我
们还不能修改记事的内容,所以我们现在只能满足于一些默认值:
- String noteName="Note"+noteNumber++;(用“Note”来构造名字并且用到我们已经在类中定义过的计数器)
- 调用dbHelper.createRow(),使用noteName作为标题,“”为内容
- 在添加之后再调用一次fillData()方法(一个低效但是简单的方法)
- 整个createNote()方法看起来应该像这样:
private void createNote() {
String noteName = "Note " + noteNumber++;
dbHelper.createRow(noteName, "");
fillData();
}
Step 13
列表适配器
我们的例子使用了一个非常简单的数组适配器,它将一个项目的数组或列表添加至列表视图。在Android中更常见的是,列表适配器和ContenProviders一起运行,并且这也是列表的一种非常简单的用法。
将ContentProvider捆绑至ListView,你可以用android.widget.SimpleCursorAdapter。
定义fillData()方法。这个方法很长:
这个方法使用了ArrayAdapter,它是将数据放入ListView的最简单方法。ArrayAdapter读取一个列表或者字串数组,并且将他们绑定到一个文本视图,在布局中提供了这个为列表行定义的文本视图(在我们的notes_row.xml布局文件中的text1域)。这个方法简单的从database helper中获取一列记事,利用从每行得到的标题字串构造了一个字符串列表,然后通过这些items创建一个ArrayAdapter,并使用我们定义过的notes_row
- 数组适配器需要一个字符串列表(List<String>)来包含要显示的。
- 数据从数据库中按行读出,每行的标题域用来产生字符串列表。
- 我们指定notes_row视图,作为数据容器。
- 如果有关于未找到类的错误,ctrl-shift-O(在mac上用cmd-shift-O)来组织导入。
注:在这个练习中我们使用了ArrayAdapter,这并不是好的解决方案,更典型的,一个SimpleCursorAdapter应该和ContenProvider一起用或者至少是一个从查询返回的Cursor。看侧边的列表适配器获得更多信息。
Step 14
运行!
- 右击Notepadv1项目
- 在弹出菜单中,选择Run As->Android Application
- 如果你看见出现一个对话框,选择Android Launcher的方式去运行程序(你同样可以用对话框顶部的链接,把这个设置为你工作空间的默认属性,推荐这样做,它可以避免插件每次都询问你同样的问题)
- 单击菜单按钮,选择Add Item 来添加记事
Solution and Next Steps
方案和下一步
你可以在zip文件Notepadv1Solution中找到这个类的解决方案,对照一下你自己的。一旦你准备好,继续入门指引练习2为程序添加新建,编辑删除记事的能力。返回入门指引首页。