Android实例剖析笔记(四)
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,你可以看到下面这样的画面:

Android实例剖析笔记(四)的更多相关文章
- Android实例剖析笔记(一)
摘要:用实例讲解Andriod的开发过程 开卷语 俗话说,“熟读唐诗三百首,不会作诗也会吟”.最近收集了很多Android的示例代码,从这些代码的阅读和实验中学习到很多知识,从而产生写这个系列的打算, ...
- Android实例剖析笔记(二)
摘要:用实例讲解Andriod的开发过程,以NotesList为实例介绍Android的菜单机制 简介 android提供了三种菜单类型,分别为options menu,context menu,su ...
- Android实例剖析笔记(三)
摘要:点介绍Activity的生命周期,通过一个简单的实验来摸索状态转换的机制 Activity的生命周期 Activity类中有许多onXXX形式的函数可以重载,比如onCreate,onStart ...
- 【转】Android实例剖析笔记(二)--用实例讲解Andriod的开发过程,以NotesList为实例介绍Android的菜单机制
原文网址:http://kb.cnblogs.com/page/78304/ 简介 android提供了三种菜单类型,分别为options menu,context menu,sub menu. op ...
- Android Studio 学习笔记(四):Adapter和RecyclerView说明
在现版本中,滚动控件有多种,而相比于ListView,GridView,RecyclerView的用途更广,因此将前两者作为Adapter适配器的引入,再对RecyclerView进行简单讲解. MV ...
- Android自动化学习笔记之MonkeyRunner:官方介绍和简单实例
---------------------------------------------------------------------------------------------------- ...
- Android群英传笔记——第四章:ListView使用技巧
Android群英传笔记--第四章:ListView使用技巧 最近也是比较迷茫,但是有一点点还是要坚持的,就是学习了,最近离职了,今天也是继续温习第四章ListView,也拖了其实也挺久的了,list ...
- Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!
Android实训案例(四)--关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程! 关于2048,我看到很多大神,比如医生,郭神,所以我也研究了一段时间,还好是研究 ...
- 官网实例详解-目录和实例简介-keras学习笔记四
官网实例详解-目录和实例简介-keras学习笔记四 2018-06-11 10:36:18 wyx100 阅读数 4193更多 分类专栏: 人工智能 python 深度学习 keras 版权声明: ...
随机推荐
- vue.js学习 自定义过滤器使用(2)
gitHub地址: https://github.com/lily1010/vue_learn/tree/master/lesson05 一 自定义过滤器(注册在Vue全局) 注意事项: (1)全局方 ...
- 树莓派GPIO控制RGB彩色LED灯
树莓派GPIO通过PWM来控制RGB彩色LED灯,可以显示任何我们想要的颜色. RGB模块简介 这个RGB彩色LED里其实有3个灯,分别是红灯.绿灯和蓝灯.控制这三个灯分别发出不同强度的光,混合起来就 ...
- GUC-14 ForkJoin
import java.time.Duration; import java.time.Instant; import java.util.concurrent.ForkJoinPool; impor ...
- CROC 2016 - Elimination Round (Rated Unofficial Edition) E - Intellectual Inquiry dp
E - Intellectual Inquiry 思路:我自己YY了一个算本质不同子序列的方法, 发现和网上都不一样. 我们从每个点出发向其后面第一个a, b, c, d ...连一条边,那么总的不同 ...
- forms.ModelForm 与 forms.Form
1. 首先 两者都是forms里的常用类. 2. 这两个类在应用上是有区别的.一般情况下,如果要将表单中的数据写入数据库或者修改某些记录的值,就要让表单类继承ModelForm; 如果提交表单后 不会 ...
- hdoj2191 珍惜现在,感恩生活(01背包 || 多重背包)
题目链接 http://acm.hdu.edu.cn/showproblem.php?pid=2191 思路 由于每种大米可能不止一袋,所以是多重背包问题,可以直接使用解决多重背包问题的方法,也可以将 ...
- .NET Runtime version 2.0.50727.xxx 执行引擎错误。 (Fatal Execution Engine Error)
如题问题困扰本人良久. 尝试VS2005.VS2008.VS2010均出现过次问题. 主要现象: 1. Window设计器会崩溃,直接挂掉.(当逐条注释掉一些静态构造函数内的代码是情况好转) 2. 发 ...
- java 代码中设置 临时 环境变量
System.setProperty("hadoop.home.dir", "D:\\software\\software_install\\dev_install\\h ...
- Android 中的广播(Broadcast)
Android 广播(broadcast) 饮水思源 本文章内容学习和总结自 郭霖大神:<Android第一行代码> Overview 就像我们的学校里的喇叭一样,是用来通知的.而Andr ...
- ArrayList,Vector,LinkedList
韩梦飞沙 韩亚飞 313134555@qq.com yue31313 han_meng_fei_sha 数组列表 和 向量Vector, 都是使用数组方式存储. 向量 使用了 同步synchr ...