第三章 开发应用
第一节 实现用户接口
第一小节 屏幕条目的分层结构
Android应用的基本功能单元是activity,一个android.app.Activity类对象。一个activity能实现很多事情,但是他本身没有屏幕呈现功能。实现屏幕呈现和设计UI,可以使用views和viewgroups,Android平台的基本用户接口单元。
Views
一个view是一个基类android.view.View的对象,它是一个数据结构,它保存了一块特定方形屏幕区域的布局和内容。一个View对象处理测量、放置、画布、焦点改变、滚动条和键盘/姿态响应。
view类是widgets(完全实现屏幕交互条目的类集)的一个基类。Widgets处理自己色测量、画布,因此你能更快的建立自己的UI。Widgets包括Text, EditText, InputMethod, MovementMethod, Button, RadioButton, CheckBox和ScrollView。
Viewgroups
一个viewgroup是android.view.Viewgroup的对象。见词释义,一个viewgroup是一些特定类型view对象的集合,viewgroup能给你的UI增加架构,并且为单个实体建立复杂的屏幕条目。
viewgroup类是layout(完全实现屏幕布局的类集)的一个基类。这些布局能够用来实现view集的架构。
树形结构UI
在Android平台上,用view或viewgroup定义一个activity的UI,象下图所示。这个树可以是很简单或很复杂的,你可以用widget集和布局或客户化的view类型来建立它。
要把这个树放到屏幕上渲染,activity调用setContentView()方法,传递一个参考给根节点。一旦android具有根节点对象的参考,就可以直接和节点失效、测量和画这个树。根节点接着要求子节点画他们自己,当然,树上的每个veiwgroup直接画它的子节点。
就象之前所说的,每个viewgroup测量它的有效的空间,布置它的子节点,让每个子节点调用Draw()方法渲染自己。每个子节点需要在父节点的大小和位置,但是父对象有最后的决定权决定每个子节点放在哪个位置、占多大空间。
LayoutParams
每个viewgroup用嵌套的扩展了ViewGroup.LayoutParams的类。这些子类定义了儿子的大小和位置。
注意每个LayoutParams子类有它自己的语法。每个子条目必须定义LayoutParams相应它的父母,虽然它也许为它的儿子定义不同的LayoutParams。
所有的viewgroup都包括高度和宽度,许多包括边界空白和界宽。你可以指定宽度和高度,虽然你也许不想这样做。更多时候也许高速你的view去适配它的内容边界大小,或者变成它所包括的对象一样大。
第二小节 常用的布局对象
FrameLayout
FrameLayout是一个简单的布局对象,在你的屏幕上保留一部分空白的空间,以后你可以用简单对象在填充这个空间。所有的子条目都定位于屏幕的左上角,你不能指定FrameLayout儿子的位置。后来的子对象将简单的部分或全部的覆盖更早的对象,或者模糊化它(假定新的对象是透明的)。
LinearLayout
一个LinearLayout分配所有的子对象在单个方向-水平或垂直,取决于你在LinearLayout上设置的属性。所有子对象都是一个跟着一个堆叠,因此一个垂直列表每行只有一个子对象,不管他们占多宽,一个水平列表只有一行的高度(它的高度就是最高的子对象的高度加上填充)。LinearLayout保持子对象之间的边界,和gravity(子对象的对齐方式:左对齐、右对齐、中心对齐等)。
LinearLayout也支持分配一个权重给子对象。这个值允许子对象扩展它自己来填充任何屏幕上的空白。这样可以防止一群小对象被绑定在大屏幕的边角上,允许他们扩展空间。子对象指定一个权重值,任何余下的空间将按比例分配给这些子对象。缺省权重是0,例如,假定有三个文本框,其中两个权重为1,则这两个将等比例的扩展来填充余下的空间,第三个将不会有任何变化。
下面两个表单呈现了LinearLayout布局:一个Button,一些Label,一些TextBox,加上一些填充值。TextBox设置它的宽度为FILL_PARENT,其它的设置为WRAP_CONTENT。对齐点,缺省在左边,左边的表单没有设置权重(就是缺省值0),右边表单的comments文本框权重设置为1。加入Name文本框也被设置成1,那么Name文本框和comments文本框具有相同的高度。
在水平的LinearLayout,这些子对象会以文本基线对齐。这样可以方便使用者撇一眼文本的时候,不至于对不齐而看走眼。当然这个也可以通过设置布局xml文件的android:baselineAligned="false"来关闭。
TableLayout
TableLayout以行列方式定位它的子对象,一个tablelayout有一个或多个TableRow对象组成,每个TableRow对象定义一行(其实,你可以有其它子对象)。TableLayout容器并不显示行、列、单元格的边界,每行有0个或多个单元格,每个单元格可放一个一个View对象。一个表有许多列,一个表可以有空单元格。单元格不能产生列,跟在HTML里面一样。下面图片显示了TableLayout,带有不可见的点线边界。
列可以被隐藏,能够延展来填充整个屏幕空间,也可以收缩来适应适应屏幕。
AbsoluteLayout
AbsoluteLayout必须指定屏幕上的x/y坐标,(0,0)为屏幕的左上角,坐标值随着向右向下而增加。边界空白不支持,重叠允许(但不建议)。通常不建议使用AbsoluteLayout,除非必须,因为它是刚性的,可能在不同设备上工作不一致。
RelativeLayout
RelativeLayout子对象指定相对于其它对象的相对位置。因此可以分配两个对象在右边界,或者让一个在另一个下面,或者处于屏幕中心。这些条目按照给定顺序渲染,假如第一个条目在屏幕中心,其它相对于它的条目将会相对于屏幕中心。假如用xml文件指定布局,必须首先指定一个参考条目。
这是个例子,包括可见和不可见条目。
本图显示了屏幕对象的类名,以及他们的属性。这些属性有些被这些条目直接支持,一些通过LayoutParams成员支持(屏幕上所有对象都是RelativeLayout的子条目)。RelativeLayout参数是宽度、高度、在下面、在顶上、在左边、填充和左边界。注意这些参数中的一些是相对于其它子对象,包括在左边、在顶上、在下面等等。
一些重要的视图组
类 | 描述 |
AbsoluteLayout | 绝对位置布局 |
FrameLayout | 框架布局 |
Gallery | 水平滚动布局 |
GridView | 网格布局 |
LinearLayout | 线性布局 |
ListView | 列表布局 |
RelativeLayout | 相对布局 |
ScrollView | 垂直滚动布局 |
Spinner | 在一行文本框里一次显示一个条目 |
SurfaceView | 直接访问画布 |
TabHost | 标签列表布局 |
TableLayout | 表格布局 |
ViewFlipper | 幻灯式布局 |
ViewSwitcher | 同上ViewFlipper |
第三小节 和AdapterView工作(绑定数据)
一些视图组有UI,这些对象具有典型子类AdapterView。例子包括Gallery和ListView,这些对象通常要做两个工作:
用数据填充布局
处理用户选择
用数据填充布局
绑定类到Adapter,Adapter从代码得到数据,或者从设备数据库查询到结果。
// Get a Spinner and bind it to an ArrayAdapter that
// references a String array.
Spinner s1 = (Spinner) findViewById(R.id.spinner1);
ArrayAdapter adapter = ArrayAdapter.createFromResource(
this, R.array.colors, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
s1.setAdapter(adapter);
// Load a Spinner and bind it to a data query.
private static String[] PROJECTION = new String[] {
People._ID, People.NAME
};
Spinner s2 = (Spinner) findViewById(R.id.spinner2);
Cursor cur = managedQuery(People.CONTENT_URI, PROJECTION, null, null);
SimpleCursorAdapter adapter2 = new SimpleCursorAdapter(this,
android.R.layout.simple_spinner_item, // Use a template
// that displays a
// text view
cur, // Give the cursor to the list adatper
new String[] {People.NAME}, // Map the NAME column in the
// people database to...
new int[] {android.R.id.text1}); // The "text1" view defined in
// the XML template
adapter2.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
s2.setAdapter(adapter2);
注意:在CursorAdapter的PROJECTION里面的People._ID是必须的,否则会得到一个异常。
处理用户选择
通过设置类的AdapterView.OnItemClickListener成员到监听器,能够捕获选择变化。
// Create a message handling object as an anonymous class.
private OnItemClickListener mMessageClickedHandler = new OnItemClickListener() {
public void onItemClick(AdapterView parent, View v, int position, long id)
{
// Display a messagebox.
Toast.makeText(mContext,"You've got an event",Toast.LENGTH_SHORT).show();
}
};
// Now hook into our object and set its onItemClickListener member
// to our class handler object.
mHistoryView = (ListView)findViewById(R.id.history);
mHistoryView.setOnItemClickListener(mMessageClickedHandler);
第四小节 用XML设计屏幕布局
因为在代码里设计屏幕布局是繁笨的,Android支持通过XML来设计屏幕。Android定义了大量的客户化条目,每个条目代表一个特定的Android View子类。可以象建立HTML文件一样设计屏幕布局,用一系列的标记,保存在应用的res/layout/目录的XML文件里。要学习XML文件格式和哪些条目被使用,请参考布局资源章节。每个文件描述一个单个的android.view.View,也可以是一些简单的可视化的条目,或者包括子对象的布局条目。当Android编译应用时,它编译每个文件到android.view.View资源,可以用setContentView(R.layout.layout_file_name)代码在Activity.onCreate()实现里面来装载。
每个XML文件有Android GUI类的一些标记组成,这些标记有类方法对应的属性(例如,EditText有text属性对应于EditText.setText)。
注意:类和方法名字之间、条目和属性名字之间不是刚好1:1的关系,虽然大部分情况下是1:1关系。
Android趋向于按照呈现在XML文件里面的顺序绘制各个条目。因此,假如条目有重叠,在XML文件的最后一个绘制在顶层。
每个XML文件被编译成一颗树,树根是单个的View或ViewGroup对象,因此必须有单个的根标记。
命名为layout_something的属性是LayoutParams成员。
下面这些值可以用来作度量衡:
px (pixels) 点
dip (device independent pixels) 独立于设备的点
sp (scaled pixels — best for text size) 扩展点
pt (points) 点
in (inches) 英寸
mm (millimeters) 毫米
例如:android:layout_width="25px"
下面的XML文件建立了屏幕布局,注意屏幕顶部的文本由Activity.setTitle设置。注意,参考相对条目的属性用ID相关资源来参考(@id/id_number)。
<?xml version="1.0" encoding="utf-8"?>
<!-- Demonstrates using a relative layout to create a form -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@drawable/blue"
android:padding="10px">
<TextView id="@+id/label"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Type here:"/>
<EditText id="@+id/entry"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:drawable/editbox_background"
android:layout_below="@id/label"/>
<Button id="@+id/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/entry"
android:layout_alignParentRight="true"
android:layout_marginLeft="10px"
android:text="OK" />
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/ok"
android:layout_alignTop="@id/ok"
android:text="Cancel" />
</RelativeLayout>
装载XML资源
装载这些布局资源非常容易,只要简单调用onCreate()方法:
protected void onCreate(Bundle savedValues)
{
// Be sure to call the super class.
super.onCreate(savedValues);
// Load the compiled layout resource into the window's
// default ViewGroup.
// The source file is res/layout/hello_activity.xml
setContentView(R.layout.hello_activity);
// Retrieve any important stored values.
restoreValues(savedValues);
}
第五小节 屏幕条目钩子
可以通过Activity.findViewById得到屏幕条目句柄,可以用这个句柄设置或恢复任何值:
TextView msgTextView = (TextView)findViewById(R.id.msg);
msgTextView.setText(R.string.push_me);
第六小节 UI通知的监听
一些UI通知自动地被Android触发。实例,Activity的onKeyDown和onKeyUp,和Widget的onFocusChanged(boolean,int,Rect)。但是呢,一些重要的回调函数,例如按钮点击,不能自动通知,必须手工注册。
public class SendResult extends Activity
{
/**
* Initialization of the Screen after it is first created. Must at least
* call setContentView() to
* describe what is to be displayed in the screen.
*/
protected void onCreate(Bundle savedValues)
{
...
// Listen for button clicks.
Button button = (Button)findViewById(R.id.corky);
button.setOnClickListener(mCorkyListener);
}
// Create an anonymous class to act as a button click listener.
private OnClickListener mCorkyListener = new OnClickListener()
{
public void onClick(View v)
{
// To send a result, simply call setResult() before your
// activity is finished, building an Intent with the data
// you wish to send.
Intent data = new Intent();
data.setAction("Corky!");
setResult(RESULT_OK, data);
finish();
}
};
第七小节 在应用上使用主题
假如你不明确指定应用主题,Android将使用缺省的由android.R.style.Theme定义的主题。多数情况下你想使用不同的主题(如Theme.Light)或建立自己的主题。
为了在XML里面设置主题,在AndroidManifest.xml文件里用theme属性来指定,可以使用<application>标记来指定缺省主题,用<activity>控制部分主题。
<!-- AndroidManifest.xml-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.home">
<application android:theme="@android:style/Theme.Dark" >
<activity class=".Home"
...
</activity>
</application>
</manifest>
也可以用编程方式来实现主题,确保在建立任何view之前设置主题,以便正确的主题被用于你的所有UI条目。
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
...
setTheme(android.R.style.Theme_Light);
setContentView(R.layout.linear_layout_3);
}
第八小节 UI条目和概念术语
Activity:
Android应用的标准屏幕。
View:
屏幕上的一个矩形区域。
ViewGroup:
包含多个View对象。
Widget:
格式化条目。
Drawable:
能够装进其它UI的可视化条目。
Panel:
面板
Dialog:
对话框
Window:
窗口
Surface:
表面,能装载画布对象。
SurfaceView:
表面视图
Canvas:
画布
OpenGL ES:
实现3D图形的库。
第二节 Android建筑模块
如果认为Android应用是一个各种类型的组件集,这些组件大部分都是松耦合,你可以精确的描述这些组件的联合而不是简单的描述一个应用。
通常,这些组件都运行在一个相同的系统进程里面,当然可以在这个进程里面建立多个线程,也可以建立完全独立的子进程,这种情况并不通用,因为Android进程对用户透明。
这些是Android API最重要的部分:
AndroidManifest.xml:
它是应用的控制文件,告诉系统你的顶级组件(activities, services, intent receivers, content providers)要做些什么,例如,"glue"指定了你的应用所接收的Intent。
Activities:
它是具有生命周期的一个基本对象,实现一些主要的代码工作,如果需要,显示一个UI给用户。典型的,你会设计一个activity作为你应用的入口点。
Views:
一个view是一个对象,知道自己应该怎样显示在屏幕上,Android用户接口由view树组成,假如你想执行一些客户化的图形技术(比如建立游戏,或者一些不常用的UI widget),你就要建立一个view。
Intents:
表示一个简单的消息对象,能够处理一些感兴趣的事情。例如,如果你的应用想要显示一些web页,它通过intent传递URI,系统定位一些能够处理这个intent事情的代码,并且运行它。Intent也能用于广播一些系统感兴趣的事件。
Services:
它是运行在后台的一块代码,能运行在自己的进程里,也可以运行在其它应用的进程里。其它组件绑定到service,通过远程过程调用来执行它。例如,媒体播放器,当用户退出媒体内容选择UI,用户可能还要继续播放,这时候service就可以实现这样的功能。
Notifications:
它是显示在状态条的小图标,用户能够和这个图标交互来获得信息,最好的例子是SMS消息、呼叫历史和语音邮件。当然应用可以建立自己的notification。Notification是一个有力的机制,用来通知用户有些事情需要处理或注意。
ContentProviders:
它是数据仓库,提供数据给设备来访问,经典的例子是用它来访问联系人列表。你的应用能够通过contentProvider访问其它应用给出的数据,也可以定义自己的contentProvider给自己提供数据。
第一小节 AndroidManifest.xml
任何应用都需要AndroidManifest.xml文件,他放在应用的根目录下,描述一些全局值,包括哪些应用组件、每个组件的实现类、哪些类型数据能处理,他们能从哪里装载。
这个文件的一个重要方面是intent过滤,这些过滤器描述了activity能从哪里、什么时候开始。当activity想去执行一个行为,例如打开一个web页,或者打开一个联系人选择条目,它建立一个intent对象。这个对象能够保持几个描述器描述你所想做的,你想处理什么数据,数据的类型和其它信息位。Android为每个应用在intent对象和intent过滤器里比较,找到最适合处理这个数据的应用,或者实现调用者的行为。
除了宣布应用的activity, content provider, service和intent receiver,还可以在androidmanifest.xml里面指定一些权限和指令。
例子,简单的androidmanifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.my_domain.app.helloactivity">
<application android:label="@string/app_name">
<activity android:name=".HelloActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
一些常用条目:
每个androidManifest.xml文件包括名称空间宣布xmlns:android="http://schemas.android.com/apk/res/android,它处于第一个的位置。这个使得大量的标准android属性有效,它能提供大量的数据和条目。
大多数的manifest包括单个<application>条目,定义了包里的所有应用级别的组件和属性。
呈现给用户的任何包作为一个顶级应用是有效的,需要包括至少一个activity组件支持MAIN行为和LAUNCHER范围。
这是一些androidmanifest.xml的细节轮廓,有效的标记:
<manifest>
文件的根节点,在它下面可以放置:
<uses-permission>
请求安全性允许,为了使你的包能正确工作。一个manifest能包含0个或多个本条目。
<permission>
安全性允许,用来限制包里面的哪些应用能够访问哪些组件和特性。一个manifest能包含0个或多个本条目。
<instrumentation>
用来测试本包或其它包的功能性。一个manifest能包含0个或多个本条目。
<application>
应用级别的根条目,包括应用的全局的或缺省的属性。例如label, icon, theme, 需要的允许等等。一个manifest包括0个或1个本条目,在它下面可方0个或多个下面的条目:
<activity>
activity是用户和应用交互的基本工具,用户装载应用后首先看到的就是activity,更多其它的屏幕将被独立的activity来实现。
一个activity必须有一个<activity>标记。假如一个activity在manifest里面没有匹配的标记,你就不能装载它。
另外,可以包含一个或多个<intent-filter>标记。
<intent-filter>
用IntentFilter的格式定义组件所支持的intent集。
<action>
组件支持的Intent行为。
<category>
组件支持的Intent范围。
<data>
组件支持的Intent data MIME type, Intent data URI scheme, Intent data URI authority, Intent data URI path。
你可以让activity跟一些meta data通信:
<meta-data>
给activity增加新的meta data,客户端能够通过ComponentInfo.metaData恢复。
<receiver>
一个BroadcastReceiver允许应用得到数据或行为改变的通知,甚至当前它没有运行。和<activity>标记一起,你可以包含一个或多个receiver支持的<intent-filter>条目或<meta-data>值。
<service>
一个service是一个能运行在后台一定时间的组件,和<activity>标记一起,你可以包含一个或多个receiver支持的<intent-filter>条目或<meta-data>值。
<provider>
一个ContentProvider是一个管理稳定数据,并且发布数据给其它应用使用的组件。可以使用一个或多个<meta-data>条目。
第三节 保存、恢复和披露数据
一个典型的桌面操作系统提供一个通用的文件系统,任何应用能够存取其它应用的文件,可能需要一些存取控制。Android提供不同的系统:所有的应用数据都是私有的,当然也提供一个标准的方法披露它的数据给其它应用。这一章描述很多不同的方法供应用保存和恢复数据,提供数据给其它应用,怎么请求其它应用的数据。
Preferences:
一个轻量级的方法提供元数据类型的键、值对。典型的用来保存应用的参数。
Files:
可以保存文件在设备或可移动存取介质上,缺省情况下其它应用不能访问这些文件。
Databases:
支持SQLite,应用可以建立自己的私有SQLite数据库。
Content Providers:
内容提供者是可选的应用组件,它对于应用的私有数据披露其读写访问,当然可以加一些访问限制。内容提供者提供标准数据请求语法,和一个标准的访问机制。Androis实现了一些标准数据类型的内容提供者,比如个人的联系人信息。
Network:
也可以使用网络来保存和恢复数据。
第一小节 使用应用Preferences
保存诸如缺省问候语、文本字体之类的应用参数。调用Context.getSharedPreferences()来读写数据。假如想跟同一个包里的其它组件共享这些数据,需要给参数集一个名字,或者用Activity.getPreferences()不带名字保持对调用activity的私有。不能跨包共享参数集,下面是个例子:
public class Calc extends Activity {
public static final String PREFS_NAME = "MyPrefsFile";
...
@Override
protected void onCreate(Bundle state){
super.onCreate(state);
...
// Restore preferences
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
boolean silent = settings.getBoolean("silentMode", false);
setSilent(silent);
}
@Override
protected void onStop(){
super.onStop();
// Save user preferences. We need an Editor object to
// make changes. All objects are from android.context.Context
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("silentMode", mSilentMode);
// Don't forget to commit your edits!!!
editor.commit();
}
}
第二小节 使用Files
Android提供本地文件流的读写访问给应用。调用Context.openFileOutput()和Context.openFileInput()带本地名字和路径来读写文件。如果从其它应用用相同的名字、路径来调用则不会成功;只能访问本地文件。
假如你的应用在编译的时候有静态文件,可以保存你的文件在你的项目res/raw/<mydatafile>,接着用Resources.openRawResource(R.raw.mydatafile)来读取。
第三小节 使用SQLite Databases
建立数据库用SQLiteOpenHelper。Android装备了sqlite3数据库工具,能够浏览表内容,运行SQL命令并且执行其它有用的功能。
所有的数据库,SQLite,保存在/data/data/<package_name>/database。
Android并没有在标准SQLITE上做限制,不过建议每个表增加一个自动增加的关键值作为索引,对于私有数据并不是必须的。但是假如你实现一个content provider,则它是必须的。
第四小节 访问Content Providers
假如想发布数据,可以建立content provider,这个方法能够让所有应用存取某个应用的数据。这个是跨包之间共享数据的唯一方法。Android提供公用数据类型的一些content provider(声音、视频、图像、个人联系信息等等)。在provider包里面有一些Android的基本content provider。
所有的content provider必须实现通用的查询数据方法和返回结果方法。当然,content provider能实现客户化的helper功能,使数据的保存、恢复更简单。
用content provider保存和恢复数据
Android提供各种数据类型供content provider使用,从音乐图片到电话号码,从android.provider包content provider提供的数据。
Android的content provider是很松的连接到他们的客户端,每个content provider披露一个统一的URI标记它处理的数据类型,客户必须使用这个URI来保存和恢复这个类型的数据。
查询数据
每个content provider披露一个统一公共的URI,用来客户端查询、增加、更新、删除数据。URI有两种格式,一种指示这个类型的所有值,一种指示这个类型的一条特定的记录。
content://contacts/people: 返回所有联系人名字。
content://contacts/people/23: 返回单个结果,ID为23的联系人。
一个应用发送一个查询给设备,指定一个通用条目类型(如所有电话号码),或一个特定条目(小马的电话号码)。Android返回结果集的一个游标,下面一个假想的查询字符集和结果集:
query = content://contacts/people/
结果:
_ID | _COUNT | NUMBER | NUMBER_KEY | LABEL | NAME | TYPE |
---|---|---|---|---|---|---|
13 | 4 | (425) 555 6677 | 425 555 6677 | California office | Bully Pulpit | Work |
44 | 4 | (212) 555-1234 | 212 555 1234 | NY apartment | Alan Vain | Home |
45 | 4 | (212) 555-6657 | 212 555 6657 | Downtown office | Alan Vain | Work |
53 | 4 | 201.555.4433 | 201 555 4433 | Love Nest | Rex Cars | Home |
注意查询字符串不是标准的SQL查询串,而是一个URI描述了返回结果的类型,这个URI由三个部分组成:开始串content://,接着哪种数据类型要恢复,最后加一个可选的特定条目的ID。例子:
content://media/internal/images
content://media/external/images
content://contacts/people
content://contacts/people/23
虽然这是通用格式,这些查询有些任意和迷惑,因此,android在android.provider包里面提供了helper类列表,它定义了这些查询串,因此应该不需要知道不同数据类型的实际URI值。这些helper类定义了叫CONTENT_URI的串。
典型的,你用定义的CONTENT_URI对象来做查询,而不是自己写URI。因此上面每个查询能够用下面URI来参考:
MediaStore.Images.Media.INTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
Contacts.People.CONTENT_URI
去查询特定的记录,你用相同的CONTENT_URI,但是要增加特定的ID值,例子:
// Get the base URI for contact with _ID=23.
// This is same as Uri.parse("content://contacts/people/23");
Uri myPerson = ContentUris.withAppendedId(People.CONTENT_URI, 23);
// Query for this record.
Cursor cur = managedQuery(myPerson, null, null, null);
TIP:可以用withAppendedPath(Uri, String)增加串到URI。
查询返回数据记录的游标,本次返回什么记录,下次会有什么记录。
应当用Activity.managedQuery()方法恢复游标,游标可以准确处理自己的位置。可以调用Activity.startManagingCursor()方法管理一个未管理的游标。
下面是一个例子,用来恢复联系人列表和他们基本的电话号码:
// An array specifying which columns to return.
string[] projection = new string[] {
People._ID,
People.NAME,
People.NUMBER,
};
// Get the base URI for People table in Contacts content provider.
// ie. content://contacts/people/
Uri mContacts = People.CONTENT_URI;
// Best way to retrieve a query; returns a managed query.
Cursor managedCursor = managedQuery( mContacts,
projection, //Which columns to return.
null, // WHERE clause--we won't specify.
People.NAME + " ASC"); // Order-by clause.
查询返回什么
查询返回0个或多个数据库记录,列名字、顺序和类型给指定给content provider,每个查询都有一个名字为_id的列,假如查询返回二进制数据,例如位图或声音文件,将返回一个随机名字的列,它给出URI格式(content://URI)让你得到这些数据。看下图:
_id | name | number |
---|---|---|
44 | Alan Vain | 212 555 1234 |
13 | Bully Pulpit | 425 555 6677 |
53 | Rex Cars | 201 555 4433 |
这个结果显示一个子集被返回,可选的子集列表放在查询的projection参数里面,内容管理者应该列出支持哪些列,需要描述每一列(参考Contacts.People.Phone,他扩展了BaseColumns,PhonesColumns和PeopleColumns),或者列出常量列名字。为了访问数据,需要知道列数据类型。
通过游标恢复的数据能够在结果集里面前后查找,可以用游标读、修改、删除行,增加新行需要不同的对象。
注意,每个记录集包括一个列名为_id的列,和一个_count域,表示当前结果集的记录数,这些域名字由BaseColumns定义。
查询文件
返回结果的文件域典型的是文件路径,但是,调用者从来不应该直接打开或读取这些文件,相反,应该调用ContentProvider.openInputStream()/ContentResolver.openOutputStream(),或helper功能。
读恢复的数据
游标对象提供了访问结果记录集的方法,假如你查询一条特定记录,你将得到仅一个值,否则,可以包含多个值。可以从这些记录读取特定的数据,当然你必须知道这个数据的类型,游标让你从索引请求列名字,或从列名取得索引号。
假如你正在读2进制数据,例如图像文件,应该调用ContentResolver.openOutputStream()从列名content://URI。
下面片段显示了读取名字和电话号码:
private void getColumnData(Cursor cur){
if (cur.moveToFirst()) {
String name;
String phoneNumber;
int nameColumn = cur.getColumnIndex(People.NAME);
int phoneColumn = cur.getColumnIndex(People.NUMBER);
String imagePath;
do {
// Get the field values
name = cur.getString(nameColumn);
phoneNumber = cur.getString(phoneColumn);
// Do something with the values.
...
} while (cur.moveToNext());
}
}
修改数据
批量更新一组数据,用列和值调用ContentResolver.update()方法。
增加新记录
要增加新记录,调用ContentResolver.insert()带URI来增加,将返回新记录的URI,包括记录号,可以用来查询和得到新记录的游标:
ContentValues values = new ContentValues();可以调用带URI的ContentResolver().openOutputStream()来保存文件:
Uri phoneUri = null;
Uri emailUri = null;
values.put(Contacts.People.NAME, "New Contact");
//1 = the new contact is added to favorites
//0 = the new contact is not added to favorites
values.put(Contacts.People.STARRED,1);
//Add Phone Numbers
Uri uri = getContentResolver().insert(Contacts.People.CONTENT_URI, values);
//The best way to add Contacts data like Phone, email, IM is to
//get the CONTENT_URI of the contact just inserted from People's table,
//and use withAppendedPath to construct the new Uri to insert into.
phoneUri = Uri.withAppendedPath(uri, Contacts.People.Phones.CONTENT_DIRECTORY);
values.clear();
values.put(Contacts.Phones.TYPE, Phones.TYPE_MOBILE);
values.put(Contacts.Phones.NUMBER, "1233214567");
getContentResolver().insert(phoneUri, values);
//Add Email
emailUri = Uri.withAppendedPath(uri, ContactMethods.CONTENT_DIRECTORY);
values.clear();
//ContactMethods.KIND is used to distinguish different kinds of
//contact data like email, im, etc.
values.put(ContactMethods.KIND, Contacts.KIND_EMAIL);
values.put(ContactMethods.DATA, "test@example.com");
values.put(ContactMethods.TYPE, ContactMethods.TYPE_HOME);
getContentResolver().insert(emailUri, values);
// Save the name and description in a map. Key is the content provider's删除记录
// column name, value is the value to save in that record field.
ContentValues values = new ContentValues(3);
values.put(MediaStore.Images.Media.DISPLAY_NAME, "road_trip_1");
values.put(MediaStore.Images.Media.DESCRIPTION, "Day 1, trip to Los Angeles");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
// Add a new record without the bitmap, but with the values.
// It returns the URI of the new record.
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
try {
// Now get a handle to the file for that record, and save the data into it.
// sourceBitmap is a Bitmap object representing the file to save to the database.
OutputStream outStream = getContentResolver().openOutputStream(uri);
sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream);
outStream.close();
} catch (Exception e) {
Log.e(TAG, "exception while writing image", e);
}
用ContentResolver.delete()带URI来删除记录。
要删除多行,调用ContentResolver.delete()带记录类型的URI(例如,android.provider.Contacts.People.CONTENT_URI)和SQL WHERE字句。
建立Content Provider
- 扩展ContentProvider。
- 定义名叫CONTENT_URI的public static final URI,这是带有content://格式字符串。必须定义一个统一的字符串,最好的方法是用完全合格的小写的类名字格式,例如:public static final Uri CONTENT_URI=Uri.parse("content://com.google.codelab.rssprovider");
- 建立系统保存数据,最通用的方法使用文件或SQLite数据库,当然可以用任何其它方式保存数据。
- 定义需要返回给客户端的列名,假如用基本的数据库,这些列名就是SQL数据库的列名。任何情况下,你需要定义一个整数列名_id来定义一列。如果使用SQLite数据库,这个类型应该是INTEGER PRIMARY KEY AUTOINCREMENT。AUTOINCREMENT是可选的,缺省情况下,SQLite会自动增加。假如删除最后一行,再增加一行,则增加的这行跟删除的一行具有相同的_id。为了避免这种情况,你需要定义成INTEGER PRIMARY KEY AUTOINCREMENT。Android提供SQLiteOpenHelper类帮助建立和管理你的数据库版本。
- 假如披露字节型数据,例如位图,他实际是个类似content://字符串,这是客户端用来恢复数据用的。content provider为这种类型应该实现列名为_data的记录。_data域列出设备上的文件路径,这个域不能给客户端直接读,但可以让ContentResolver读。客户调用ContentResolver.openOutputStream()读取这个内容。ContentResolver请求记录的_data域,因为它比客户端有更高的权限,应该可以直接访问这个文件,并且返回给客户端。
- 声明public static Strings供客户返回,或者从游标返回值,
- 在应答查询时返回记录集的游标对象,意味着实现了query(),update(), insert()和delete()方法。一般情况下,也许调用ContentResolver.notifyChange()来通知监听器来更新信息。
- 在AndroidManifest.xml里面增加一个<provider>标记,用它的authorities属性来定义内容类型的鉴权部分。例如,假如类型类型是content://com.example.autos/auto请求所有自动化的列表,那么authorities应当是com.example.autos。假如数据不需要在多个版本之间同步,需要设置multiprocess属性为true。
- 假如你正在处理新的数据类型,必须定义新的MIME类型android.ContentProvider.getType(url)。MIME有两种格式:一是为特定的记录,一是为多记录。下面是通用格式:
- vnd.android.cursor.item/vnd.yourcompanyname.contenttype
为单记录,例如,为列车122记录:content://com.example.transportationprovider/trains/122
也许返回MIME类型
vnd.android.cursor.item/vnd.example.rail - vnd.android.cursor.dir/vnd.yourcompanyname.contenttype
为多记录,例如,为所有列车记录:content://com.example.transportationprovider/trains
也许返回MIME类型
vnd.android.cursor.dir/vnd.example.rail
一个私有的content provider实现例子,看notepad例子的NodePadProvider类。
下面是各个URI部分的解释:
A. 标准的前缀,不能修改。
B. 鉴权部分,为第三方应用,包括一个类,相应的值在<provider>属性里面:
<provider class="TransportationProvider" authorities="com.example.transportationProvider" />
C. 决定哪种类型的数据被请求,可以0个或多个分段:假如content provider仅仅披露一种类型数据,这项可以不需要写;假如提供几种类型,包括子类型,可以是几个条目:"land/bus,land/train,sea/ship,sea/submarine。
D. 假如一个特定记录被请求,就是一个特定_id记录值被请求,假如特定类型的所有记录被请求,则可以省略。例如:content://com.example.transportationProvider/trans。
第五小节 网络访问
除了上面这些在设备上的存取方式,也可以从网络保存和恢复数据,如果要实现网络操作,必须用下面的包:
java.net.*
android.net.*
没有评论:
发表评论