Sample导入
NotePad是早期Android版本的Sample,笔者并没有找到其Android Studio版本的代码,猜想主要是由于该Sample采用的很多技术都已经被废弃,于是将其早前Eclipse版本转化而来,具体的代码见Github: NotePad
整个NotePad的目录结构如下图所示:
一共包含了6个类,其中4个Activity,一个ContentProvider,还有一个数据契约类。
- NotesList 应用程序的入口,笔记本的首页面会显示笔记的列表
- NoteEditor 编辑笔记内容的Activity
- TitleEditor 编辑笔记标题的Activity
- NotesLiveFolder ContentProvider的LiveFolder(实时文件夹),这个功能在Android API 14后被废弃,不再支持。因此代码中所有涉及LiveFolder的内容将不再阐述。
- NotePadProvider 这是笔记本应用的ContentProvider,也是整个应用的关键所在
[scheme:]scheme-specific-part[#fragment]
[ ]表示可选,层次化的URI的scheme-specific-part部分又可以进一步分解为:
[scheme:][//authority][path][?query][#fragment]
一些URI的例子:
http://java.sun.com/j2se/1.3/
file:///~/calendar
content://com.google.provider.NotePad/notes
URI协议使用“content:”就表示使用了ContentProvider。
MIME
MIME(Multipurpose Internet Mail Extensions),全称多用途互联网邮件扩展类型,Android使用MIME指定数据的类型。ContentProvider的getType(Uri uri)返回一个MIME格式的字符串,其描述内容为uri参数所对应的数据类型。
对于文本、HTML 或 JPEG 等常见数据类型,getType() 应该为该数据返回标准 MIME 类型。
对于指向一个或多个表数据行的内容 URI,getType() 应该以 Android 供应商特有 MIME 格式返回 MIME 类型,包括3个部分:<类型部分><子类型部分><提供程序特有部分>
- 类型部分:vnd
- 子类型部分:
- 如果 URI 模式用于单个行:android.cursor.item/
- 如果 URI 模式用于多个行:android.cursor.dir/
- 提供程序特有部分:vnd.<name>.<type>,<name> 值应具有全局唯一性,<type> 值应在对应的 URI 模式中具有唯一性。
MIME举例:
vnd.android.cursor.dir/vnd.com.example.provider.table1
表示table1中多行记录的MIME类型;
vnd.android.cursor.item/vnd.com.example.provider.table1
表示table1中单行记录的MIME类型。
NotePad相关类解析
NotePad
NotePad是数据契约类,用来提供一种统一的数据访问格式。整个笔记本应用只有一张表——“notes”。notes表共包含5个属性列:
列名 | 类型 | 说明 |
---|---|---|
_ID | INT | 继承自BaseColumns的主键 |
title | TEXT | 笔记的标题 |
note | TEXT | 笔记的内容 |
created | INT | 笔记的创建时间 |
modified | INT | 笔记的修改时间 |
NotePad定义了两种URI,这里URI对应三部分内容:[scheme:][//authority][path]。[scheme://]对应于“content://”; authority部分是"com.google.provider.NotePad";path则根据不同URI模式而不同。对于整个表操作的URI:
content://com.google.provider.NotePad/notes
;对于表中某一行进行操作的URI:
content://com.google.provider.NotePad/notes/
同时,NotePad也定义与上述两种Uri匹配的两种MIME类型。对应URI模式用于多行(对应于整个表的情况):
vnd.android.cursor.dir/vnd.google.note
对应URI模式用于单行(对应于表中某一行的情况):
vnd.android.cursor.item/vnd.google.note
NoteList
应用程序入口,是一个ListActivity,以列表方式显示笔记本条目。NoteList的运行截图:
没有笔记被复制时,“Paste”菜单不可用
"Hello World"笔记被复制时,“Paste”菜单可用
长按笔记之后弹出的上下文菜单
数据装配
NoteList使用SimpleCursorAdapter来装配数据,首先查询数据库的内容,如下代码所示,这里使用ContentProvider默认的URI。
Cursor cursor = managedQuery(
getIntent().getData(),
PROJECTION,
null,
null,
NotePad.Notes.DEFAULT_SORT_ORDER);
managedQuery已经不推荐使用,可以使用ContentResolver进行数据解析,其本质上就是调用ContentProvider定义的数据使用方法。
然后通过SimpleCursorAdapter来进行装配:
SimpleCursorAdapter adapter
= new SimpleCursorAdapter(
this,
R.layout.noteslist_item,
cursor,
dataColumns,
viewIDs);
第一个参数指明上下文(Context),第二个参数指明数据列表项(Item)的布局,第三个参数为上文managedQuery已获取的数据的游标,第四个参数为数据库表的投影列,第五个参数为数据库表投影列在ListView上显示所对应的布局ID。这里NoteList包含的ListView只显示title这一列,如果要在ListView中显示更多表属性列内容的话,可以在这里做文章。
此外,5个参数版本的SimpleCursorAdapter已经不推荐使用,Android官方推荐的标准版本为
SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags)
多了一个flags,决定adapter的行为,主要有两类:FLAG_AUTO_REQUERY和FLAG_REGISTER_CONTENT_OBSERVER.
菜单设计
NoteList包含两种菜单的设计,Options Menu(可选菜单)和Context Menu(上下文菜单)。Options Menu实现了动态菜单(基于Intent的菜单项),使用menu.addIntentOptions()方法,具体可参见[4]。
onCreateOptionsMenu方法只在菜单创建时调用一次,而onPrepareOptionsMenu方法则在每次显示菜单之前被调用。在onPrepareOptionsMenu方法中,Paste菜单项会根据剪贴板中是否有内容来决定是否enable;如果当前List中的条目数非空,则获取相应的ListItem ID,拼接URI,添加至addIntentOptions()。
页面跳转
不管是可选菜单、上下文菜单中的操作,还是单击列表中的笔记条目,其相应的页面跳转都是通过Intent的Action+URI进行。比如startActivity(new Intent(Intent.ACTION_EDIT, noteUri)),这里就会寻找能够进行EDIT的Activity跳转,同时传递出URI数据。所有的Intent过滤规则都在AndroidManifest.xml中定义。
NotePadProvider
NotePad的ContentProvider,NotePad的内容提供者,这里要注意实际上NotePad应用并不允许其他程序共享其数据,因为它在AndroidManifest.xml中声明provider标签的exported属性为false:
<provider android:name="NotePadProvider"
android:authorities="com.google.provider.NotePad"
android:exported="false">
<grant-uri-permission android:pathPattern=".*" />
</provider>
DatabaseHelper
DatabaseHelper是SQLiteOpenHelper的子类,用来辅助数据库的管理。其onCreate方法和onUpgrade方法必须被重写,通常用来创建数据库表和升级数据库。而构造函数通常也是必须的,用来调用父类(SQLiteOpenHelper)的构造函数来创建数据库。具体内容可以参考:[3]
UriMatcher实用类
UriMatcher,它会将内容 URI“模式”映射到整型值。 这样就可以在一个 switch 语句中使用这些整型值,为匹配特定模式的一个或多个内容 URI 选择所需操作,具体可参考[2]。NotePad的UriMatcher设定了两个URI的对应关系
content://com.google.provider.NotePad/notes 映射整数 1
content://com.google.provider.NotePad/notes/# 映射整数 2
ContentProvider的基本方法
ContentProvider有6个基本方法必须被重写:
方法 | 说明 |
---|---|
onCreate | ContentProvider创建时被调用 |
getType | 获取MIME类型 |
insert | 插入记录时被调用 |
delete | 删除记录时被调用 |
update | 更新记录时被调用 |
query | 查询记录时被调用 |
ContentProvider基本方法的具体实现看似很复杂,其实有很多相似的套路:
- 通过SQLiteOpenHelper的实例(mOpenHelper)的getWritableDatabase或者getReadableDatabase方法获取SQLiteDatabase的对象实例(db)
- 通过db实例的增、删、改、查方法来操作数据库,但是要注意这里要区分上文提到的两种URI:对应于表的多行的URI和对应于表的单行的URI。
以下以query为例说明基本的实现逻辑:
ContentProvider的Query方法
基于不同的URI模式匹配来补充查询中的“Where”条件(UriMatcher类)。如果匹配的是NOTES(表),则不作特殊处理;如果匹配的NOTE_ID(表中的行),则补充Where中的ID条件,从Uri中提取ID。
NoteEditor
笔记内容编辑页面,页面截图如下:
NoteEditor通过扩展EditText定义了笔记本“编辑框“,主要自定义了画笔,重写了onDraw函数。其onCreate函数获取了Intent中的Action和URI,随后根据不同Action,进行不同的操作。
onResume函数有个小细节,当处于EDIT(编辑)和INSERT(插入)状态时,NoteEditor显示的标题不同。
TitleEditor
笔记标题编辑的Activity,页面截图如下:
TitleEditor设计对对话框的样式,查看AndroidManifest.xml中的主题定义:
android:theme=“@android:style/Theme.Holo.Dialog”
项目编译相关
如何编译项目
Github上的NotePad由于上传时间问题与最新的Android Studio编译环境存在差异,需修改相关的编译文件。
以下基于Android Studio3.5进行修改,更新的版本差异较大,应结合具体Android Studio版本进行修改。
在“工程视图”中,大致需要修改3个文件,如下图所示:
按照从上往下的顺序修改:
分别为应用程序模块的gradle脚本文件,gradle的Wrapper文件,以及项目的gradle脚本文件。
应用程序模块的gradle脚本文件修改
典型的修改之后的例子如下:
主要包括本地目标SDK版本,compileSdkVersion和targetSdkVersion,以及buildToolsVersion(最新的Android Studio可能无需指定)。
特别强调,如果需要添加一些依赖,应增加“dependencies”脚本模块。
gradle的Wrapper文件修改
主要修改distributionUrl值,根据本机实际情况进行修改,如下图:
项目的gradle脚本文件修改
主要增加了google()代码仓库,以及修改了classpath的值,如下图所示:
项目编译问题解决
- Android Studio 2021.3.1版本环境下的编译:编译参考链接
- Android Studio 2020.3.1版本环境下的编译:解决参考链接
- BUG:android: exported needs to be explicitly specified for <activity>,解决参考链接
参考文献
注:Android开发文档来自于developer.android.com,也可以参考相关的国内镜像
[1] Android开发文档,URI,
[2] Android开发文档,创建内容提供程序
[3] Android开发文档,创建数据库
[4] Android开发文档,菜单