摘要:分析NoteEditor这个类和以及Content Provider机制

  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实例剖析笔记(四)的更多相关文章

  1. Android实例剖析笔记(一)

    摘要:用实例讲解Andriod的开发过程 开卷语 俗话说,“熟读唐诗三百首,不会作诗也会吟”.最近收集了很多Android的示例代码,从这些代码的阅读和实验中学习到很多知识,从而产生写这个系列的打算, ...

  2. Android实例剖析笔记(二)

    摘要:用实例讲解Andriod的开发过程,以NotesList为实例介绍Android的菜单机制 简介 android提供了三种菜单类型,分别为options menu,context menu,su ...

  3. Android实例剖析笔记(三)

    摘要:点介绍Activity的生命周期,通过一个简单的实验来摸索状态转换的机制 Activity的生命周期 Activity类中有许多onXXX形式的函数可以重载,比如onCreate,onStart ...

  4. 【转】Android实例剖析笔记(二)--用实例讲解Andriod的开发过程,以NotesList为实例介绍Android的菜单机制

    原文网址:http://kb.cnblogs.com/page/78304/ 简介 android提供了三种菜单类型,分别为options menu,context menu,sub menu. op ...

  5. Android Studio 学习笔记(四):Adapter和RecyclerView说明

    在现版本中,滚动控件有多种,而相比于ListView,GridView,RecyclerView的用途更广,因此将前两者作为Adapter适配器的引入,再对RecyclerView进行简单讲解. MV ...

  6. Android自动化学习笔记之MonkeyRunner:官方介绍和简单实例

    ---------------------------------------------------------------------------------------------------- ...

  7. Android群英传笔记——第四章:ListView使用技巧

    Android群英传笔记--第四章:ListView使用技巧 最近也是比较迷茫,但是有一点点还是要坚持的,就是学习了,最近离职了,今天也是继续温习第四章ListView,也拖了其实也挺久的了,list ...

  8. Android实训案例(四)——关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程!

    Android实训案例(四)--关于Game,2048方块的设计,逻辑,实现,编写,加上色彩,分数等深度剖析开发过程! 关于2048,我看到很多大神,比如医生,郭神,所以我也研究了一段时间,还好是研究 ...

  9. 官网实例详解-目录和实例简介-keras学习笔记四

    官网实例详解-目录和实例简介-keras学习笔记四 2018-06-11 10:36:18 wyx100 阅读数 4193更多 分类专栏: 人工智能 python 深度学习 keras   版权声明: ...

随机推荐

  1. (转载)solr实现满足指定距离范围条件的搜索

    配置schema.xml <?xml version="1.0" encoding="UTF-8" ?> <schema name=" ...

  2. MySQL约束笔记

    MySQL 查看约束,添加约束,删除约束 添加列,修改列,删除列 · 查看表的字段信息:desc 表名; · 查看表的所有信息:show create table 表名; 添加主键约束:alter t ...

  3. 中断、轮询、事件驱动、消息驱动、数据流驱动(Flow-Driven)?

    轮询.事件驱动.消息驱动.流式驱动 ---数据流驱动 Unidirectional Architecture? 中断.事件.消息这样一种机制来实现更好的在多任务系统里运行... 阻塞,非阻塞同步,异步 ...

  4. 彻底卸载sql2008后重新安装

    彻底卸载sql2008方法 --打开控制面板,在控制面板中卸载所有带sql server的程序. --删除C:\Program Files\Microsoft SQL Server这整个文件夹, -- ...

  5. Temp权限导致Win10安装程序失败提示2502/2503错误

    有Win10用户在安装程序时遇到了安装失败错误代码2502/2503的问题.如图: 其原因是Windows Installer安装程序需要当前用户具有C:\Windows\Temp目录的完全控制权限, ...

  6. USACO 6.3 Cowcycles

    CowcyclesOriginally by Don Gillies [International readers should note that some words are puns on co ...

  7. Ubuntu 17.04 搭建 NodeJS

    可以在云主机上执行以下的命令: apt-get update apt-get install -y python-software-properties software-properties-com ...

  8. Ionic Js十六:滚动条

    ion-scroll ion-scroll 用于创建一个可滚动的容器. <ion-scroll [delegate-handle=""] [direction="& ...

  9. HDU - 1716 排列2 水题

    排列2 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submis ...

  10. oneDay

    难受过 迷茫过 失望过 耍脾气过 开心过 伤心过 疼过 走了这么久的路: 我只想说 程序的道路上 很难走: 本来准备都放弃了: 自己逼自己了很久想明白了: 不能什么时候都想着靠外力 自己的不足就是自己 ...