NoteEditor深入分析
首先来弄清楚“日志编辑“的状态转换,通过上篇文章的方法来做下面这样一个实验,首先进入“日志编辑“时会触发onCreate和onResume,然后用户通过Option Menu选择”Edit title”后,会触发onSaveInstanceState和onPause,最后,用户回到编辑界面,则再次触发onResume。
最终通过LogCat可以得到下图:
那么下面就按照上述顺序对此类进行剖析。首先是onCreate方法,一开始先获取导致进入“日志编辑”界面的intent,分析其操作类型可得知是“编辑日志”还是“新增日志”。
final
Intent intent
=
getIntent();
//
Do some setup based on the action being performed.
final
String action
=
intent.getAction();
若是“编辑日志”,则设置当前状态为“编辑”,并保存待编辑日志的URI.
mState
=
STATE_EDIT;
mUri
=
intent.getData();
若是“新增日志”,则设置当前状态为“新增”,并通过content provider向数据库中新增一个“空白日志”,后者返回“空白日志”的URI.
mState
=
STATE_INSERT;
mUri
=
getContentResolver().insert(intent.getData(),
null
);
然后不管是“编辑”或“新增”,都需要从数据库中读取日志信息(当然,若是“新增”,读出来的肯定是空数据)。
mCursor
=
managedQuery(mUri, PROJECTION,
null
,
null
,
null
);
最后,类似于web应用中使用的Session,这里也将日志文本保存在InstanceState中,因此,若此activity的实例此前是处于stop状态,则我们可以从它那取出它原本的文本数据.
if
(savedInstanceState
!=
null
)
{
mOriginalContent
=
savedInstanceState.getString(ORIGINAL_CONTENT);
}
第二个来分析onResume函数,首先把游标置于第一行(也只有一行)
mCursor.moveToFirst();
然后取出“正文”字段,这时有一个比较有趣的技巧,“设置文本”并不是调用setText,而是调用的setTextKeepState,后者相对于前者有一个优点,就是当界面此前stop掉,现在重新resume回来,那么此前光标所在位置仍然得以保存。而若使用setText,则光标会重置到行首。
String note
=
mCursor.getString(COLUMN_INDEX_NOTE);
mText.setTextKeepState(note);
最后,将当前编辑的正文保存到一个字符串变量中,用于当activity被暂停时使用。
if
(mOriginalContent
==
null
)
{
mOriginalContent
=
note;
}
通过前面的图可以得知,activity被暂停时,首先调用的是onSaveInstanceState函数。
outState.putString(ORIGINAL_CONTENT, mOriginalContent);
这里就仅仅将当前正编辑的正文保存到InstanceState中(类似于Session)。最后来看onPause函数,这里首先要考虑的是若activity正要关闭,并且编辑区没有正文,则将此日志删除。
if
(isFinishing()
&&
(length
==
0
)
&&
!
mNoteOnly)
{
setResult(RESULT_CANCELED);
deleteNote();
}
否则的话,就更新日志信息
ContentValues values
=
new
ContentValues();
if
(
!
mNoteOnly)
{
values.put(Notes.MODIFIED_DATE, System.currentTimeMillis());
if
(mState
==
STATE_INSERT)
{
String title
=
text.substring(
0
, Math.min(
30
, length));
if
(length
>
30
)
{
int
lastSpace
=
title.lastIndexOf(
'
'
);
if
(lastSpace
>
0
)
{
title
=
title.substring(
0
, lastSpace);
}
}
values.put(Notes.TITLE, title);
}
}
values.put(Notes.NOTE, text);
getContentResolver().update(mUri, values,
null
,
null
);
}
}
在生成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
);
这种生成动态菜单的机制在Android实例剖析笔记(二)这篇文章中已经介绍过了,就不赘述了。最后,来看下放弃日志和删除日志的实现,由于还没有接触到底层的content provider,这里都是通过getContentResolver()提供的update,delete,insert来向底层的content provider发出请求,由后者完成实际的数据库操作。
private
final
void
cancelNote()
{
if
(mCursor
!=
null
)
{
if
(mState
==
STATE_EDIT)
{
//
Put the original note text back into the database
mCursor.close();
mCursor
=
null
;
ContentValues values
=
new
ContentValues();
values.put(Notes.NOTE, mOriginalContent);
getContentResolver().update(mUri, values,
null
,
null
);
}
else
if
(mState
==
STATE_INSERT)
{
//
We inserted an empty note, make sure to delete it
deleteNote();
}
}
setResult(RESULT_CANCELED);
finish();
}
private
final
void
deleteNote()
{
if
(mCursor
!=
null
)
{
mCursor.close();
mCursor
=
null
;
getContentResolver().delete(mUri,
null
,
null
);
mText.setText(
""
);
}
}
剖析NotePadProvider
NotePadProvider就是所谓的content provider,它继承自android.content.ContentProvider,也是负责数据库层的核心类,主要提供五个功能:
1)查询数据
2)修改数据
3)添加数据
4)删除数据
5)返回数据类型
这五个功能分别对应下述五个可以重载的方法:
public
int
delete(Uri uri, String selection, String[] selectionArgs)
{
return
0
;
}
public
String getType(Uri uri)
{
return
null
;
}
public
Uri insert(Uri uri, ContentValues values)
{
return
null
;
}
public
boolean
onCreate()
{
return
false
;
}
public
Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
{
return
null
;
}
public
int
update(Uri uri, ContentValues values, String selection,
String[] selectionArgs)
{
return
0
;
}
这些都要你自己实现,不同的实现就是对应不同的content-provider。但是activity使用content-provider不是直接创建一个对象,然后调用这些具体方法。
而是调用managedQuery,getContentResolver().delete,update等来实现,这些函数其实是先找到符合条件的content-provider,然后再调用具体content-provider的函数来实现,那又是怎么找到content-provider,就是通过uri中的authority来找到content-provider,这些都是通过系统完成,应用程序不用操心,这样就达到了有效地隔离应用和内容提供者的具体实现的目的。
有了以上初步知识后,我们来看NotePadProvider是如何为上层提供数据库层支持的。下面这三个字段指明了数据库名称,数据库版本,数据表名称。
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
extends
SQLiteOpenHelper
{
DatabaseHelper(Context context)
{
super
(context, DATABASE_NAME,
null
, DATABASE_VERSION);
}
@Override
public
void
onCreate(SQLiteDatabase db)
{
db.execSQL(
"
CREATE TABLE
"
+
NOTES_TABLE_NAME
+
"
(
"
+
Notes._ID
+
"
INTEGER PRIMARY KEY,
"
+
Notes.TITLE
+
"
TEXT,
"
+
Notes.NOTE
+
"
TEXT,
"
+
Notes.CREATED_DATE
+
"
INTEGER,
"
+
Notes.MODIFIED_DATE
+
"
INTEGER
"
+
"
);
"
);
}
@Override
public
void
onUpgrade(SQLiteDatabase db,
int
oldVersion,
int
newVersion)
{
Log.w(TAG,
"
Upgrading database from version
"
+
oldVersion
+
"
to
"
+
newVersion
+
"
, which will destroy all old data
"
);
db.execSQL(
"
DROP TABLE IF EXISTS notes
"
);
onCreate(db);
}
}
在Android实例剖析笔记(一)这篇文章中我们已经见识到了getType函数的用处了,也正是通过它的解析,才能区分开到底是对全部日志还是对某一条日志进行操作。
public
String getType(Uri uri)
{
switch
(sUriMatcher.match(uri))
{
case
NOTES:
return
Notes.CONTENT_TYPE;
case
NOTE_ID:
return
Notes.CONTENT_ITEM_TYPE;
default
:
throw
new
IllegalArgumentException(
"
Unknown URI
"
+
uri);
}
}
上面的sUriMatcher.match是用来检测uri是否能够被处理,而sUriMatcher.match(uri)返回值其实是由下述语句决定的。
sUriMatcher
=
new
UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(NotePad.AUTHORITY,
"
notes
"
, NOTES);
sUriMatcher.addURI(NotePad.AUTHORITY,
"
notes/#
"
, NOTE_ID);
sNotesProjectionMap这个私有字段是用来在上层应用使用的字段和底层数据库字段之间建立映射关系的,当然,这个程序里两处对应的字段都是一样(但并不需要一样)。
private
static
HashMap
<
String, String
>
sNotesProjectionMap;
static
{
sNotesProjectionMap
=
new
HashMap
<
String, String
>
();
sNotesProjectionMap.put(Notes._ID, Notes._ID);
sNotesProjectionMap.put(Notes.TITLE, Notes.TITLE);
sNotesProjectionMap.put(Notes.NOTE, Notes.NOTE);
sNotesProjectionMap.put(Notes.CREATED_DATE, Notes.CREATED_DATE);
sNotesProjectionMap.put(Notes.MODIFIED_DATE, Notes.MODIFIED_DATE);
}
数据库的增,删,改,查操作基本都一样,具体可以参考官方文档,这里就仅仅以删除为例进行说明。一般可以分为三步来完成,首先打开数据库
SQLiteDatabase db
=
mOpenHelper.getWritableDatabase();
然后根据URI指向的是日志列表还是某一篇日志,到数据库中执行删除动作
switch
(sUriMatcher.match(uri)) {
case
NOTES:
count
=
db.delete(NOTES_TABLE_NAME, where, whereArgs);
break
;
case
NOTE_ID:
String noteId
=
uri.getPathSegments().get(
1
);
count
=
db.delete(NOTES_TABLE_NAME, Notes._ID
+
"
=
"
+
noteId
+
(
!
TextUtils.isEmpty(where)
?
"
AND (
"
+
where
+
'
)
'
:
""
), whereArgs);
break
;
}
最后,一定记得通知上层:其传递下来的URI在底层数据库中已经发生了变化。
getContext().getContentResolver().notifyChange(uri,
null
);
对NotePad的改进
首先我想指出NotePad的一个bug,其实这个小bug在2月份就有人向官方报告了,参见http://code.google.com/p/android/issues/detail?id=1909。NoteEditor类中的变量mNoteOnly根本就是没有用处的,因为它始终都是false,没有任何变化,所以可以删除掉。第二点是在NoteEditor类中,有下面这样的语句:
setResult(RESULT_OK, (
new
Intent()).setAction(mUri.toString()));
setResult(RESULT_CANCELED);
可到底想展示什么技术呢?实际上并没有完整展现出来,这里我对其进行修改后来指明。参见http://code.google.com/p/android/issues/detail?id=1671)。首先在NotesList类中增加一个变量
private
static
final
int
REQUEST_INSERT
=
100
;
//
请求插入标识符
然后修改onOptionsItemSelected函数如下:
@Override
public
boolean
onOptionsItemSelected(MenuItem item)
{
switch
(item.getItemId())
{
case
MENU_ITEM_INSERT:
this
.startActivityForResult(
new
Intent(Intent.ACTION_INSERT, getIntent().getData()), REQUEST_INSERT);
return
true
;
}
return
super
.onOptionsItemSelected(item);
}
最后重载onActivityResult函数来处理接收到的activity result。
protected
void
onActivityResult(
int
requestCode,
int
resultCode, Intent data)
{
if
(requestCode
==
REQUEST_INSERT)
{
if
(resultCode
==
RESULT_OK)
{
Log.d(TAG,
"
OK!!!
"
);
}
else
if
(resultCode
==
RESULT_CANCELED)
{
Log.d(TAG,
"
CANCELED!!!
"
);
}
}
}
试试,当你在NoteEditor中保存或放弃日志时,观察LogCat,你可以看到下面这样的画面: