预备知识
搭建开发环境,尝试编写”HelloWorld”,了解Android的基本概念,熟悉Android的API(官方文档中都有,不赘述)。
程序截图
先来简单了解下程序运行的效果




程序入口点
类似于win32程序里的WinMain函数,Android自然也有它的程序入口点。它通过在AndroidManifest.xml文件中配置来指明,可以看到名为NotesList的activity节点下有这样一个intent-filter,其action为android.intent.action.MAIN,
Category指定为android.intent.category.LAUNCHER,这就指明了这个activity是作为入口activity,系统查找到它后,就会创建这个activity实例来运行,若未发现就不启动(你可以把MAIN改名字试试)。
/>
/>
NotesList详解
就从入口点所在的activity(见图1)开始,可以看到这个activity最重要的功能就是显示日志列表。这个程序的日志都存放在Sqlite数据库中,因此需要读取出所有的日志记录并显示。
先来看两个重要的私有数据,第一个PROJECTION字段指明了“日志列表“所关注的数据库中的字段(即只需要ID和Title就可以了)。
private
static
final String[] PROJECTION =
new// 0
第二个字段COLUMN_INDEX_TITLE指明title字段在数据表中的索引。
private
static
final
int COLUMN_INDEX_TITLE =
1;
然后就进入第一个调用的函数onCreate。
null)
SimpleCursorAdapter adapter =
new SimpleCursorAdapter(this, R.layout.noteslist_item,cursor,
int[] { android.R.id.text1 });
/>
既然有了“日志列表”,就自然要考虑如何处理某一条日志的单击事件,这通过重载onListItemClick方法来完成,
protected
void onListItemClick(ListView l, View v, int position, long=ContentUris.withAppendedId(getIntent().getData(), id);
那么,上面这句startActivity(new
那么,Android又是如何找到NoteEditor这个对应的activity的呢?这就是intent发挥作用的时刻了。
new Intent(Intent.ACTION_EDIT, uri)这里的Intent.ACTION_EDIT=”android.intent.action.EDIT”,另外通过设置断点,我们看下这里的uri值:
/>
>
/>
/>
/>
/>
/>
/>
/>
/>
Intent.ACTION_EDIT=”android.intent.action.EDIT”,想必大家已经明白是怎么回事了吧。
下面就进入activity选择机制了:系统从intent中获取道uri,得到了content://com.google.provider.NotePad/notes/1,去掉开始的content:标识,得到com.google.provider.NotePad/notes/1,然后获取前面的com.google.provider.NotePad,然后就到Androidmanfest.xml中找到authorities为com.google.provider.NotePad的provider,这个就是后面要讲的contentprovider,然后就加载这个contentprovider。
/>
在这里是NotePadProvider,然后调用NotePadProvider的gettype函数,并把上述URI传给这个函数,函数返回URI所对应的类型(这里返回Notes.CONTENT_ITEM_TYPE,代表一条日志记录,而CONTENT_ITEM_TYPE= " vnd.android.cursor.item/vnd.google.note ")。
publicswitchcasereturnca
new IllegalArgumentException
+
new UriMatcher(UriMatcher.NO_MATCH);
决定的。
然后系统使用获得的" vnd.android.cursor.item/vnd.google.note"和”android.intent.action.EDIT”到androidmanfest.xml中去找匹配的activity.
/>
/>
/>
/>
/>
正好NoteEditor这个activity的intent-filter满足上述条件,这样就找到了NoteEditor。于是系统加载这个类并实例化,运行,然后就到了NoteEditor的OnCreate函数中(见后续文章)。
小技巧
1,在命令行中使用”adb shell”命令进入系统中,然后”cd app”进入应用程序所在目录,”rmXXX”就可以删除你指定的apk,从而去掉其在系统顶层界面占据的图标,若两次”cddata”则可以进入应用程序使用的数据目录,你的数据可以保存在这里,例如Notepad就是把其数据库放在它的databases目录下,名为note_pad.db.
2,第一次启动模拟器会比较慢,但以后就别关闭模拟器了,修改代码,调试都不需要再次启动的,直接修改后run或debug就是。
简介
android提供了三种菜单类型,分别为options menu,context menu,sub menu。
options menu就是通过按home键来显示,contextmenu需要在view上按上2s后显示。这两种menu都有可以加入子菜单,子菜单不能种不能嵌套子菜单。optionsmenu最多只能在屏幕最下面显示6个菜单选项,称为icon menu,iconmenu不能有checkable选项。多于6的菜单项会以more icon menu来调出,称为expandedmenu。optionsmenu通过activity的onCreateOptionsMenu来生成,这个函数只会在menu第一次生成时调用。任何想改变optionsmenu的想法只能在onPrepareOptionsMenu来实现,这个函数会在menu显示前调用。onOptionsItemSelected用来处理选中的菜单项。
contextmenu是跟某个具体的view绑定在一起,在activity种用registerForContextMenu来为某个view注册contextmenu。contextmenu在显示前都会调用onCreateContextMenu来生成menu。onContextItemSelected用来处理选中的菜单项。
android还提供了对菜单项进行分组的功能,可以把相似功能的菜单项分成同一个组,这样就可以通过调用setGroupCheckable,setGroupEnabled,setGroupVisible来设置菜单属性,而无须单独设置。
Options Menu
Notepad中使用了options menu和context menu两种菜单。首先来看生成optionsmenu的onCreateOptionsMenu函数。
这是一个标准的插入一个菜单项的方法,菜单项的id为MENU_ITEM_INSERT。
有意思的是下面这几句代码:
Intent intent =
new Intent(null, getIntent().getData());
这到底有何用处呢?其实这是一种动态菜单技术(也有点像插件机制),若某一个activity,其类型是”android.intent.category.ALTERNATIVE”,数据是”vnd.android.cursor.dir/vnd.google.note”的话,系统就会为这个activity增加一个菜单项。在androidmanfest.xml中查看后发现,没有一个activity符合条件,所以这段代码并没有动态添加出任何一个菜单项。
为了验证上述分析,我们可以来做一个实验,在androidmanfest.xml中进行修改,看是否会动态生成出菜单项。
实验一
首先我们来创建一个新的activity作为目标activity,名为HelloAndroid,没有什么功能,就是显示一个界面。
public
class HelloAndroid extendsprotected
void onCreate(Bundle savedInstanceState) {
>
然后修改androidmanfest.xml,加入下面这段配置,让HelloAndroid满足上述两个条件:
/>
/>
好了,运行下试试,哎,还是没有动态菜单项加入呀!
怎么回事呢?查看代码后发现,原来是onPrepareOptionsMenu搞的鬼!这个函数在onCreateOptionsMenu之后运行,下面这段代码中,由于Menu.CATEGORY_ALTERNATIVE是指向同一个组,所以把onCreateOptionsMenu中设置的菜单项给覆盖掉了,而由于onPrepareOptionsMenu没有给Menu.CATEGORY_ALTERNATIVE附新值,故Menu.CATEGORY_ALTERNATIVE还是为空。
new Intent(null, uri);
好的,那我们暂时把上面这几句给注释掉,当然,也可以不注释这几句,在onCreateOptionsMenu中改groupid号,即将Menu.CATEGORY_ALTERNATIVE改为Menu.first,其他的也行,但注意不要改为menu.none,这样会覆盖掉
menu.add(0, MENU_ITEM_INSERT, 0, R.string.menu_insert)
运行后就可以看到动态菜单出来了!

上面这个optionsmenu是在NotesList界面上没有日志列表选中的情况下生成的,若先选中一个日志,然后再点”menu”,则生成的optionsmenu是下面这样的:
、
哎,又动态增加了两个菜单项”Edit note”和”Edittitle”,这又是如何动态加入的呢?这就是onPrepareOptionsMenu的功劳了。
new Intent[10] =
new Intent(Intent.ACTION_EDIT, uri);
new MenuItem[1];
然后为选中的日志创建一个intent,操作类型为Intent.ACTION_EDIT,数据为选中日志的URI.于是会为选中的日志创建一个”Editnote”菜单项。
Intent intent =
new Intent(null, uri);
这几句和上面onCreateOptionsMenu函数中类似,用于动态增加菜单项,若某一个activity,其类型是”android.intent.category.ALTERNATIVE”,数据是”vnd.android.cursor.item/vnd.google.note”的话,系统就会为这个activity增加一个菜单项。在androidmanfest.xml中查看后发现,TitleEditor这个activity符合条件,于是系统就为TitleEditor这个activity动态添加一个菜单项”Edittitle”。
else {
若日志列表为空,则从菜单中删除组号为Menu.CATEGORY_ALTERNATIVE的菜单项,只剩下”Addnote”菜单项。
菜单项选中事件的处理非常简单,通过onOptionsItemSelected来完成,这里只是简单地调用startActivity(newIntent(Intent.ACTION_INSERT,getIntent().getData()));这个intent的操作类型为Intent.ACTION_INSERT,数据为日志列表的URI,即”content://com.google.provider.NotePad/notes”
public
boolean onOptionsItemSelected(MenuItem item) {
truereturn
super
Context Menu
下面介绍另一种菜单---上下文菜单,这通过重载onCreateContextMenu函数实现。
首先确认已经选中了日志列表中的一个日志,若没选择,则直接返回。Cursor指向选中的日志项。
null// For some reason the requested item isn't available, donothing
return
然后,设置上下文菜单的标题为日志标题
// Setup the menu header
最后为上下文菜单增加一个菜单项
// Add a menu item to delete the note
对于上下文菜单项选中的事件处理,是通过重载onContextItemSelected实现的。
switchcase// Delete the note that the context menu is for
truereturn
false
对于日志的删除,首先调用ContentUris.withAppendedId(getIntent().getData(),info.id);来拼接出待删除日志的URI.然后getContentResolver().delete(noteUri, null,null);调用下层的Content Provider去删除此日志。
实验二
来做个简单实验,在上述代码基础上增加一个上下文菜单项。首先在onCreateContextMenu函数中增加一个上下文菜单项:
menu.add(0,MENU_ITEM_INSERT,0,R.string.menu_insert);
然后为其在onContextItemSelected函数中增加一个处理过程:
case MENU_ITEM_INSERT:
void onClick(DialogInterface dialog, int// TODO Auto-generatedmethod stub
true
实验结果如下:
附记
感谢Evan JIANG对前一篇文章的错误之处进行指正,
“
只是指明会在Launcher中显示图标,同一个apk可以在桌面上加很多的图标,分别启动内部不同的多个界面。“,实验后发现确实如此,学习了。
Activity的生命周期
Activity类中有许多onXXX形式的函数可以重载,比如onCreate,onStart,onStop,onPause,那么它们的调用顺序到底是如何的呢?下面就通过一个实验来进行分析。在做这个实验之前,我们先得知道如何在Android中进行Log输出的。
我们要使用的是android.util.log类,这个类相当的简单易用,因为它提供的全是一些静态方法:
Log.v(String tag, String msg);
Log.d(String tag, String msg);
Log.i(String tag, String msg);
Log.w(String tag, String msg);
Log.e(String tag, String msg);
前面的tag是由我们定义的一个标识,一般可以用“类名_方法名“来定义。要在Eclipse中查看输出的log信息,需要打开Logcat(WindowàShowViewàotheràAndroidàLogCat即可打开)
实验一
我们要做的实验非常简单,就是有两个Activity(我这里分别叫做frmLogin和hello2),t它们各自有一个button,可以从第一个跳到第二个,也可以从第二个跳回到第一个。
配置文件AndroidManifest.xml非常简单,第二个activity并没有多余的信息需要指定。
/>
/>
第一个activity的代码如下:
public
class frmLogin extendsprivate
final
static String TAG =
"FrmLogin"public
void onCreate(Bundle savedInstanceState)
void setViewOneCommand()
void onClick(View v)
new Intent();
void onClick(View v)
voidsuper"onDestroy"protected
voidsuper"onPause"protected
voidsuper"onRestart"protected
voidsuper"onResume"protected
voidsuper"onStart"protected
voidsuper"onStop"
我在每个onXXX方法中都加入了log方法,值得注意的一点是按钮单击事件处理函数中,在最后我调用了finish();待会我会将此行注释掉进行对比实验。
第二个activitysetClass的两个参数反一下,这样就可以简单地实现在两个Activity界面中来回切换的功能了。
下面开始实验,第一个实验室从第一个activity跳到第二个activity(此时第一个关闭),然后从第二个跳回第一个(此时第二个关闭)
运行后观察LogCat,得到如下画面:

然后来进行第二个实验,对代码进行调整,我们把第一个activity中的finish()注释掉,从第一个activity跳到第二个(此时第一个没有关闭),然后第二个直接关闭(则第一个会重新来到前端),结果如图所示,可以看出调用了FrmLogin的onRestart而不是onStart,因为第一个activity只是stop,而并没有被destory掉。

前面两个实验都很好理解,可第三个实验就让我不明白了,过程如下:从第一个activity跳到第二个activity(此时第一个不关闭),然后第二个跳回第一个(此时第二个也不关闭),然后第一个再跳回第二个(此时第一个不关闭),照上面来推断,应该还是会调用onRestart才对,可实际上它调用的却是onStart,why???

1.Android用ActivityStack来管理多个Activity,所以呢,同一时刻只会有最顶上的那个Activity是处于active或者running状态。其它的Activity都被压在下面了。
2.如果非活动的Activity仍是可见的(即如果上面压着的是一个非全屏的Activity或透明的Activity),它是处于paused状态的。在系统内存不足的情况下,paused状态的Activity是有可被系统杀掉的。只是不明白,如果它被干掉了,界面上的显示又会变成什么模样?看来下回有必要研究一下这种情况了。
3.几个事件的配对可以比较清楚地理解它们的关系。Create与Destroy配成一对,叫entrielifetime,在创建时分配资源,则在销毁时释放资源;往上一点还有Start与Stop一对,叫visiblelifetime,表达的是可见与非可见这么一个过程;最顶上的就是Resume和Pause这一对了,叫foregroundlifetime,表达的了是否处于激活状态的过程。
4.因此,我们实现的Activity派生类,要重载两个重要的方法:onCreate()进行初始化操作,onPause()保存当前操作的结果。
除了Activity Lifecycle以外,Android还有一个Process Lifecycle的说明:
在内存不足的时候,Android是会主动清理门户的,那它又是如何判断哪个process是可以清掉的呢?文档中也提到了它的重要性排序:
1.最容易被清掉的是emptyprocess,空进程是指那些没有Activity与之绑定,也没有任何应用程序组件(如Services或者IntentReceiver)与之绑定的进程,也就是说在这个process中没有任何activity或者service之类的东西,它们仅仅是作为一个cache,在启动新的Activity时可以提高速度。它们是会被优先清掉的。因此建议,我们的后台操作,最好是作成Service的形式,也就是说应该在Activity中启动一个Service去执行这些操作。
2.接下来就是backgroundactivity了,也就是被stop掉了那些activity所处的process,那些不可见的Activity被清掉的确是安全的,系统维持着一个LRU列表,多个处于background的activity都在这里面,系统可以根据LRU列表判断哪些activity是可以被清掉的,以及其中哪一个应该是最先被清掉。不过,文档中提到在这个已被清掉的Activity又被重新创建的时候,它的onCreate会被调用,参数就是onFreeze时的那个Bundle。不过这里有一点不明白的是,难道这个Activity被killed时,Android会帮它保留着这个Bundle吗?
3.然后就轮到serviceprocess了,这是一个与Service绑定的进程,由startService方法启动。虽然它们不为用户所见,但一般是在处理一些长时间的操作(例如MP3的播放),系统会保护它,除非真的没有内存可用了。
4.接着又轮到那些visible activity了,或者说visibleprocess。前面也谈到这个情况,被Paused的Activity也是有可能会被系统清掉,不过相对来说,它已经是处于一个比较安全的位置了。
5.最安全应该就是那个foregroundactivity了,不到迫不得已它是不会被清掉的。这种process不仅包括resume之后的activity,也包括那些onReceiveIntent之后的IntentReceiver实例。
在AndroidApplication的生命周期的讨论中,文档也提到了一些需要注意的事项:因为Android应用程序的生存期并不是由应用本身直接控制的,而是由Android系统平台进行管理的,所以,对于我们开发者而言,需要了解不同的组件Activity、Service和IntentReceiver的生命,切记的是:如果组件的选择不当,很有可能系统会杀掉一个正在进行重要工作的进程。
自定义控件
这里主要介绍下“编辑日志”中使用的一个自定义EditText控件,它的效果如下图:
主要功能就是在文本语句之间绘制分割线。
public
static
class LinedEditText extendsprivateprivate// we need thisconstructor for LayoutInflater
public LinedEditText(Context context, AttributeSetattrs)
new=
new Paint();
voidint count ===for (int i =
0; i < count; i++int baseline = getLineBounds(i, r);
1, r.right, baseline +
1super
主要工作就是重载onDraw方法,利用从TextView继承下来的getLineCount函数获取文本所占的行数,以及getLineBounds来获取特定行的基准高度值,而且这个函数第二个参数会返回此行的“外包装”值。再利用这些值绘制这一行的线条。
为了让界面的View使用自定义的EditText类,必须在配置文件中进行设置
/>
这里class="com.example.android.notepad.NoteEditor$LinedEditText"就指明了应当使用自定义的LinedEditText类。
NoteEditor深入分析
首先来弄清楚“日志编辑“的状态转换,通过上篇文章的方法来做下面这样一个实验,
首先进入“日志编辑“时会触发onCreate和onResume,然后用户通过Option Menu选择”Edittitle”后,会触发onSaveInstanceState和onPause,最后,用户回到编辑界面,则再次触发onResume.
最终通过LogCat可以得到下图:

那么下面就按照上述顺序对此类进行剖析。
首先是onCreate方法,一开始先获取导致进入“日志编辑”界面的intent,分析其操作类型可得知是“编辑日志”还是“新增日志”。
final Intent intent =// Do some setup based on the action beingperformed.
final String action = intent.getAction();
若是“编辑日志”,则设置当前状态为“编辑”,并保存待编辑日志的URI.
若是“新增日志”,则设置当前状态为“新增”,并通过contentprovider向数据库中新增一个“空白日志”,后者返回“空白日志”的URI.
然后不管是“编辑”或“新增”,都需要从数据库中读取日志信息(当然,若是“新增”,读出来的肯定是空数据)。
mCursor = managedQuery(mUri, PROJECTION, null, null,null);
最后,类似于web应用中使用的Session,这里也将日志文本保存在InstanceState中,因此,若此activity的实例此前是处于stop状态,则我们可以从它那取出它原本的文本数据.
if (savedInstanceState !=
null= savedInstanceState.getString(ORIGINAL_CONTENT);
第二个来分析onResume函数,首先把游标置于第一行(也只有一行)
然后取出“正文”字段,这时有一个比较有趣的技巧,“设置文本”并不是调用setText,而是调用的setTextKeepState,后者相对于前者有一个优点,就是当界面此前stop掉,现在重新resume回来,那么此前光标所在位置仍然得以保存。而若使用setText,则光标会重置到行首。
最后,将当前编辑的正文保存到一个字符串变量中,用于当activity被暂停时使用。
if (mOriginalContent ==
null)
通过前面的图可以得知,activity被暂停时,首先调用的是onSaveInstanceState函数。
outState.putString(ORIGINAL_CONTENT, mOriginalContent);
这里就仅仅将当前正编辑的正文保存到InstanceState中(类似于Session).
最后来看onPause函数,这里首先要考虑的是若activity正要关闭,并且编辑区没有正文,则将此日志删除。
if (isFinishing() && (length ==
0) &&
!mNoteOnly)
newif (!mNoteOnly)
30)
'if (lastSpace >
0)
在生成Option Menu的函数onCreateOptionsMenu中,我们再一次看到下面这段熟悉的代码了:
Intent intent =
new Intent(null, getIntent().getData());
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
new ComponentName(this, NoteEditor.class), null, intent, 0,null);
这种生成动态菜单的机制在这篇文章中已经介绍过了,就不赘述了。
最后,来看下放弃日志和删除日志的实现,由于还没有接触到底层的contentprovider,这里都是通过getContentResolver()提供的update,delete,insert来向底层的contentprovider发出请求,由后者完成实际的数据库操作。
private
final
voidif (mCursor !=
nullif (mState ==// Put the original note text back into thedatabase
null=
new ContentValues();
if (mState ==// We inserted an empty note, make sure to deleteit
final
voidif (mCursor !=
null)
null;
剖析NotePadProvider
NotePadProvider就是所谓的contentprovider,它继承自android.content.ContentProvider,也是负责数据库层的核心类,主要提供五个功能:
这五个功能分别对应下述五个可以重载的方法:
public
int delete(Uri uri, String selection, String[]selectionArgs)
{
0publicreturn
nullpublic Uri insert(Uri uri, ContentValuesvalues)
{
nullpublic
booleanreturn
falsepublic Cursor query(Uri uri, String[] projection, Stringselection,
{
nullpublic
int update(Uri uri, ContentValues values, Stringselection,
{
0
这些都要你自己实现,不同的实现就是对应不同的content-provider。但是activity使用content-provider不是直接创建一个对象,然后调用这些具体方法。
而是调用managedQuery,getContentResolver().delete,update等来实现,这些函数其实是先找到符合条件的content-provider,然后再调用具体content-provider的函数来实现,那又是怎么找到content-provider,就是通过uri中的authority来找到content-provider,这些都是通过系统完成,应用程序不用操心,这样就达到了有效地隔离应用和内容提供者的具体实现的目的。
下面这三个字段指明了数据库名称,数据库版本,数据表名称。
private
static
final String DATABASE_NAME =
"note_pad.db";
private
static
final
int DATABASE_VERSION =
2;
private
static
final String NOTES_TABLE_NAME =
"notes";
实际的数据库操作其实都是通过一个私有静态类DatabaseHelper实现的,其构造函数负责创建指定名称和版本的数据库,onCreate函数则创建指定名称和各个数据域的数据表(就是简单的建表SQL语句)。onUpgrade负责删除数据表,再重新建表。
private
static
class DatabaseHelper extendsSQLiteOpenHelper
void onCreate(SQLiteDatabase db)
+ NOTES_TABLE_NAME +
" ("
+ Notes._ID +
" INTEGER PRIMARY KEY,"
+ Notes.TITLE +
" TEXT,"
+ Notes.NOTE +
" TEXT,"
+ Notes.CREATED_DATE +
" INTEGER,"
+ Notes.MODIFIED_DATE +
" INTEGER"
+
");"public
void onUpgrade(SQLiteDatabase db, int oldVersion, intnewVersion)
+ oldVersion +
" to "
+ newVersion +
", which will destroy all old data""DROP TABLE IF EXISTSnotes"
在这篇文章中我们已经见识到了getType函数的用处了,也正是通过它的解析,才能区分开到底是对全部日志还是对某一条日志进行操作。
publicswitchcasereturnca
new IllegalArgumentException
+
上面的sUriMatcher.match是用来检测uri是否能够被处理,而sUriMatcher.match(uri)返回值其实是由下述语句决定的。
new UriMatcher(UriMatcher.NO_MATCH);
sNotesProjectionMap这个私有字段是用来在上层应用使用的字段和底层数据库字段之间建立映射关系的,当然,这个程序里两处对应的字段都是一样(但并不需要一样)。
private
static HashMapstatic=
new HashMap();
}
数据库的增,删,改,查操作基本都一样,具体可以参考官方文档,这里就仅仅以删除为例进行说明。
一般可以分为三步来完成,首先打开数据库
switchcase= db.delete(NOTES_TABLE_NAME, where,whereArgs);
"="
++ (!TextUtils.isEmpty(where) ?
" AND ("
+ where +
')' : ""break
setResult(RESULT_OK, (newIntent()).setAction(mUri.toString()));
setResult(RESULT_CANCELED);
可到底想展示什么技术呢?实际上并没有完整展现出来,这里我对其进行修改后来指明 参见)。
private
static
final
int REQUEST_INSERT =
100;//请求插入标识符
然后修改onOptionsItemSelected函数如下:
public
boolean onOptionsItemSelected(MenuItemitem)
truereturn
super
protected
void onActivityResult(int requestCode, intif(requestCode==if(resultCode==RESULT_OK)
if(resultCode==RESULT_CANCELED)
转自:http://littlefermat.blog.163.com/blog/static/59771167201062510044904/
本文深入分析Android SDK自带的NotePad应用,涵盖Activity生命周期、菜单机制、自定义控件及ContentProvider实现等内容。
3063

被折叠的 条评论
为什么被折叠?



