Android学习笔记

来源:互联网 发布:凤凰炒股大赛软件 编辑:程序博客网 时间:2024/05/20 21:19

Android学习笔记

Android platform是一个用于开发移动程序的软件包,它包括了操作系统、中间件及一些关键应用。开发者能使用android SDK为Androidplatform开发应用,这些应用使用JAVA语言书写,运行在虚拟机Dalvik(一个专为手机程序开发的基于linux内核的JAVA虚拟机)。

1 什么是Android

1.1 Android的特性

ü         应用框架,让一些基础设施得以重用

ü         Dalvik虚拟机,专为开发移动程序优化

ü         集成的浏览器,(基于WebKit引擎)

ü         优化的图形库,(2D图形库以及基于OpenGL ES 1.0规范的3D图形库)

ü         SQLite,用于结构化数据的存储,是一个数据库

ü         多媒体支持,支持多种音频,视频格式(MPEG4, H.264, MP3, AAC, AMR, JPG, PNG, GIF)

ü         GSM技术(依赖具体硬件)

ü         Bluetooth, EDGE, 3G, WiFi(依赖具体硬件)

ü         Camera, GPS, compass,accelerometer(依赖具体硬件)

ü         丰富的开发环境(DEBUG工具,内存及性能工具,Eclipse的插件等)

1.2 Android的架构

   Android操作系统的模块如下:

Ø        应用:Android已集成了一些应用,如邮件客户端,SMS程序,日历,地图,浏览器等

Ø        应用框架:

Ø        程序库:

Ø        例行程序

Ø        Linux内核

2 Andvoid起步

2.1 开发环境配置

   使用Eclipse + AndroidDevelopment Tools (ADT)插件。在Help > Software Updates > Find and Install....中键入更新地址:

https://dl-ssl.google.com/android/eclipse/

2.2 运行第一个Andvoid程序

使用Eclipse插件

   1、建立的工程类型为:AndvoidProject

   2、建立LaunchConfiguration. Run > Open Run Dialog... or Run > Open Debug Dialog

命令行运行程序

   1、创建工程 activityCreator your.package.name.ActivityName

  2、编译。在build.xml所在的目录ant一下。

  3、启动模拟器. 运行命令:emulator

  4、在模拟器中,切换到主屏幕。

  5、在命令行输入: adbinstall myproject/bin/<appname>.apk将其上载至模拟器。

  6、在模拟器中选择程序并启动。

 

Android需要专门的编译工具来正确的编译资源文件和Android程序的其他部分。基于此,你需要为你的程序准备一个专门的编译环境。

Andorid的编译过程通常包括编译XML和其他资源文件、创建正确的输入格式。经过编译的Android程序是一个.apk文件,.apk文件是一个压缩文件,它其中包含了.dex文件、资源文件、rawdata文件和其他文件。

Andoriod暂时还不支持用本地代码(C/C++)开发第三方程序。

 

移除Andorid程序

要移除你安装在模拟器上的程序,你需要通过run adb并删除相应的.apk文件。通过adb shell命令在模拟器上打开一个UNIX shell,进入目录data/app/,通过命令rm 你程序的名称.apk来移除文件。

2.3 调试程序

  Andvoid用于调试的手段有:

 DDMS,DDMS是一个图形化的程序,支持端口转发(因此你可以在程序中设置断点),支持模拟器上的截屏,支持线程和堆栈信息和其他的一些特性。

  Logcat,Dump一份系统消息的日志。这些消息包括模拟器抛出错误时的堆栈跟踪。

 Android Log, 打印日志的类,用来将消息写入模拟器上的日志文件中。如Log.v()用于打印verbose级别的日志

 Traceview,Android可以保存一个日志用来记录被调用的方法以及该方法被调用的次数,通过Traceview你可以在一个图形化的界面中查看这个日志文件。

  可接解设置emulator的设置以方便调试,

 

模拟器上调试和测试的设置

   Android提供了众多的设置使你可以更容易的调试和测试程序。要进入开发设置页面,在模拟器中转到Dev Tools > Development Settings。在该设置页面有以下选项:

·        Debugapp:选择要调试的程序。你不需要设定其关联至调试器,但是设定这个值有两个效果:

o       在调试的时候,如果你在一个断点处暂停了过长的时间,这个设定会防止Android抛出一个错误

o       这个设定使你可以选择“等待调试器”选项,使程序只有在调试器关联上之后才启动

·        Waitfor Debugger:阻塞所选的程序的加载直到有调试器关联上,这样你就可以在onCreate()中设置断点,这对于调试一个Activity的启动进程是非常重要的。当你对该选项进行了更改,任何正在运行的程序的实例都会被终止。你只有在上面的选项中选择了一个调试程序才能够选中该选项。你一也可以在代码中添加waitForDebugger()来实现同样的功能。

·        Immediatelydestroy activities:告诉系统一旦一个activity停止了就销毁该activity(例如当Android释放内存的时候)。这对于测试代码onFreeze(Bundle)/onCreate(android.os.Bundle)是非常有用的,否则会比较困难。如果你的程序没有保存状态,那么选择这个选项很可能会引发很多问题。

·        Showscreen updates:对于任何正在被重绘的screen sections都会在其上闪现一个粉红色的矩形。这对于发现不必要的screen绘制是很有必要的。

·        ShowCPU usage:在屏幕上方显示CPU信息,显示有多少CPU资源正在被使用。上方红色条显示总的CPU使用率,它下方绿色的条显示CPU用在compositing the screen上的时间。注意:在没有重启模拟器之前,一旦你开启了该功能就不能关闭。

·        Showscreen FPS:显示当前的帧率。这对于查看游戏达到的总的帧率是非常有用的。注意:在没有重启模拟器之前,一旦你开启了该功能就不能关闭。

·        Showbackground:当没有activity screens可见时,显示一个背景模式。一般是不会出现的,仅仅在Debug的时候会出现。

设定的选项在模拟器重启之后仍然有效,如果要取消设定的选项,在取消设定以后还要重启模拟器,才能生效。

2.4 andvoid中的概念

   一个andvoid应用包括四个部分:

n         Activity活动      (个人认为 :类似于JSP, 也相当于SWT中的Shell, View则相当于wegiet)

n         IntentReceiver (个人认为 :类似于Struts action)

n         Service       (个人认为 :类似于Servlet)

n         ContentProvider (个人认为 :用于持久化)

用上面哪些组件,要在AndroidManifest.xml文件中声明。

1、  Activity. 一个activity是应用中的一个单一的屏幕,它继承自Activity类,它将显示由Views组成的UI以及响应事件。(个人理解,相当于JSP)

2、  Intent与Intent Filters.Intent用于从一个屏幕跳到别一个屏幕,描述一个应用想做什么,它的数据结构包括action与data两部分,action如MAIN、VIEW、PICK、EDIT等等, data被表达成一个URI;IntentFilter相当于告诉跳到哪个activity,;IntentReceiver用于响应事件,虽然它不显示UI,但它可以用NotificationManager去通知用户。它需要在AndroidManifest.xml文件中注册,或者是用Context.registerReceiver()硬编码。

3、  Service是一段有生命周期的无UI的代码。

4、  Content Provider, 持久化,例如存储在文件系统中,或者存储在SQLite数据库中。

2.5 例子

   1,下载例子工程。http://code.google.com/android/intro/codelab/NotepadCodeLab.zip

     例如:Notepadv1工程是问题工程,Notepadv1Solution则是相应的解决工程。

   2,3个例子的函义如下:

通过练习1>
1.        数据库对象的例子程序
2.        如何在一个Activity初始化的时候创建数据库对象
3.        如何为一个Activity创建一个pop菜单
4.        如何得到一个用户在POP菜单中选择的ITEM 的ID
5.        如何向一个ListView中写入数据

通过练习2>
1.        如何取得数据集中被选择数据行的ID, 注意rows是java的一个listarray对象,所以它有它的getSelection()的方法
2.        如何调用一个SubActivity以及在调用一个SubActivity之前应该做些事情。
3.        要实现一个方法, 当SubActivity返回后,应该做些什么
4.        关于layout。由于Android采用MVC的模式, 所以屏幕的布局采用了XML进行定义。一个好的布局会让程序显的更漂亮。可以参考http://code.google.com/android/reference/view-gallery.html,有很多的布局模式可供利用。
5.        如何创建一个开始的时候代码中不含onCreate()的类,因为到目前为止还没有创建过自己的类,以前看到的类和修改的方法都是已经写好的了。不过这个类也是一个从android.app.Activity继承的。创建后还是要通过Source Override一个onCreate的
6.        从一个SubActivity返回时,在返回之前应该做哪些事情
7.        如何在AndroidManifest.xml中声明一个新的Activity
8.        如何创建一个内部隐含的onClickLister及实现其应有的功能。

通过练习3>
1. 除了onCreate()还有哪些和lifecycle有关的方法
a.onFreeze():
b.onPause()
c.onResume()
等等…
2. 大部分程序都有必要考虑lifecycle的问题,应该把下面的这个图记住:
   3. 为了不丢失需要保存的信息,要明确的理解Activity的各个状态,并相应的给出在各个状态的行为。
4.开始的时候对于onFreeze()和onPause()有些理解上的困难,要多读几遍DOC来会理解。通过额外练习>

进一步的理解lifecycle和通过手机按键进行操作时的时间和程序状态关系

另外,SDK目录下也有一些例子。

练习目标:
1. 使用ListActivities,并使用菜单
2. 学习使用操作SQLite数据库
3. 使用ArrayAdapter绑定数据到ListView中
4. 掌握一些基本的操作,如菜单的显示,菜单命令的处理,增加数据项等。

第一步
在SDK中下载获得Notepadv1的代码,并导入到Eclipse中。导入步骤:
 a. 在Package Explorer中,右键选择Import.../General/Existing Projects into Workspace
 b. 点Browse按钮,选择Notepadv1的目录,并点OK
 c. 你将会看到Notepadv1被列在项目区中,默认会被打勾,如果没有打勾,请手动勾上。
 d. 点Finish
 e. Notepadv1将被列在Package Explorer中
 f. 如果有提示关于AndroidManifest.xml的错误,请选中此项目,并右键选择Android  Tools->Fix Project,他将会自动帮你修复错误。

第二步
看一下数据库操作类:DBHelper,还是比较简单的,自己看去 :)。

第三步
打开res/layout/notepad_list.xml这个文件,快速的看下就可以了:
 a.<?xml version="1.0" encoding="utf-8"?>,XML文件的固定头

b.一个Layout的定义,这里是 LinearLayout,但不一定是这个,可以是其他的Layout

第四步
在上面的那个文件中加入:
  <ListView id="@id/android:list"
       android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
  <TextView id="@id/android:empty"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/no_notes"/>

 a.ListView和TextView不会同是显示,如果没有数据,则默认显示TextView(这个View里会显示一个字符串)。如果有数据,则会显示ListView。
b.@是默认的关键字,XML解析器将会自动替换这个符号后面的ID
c.android:list 和android:empty 是android平台预定义好的ID,如果你想显示空的TextView,可以调用setEmptyView().

第五步:
建立一个新文件res/layout/notes_row.xml,文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<TextView id="@+id/text1"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

然后保存,R.java下将会自动刷新。

第六步
改变继承类
public class Notepadv1 extends ListActivity

第七步:
看一下这三个事件:
onCreate():界面初始化的时候调用
onCreateOptionsMenu():按了Menu按钮的时候调用
onOptionsItemSelected() :选择了一个菜单项的时候调用

第八步:
改写OnCreate函数:
 
    private DBHelper dbHelper;

    @Override
    public void onCreate(Bundle icicle)
    {
        super.onCreate(icicle);
       setContentView(R.layout.notepad_list);
        dbHelper = new DBHelper(this);
        fillData();
    }


第九步:
在strings.xml中增加:<stringname="menu_insert">Add Item</string>

并在Notepadv1类中加入:publicstatic final int INSERT_ID = Menu.FIRST;

改写onCreateOptionsMenu()
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        boolean result =super.onCreateOptionsMenu(menu);
        menu.add(0, INSERT_ID,R.string.menu_insert);
        return result;
    }


第十步:
改写onOptionsItemSelected()
@Override
    public boolean onOptionsItemSelected(Item item) {
        switch (item.getId()) {
        case INSERT_ID:
           createNote();
            break;
        }
      
        returnsuper.onOptionsItemSelected(item);
    }

第十一步:
实现两个函数:
private void createNote() {
        String noteName = "Note "+ noteNumber++;
        dbHelper.createRow(noteName,"");
        fillData();
    }

private void fillData() {
        // We need a list of strings for thelist items
        List<String> items = newArrayList<String>();

        // Get all of the rows fromthe database and create the item list
        List<Row> rows =dbHelper.fetchAllRows();
        for (Row row : rows) {
           items.add(row.title);
        }
       
        // Now create an array adapter andset it to display using our row
        ArrayAdapter<String> notes =
            newArrayAdapter<String>(this, R.layout.notes_row, items); //指定notes_row视图,作为数据容器。
        setListAdapter(notes);
       
    }

第十二步:
运行:RunAs -> Android Application

2.6 开发工具

2.6.1 仿真器

   仿真器是在计算机中运行的一个虚拟的移动装置,用它来设计、调试应用。

2.6.2 ADT

ADT是一个用于开发andvoid应用的确良eclipse插件,它方便了我们的开发。例如,它让我们方便从eclipse内部访问DDMS工具(可用于截屏,管理port-forwarding,设断点,查看线程和进程信息)

2.6.3 DDMS

DDMS(Dalvik Debug Monitor Service)和Dalvik虚拟机集成,将在IDE和模拟器起到一个转发服务。用它能管理仿填器或者设备的进程,并且辅助调试。你可用它杀死进程,选择某一进程去DEBUG,产生TRACE数据,查看堆和线程信息等等。

2.6.4 ADB

ADB(Andvoid Debug Bridge)。在命令行操作。它能安装.apk文件到仿真器上等等。用于将文件发送到仿真器。adb(Android Debug Bridge)是Android提供的一个通用的调试工具,借助这个工具,我们可以管理设备或手机模拟器的状态。还可以进行以下的操作:

1、快速更新设备或手机模拟器中的代码,如应用或Android系统升级;

2、在设备上运行shell命令;

3、管理设备或手机模拟器上的预定端口;

4、在设备或手机模拟器上复制或粘贴文件;

以下为一些常用的操作:

1、安装应用到模拟器:

adb install

比较郁闷的是,Android并没有提供一个卸载应用的命令,只能自己手动删除:

adb shell

cd /data/app

rm app.apk

2、进入设备或模拟器的shell:

adb shell

通过上面的命令,就可以进入设备或模拟器的shell环境中,在这个Linux Shell中,你可以执行各种Linux的命令,另外如果只想执行一条shell命令,可以采用以下的方式:

adb shell [command]

如:adb shell dmesg会打印出内核的调试信息。

3、发布端口:

你可以设置任意的端口号,做为主机向模拟器或设备的请求端口。如:

adb forward tcp:5555 tcp:8000

4、复制文件:

你可向一个设备或从一个设备中复制文件,

复制一个文件或目录到设备或模拟器上:

adb push

如:adb push test.txt /tmp/test.txt

从设备或模拟器上复制一个文件或目录:

adb pull

如:adb pull /addroid/lib/libwebcore.so.

5、搜索模拟器/设备的实例:

取得当前运行的模拟器/设备的实例的列表及每个实例的状态:

adb devices

6、查看bug报告:

adb bugreport

7、记录无线通讯日志:

一般来说,无线通讯的日志非常多,在运行时没必要去记录,但我们还是可以通过命令,设置记录:

adb shell

logcat -b radio

8、获取设备的ID和序列号:

adb get-product

adb get-serialno

9、访问数据库SQLite3

adb shell

sqlite3

2.6.5 aapt

   Aapt(Andvoid Asset Packaging Tool),用于创建.apk文件。

2.6.6 aidl

Aidl(Andvoid Interface Description Language)用于产生代码。

2.6.7 sqlite3

用于访问SQLite数据文件。

2.6.8 Traceview

   查看LOG信息

2.6.9 mksdcard

   Helps you create a disk image that youcan use with the emulator, to simulate the presence of an external storage card(such as an SD card

2.6.10 dx

  Dx工具用于重写.class字节码到Andvoid字节码

2.6.11 activityCreator

   用于产生ant文件的脚本。当然,若用eclipse插件就不需要它了。

2.7 Andvoid应用的生命周期

在大部份情况下,每个Android应用都将运行在自己的Linux进程当中。当这个应用的某些代码需要执行时,进程就会被创建,并且将保持运行,直到该进程不再需要,而系统需要释放它所占用的内存,为其他应用所用时,才停止。

     Android一个重要并且特殊的特性就是,一个应用的进程的生命周期不是由应用自身直接控制的,而是由系统,根据运行中的应用的一些特征来决定的,包括:这些应用对用户的重要性、系统的全部可用内存。

      对于应用开发者来说,理解不同的应用组件(特别是Activity、Service、IntentReceiver)对应用进程的生命周期的影响,这是非常重要的。如果没有正确地使用这些组件,将会导致当应用正在处理重要的工作时,进程却被系统消毁的后果。

    对于进程生命周期,一个普遍的错误就是:当一个Intent Receiver在它的onReceiveIntent()方法中,接收到一个intent后,就会从这个方法中返回。而一旦从这个方法返回后,系统将会认为这个Intent Receiver不再处于活动状态了,也就会认为它的宿主进程不需要了(除非宿主进程中还存在其它的应用组件)。从而,系统随时都会消毁这个进程,收回内存,并中止其中还在运行的子线程。问题的解决办法就是,在IntentReceiver中,启动一个Service,这样系统就会知道在这个进程中,还有活动的工作正在执行。

    为了决定在内存不足情况下消毁哪个进程,Android会根据这些进程内运行的组件及这些组件的状态,把这些进程划分出一个“重要性层次”。这个层次按顺序如下:

    1、前端进程是拥有一个显示在屏幕最前端并与使用者做交互的Activity(它的onResume已被调用)的进程,也可能是一个拥有正在运行的IntentReceiver(它的onReceiveIntent()方法正在运行)的进程。在系统中,这种进程是很少的,只有当内存低到不足于支持这些进程的继续运行,才会将这些进程消毁。通常这时候,设备已经达到了需要进行内存整理的状态,为了保障用户界面不停止响应,只能消毁这些进程;

   2、可视进程是拥有一个用户在屏幕上可见的,但并没有在前端显示的Activity(它的onPause已被调用)的进程。例如:一个以对话框显示的前端activity在屏幕上显示,而它后面的上一级activity仍然是可见的。这样的进程是非常重要的,一般不会被消毁,除非为了保障所有的前端进程正常运行,才会被消毁。

    3、服务进程是拥有一个由startService()方法启动的Service的进程。尽管这些进程对于使用者是不可见的,但他们做的通常是使用者所关注的事情(如后台MP3播放器或后台上传下载数据的网络服务)。因此,除非为了保障前端进程和可视进程的正常运行,系统才会消毁这种进程。

   4、后台进程是拥有一个用户不可见的Activity(onStop()方法已经被调用)的进程。这些进程不直接影响用户的体验。如果这些进程正确地完成了自己的生命周期(详细参考Activity类),系统会为了以上三种类型进程,而随时消毁这种进程以释放内存。通常会有很多这样的进程在运行着,因些这些进程会被保存在一个LRU列表中,以保证在内存不足时,用户最后看到的进程将在最后才被消毁。

   5、空进程是那些不拥有任何活动的应用组件的进程。保留这些进程的唯一理由是,做为一个缓存,在它所属的应用的组件下一次需要时,缩短启动的时间。同样的,为了在这些缓存的空进程和底层的核心缓存之间平衡系统资源,系统会经常消毁这些空进程。

    当要对一个进程进行分类时,系统会选择在这个进程中所有活动的组件中重要等级最高的那个做为依据。可以参考Activity、Service、IntentReceiver文档,了解这些组件如何影响进程整个生命周期的更多细节。这些类的文档都对他们如何影响他们所属的应用的整个生命周期,做了详细的描述。

 

2 开发应用

2.1 前端UI

2.1.1 .屏幕元素的层次

1. Views

    一个View是android.view.View基础类的一个对象,它是一个有屏幕上特定的一个矩形内布局和内容属性的数据结构。一个View对象处理测量和布局,绘图,焦点变换,滚动条,还有屏幕区域自己表现的按键和手势。

   View类作为一个基类为widget(窗体部件)服务,widget--是一组用于绘制交互屏幕元素的完全实现子类。Widget处理它们自己的测距和绘图,所以你可以更快速地用它们去构建你的UI。可用到的widget包括Text,EditText,InputMethod,Button,RadioButton,Checkbox,和ScrollView。

2. Viewgroups

一个ViewGroup是一个android.view.Viewgroup类的对象。一个viewgroup是一个特殊的view对象,它的功能是去装载和管理一组下层的view和其他viewgroup,Viewgroup让你可以为你的UI增加结构并且将复杂的屏幕元素构建成一个独立的实体。

   Viewgroup类作为一个基类为layout(布局)服务,layout--是一组提供屏幕界面通用类型的完全实现子类。layout让你可以为一组view构建一个结构。

3. A Tree-Structured UI

     在Android平台上,你用view树和viewgroup节点来定义一个Activity的UI,就如同下面图表一样。这个树可以如你需要那样简单或者复杂,并且你可以使用Android的预定义widget和layout或者你自定义的view类型来构建它。

    要将屏幕绑定一个树以便于渲染,你的Activity调用它的setContentView()方法并且传递一个参数给根节点对象。一旦Android系统获得了根节点的参数,它就可以直接通过节点来无效化,测距和绘制树。当你的Activity被激活并且获得焦点时,系统会通知你的activity并且请求根节点去测距并绘制树,根节点就会请求它的子节点去绘制它们自己。每个树上的viewgroup节点都为它的子节点的绘制负责。

    正如之前提到的,每个view group都有测量它的有效空间,布局它的子对象,并且调用每个子对象的Draw()方法去绘制它们自己。子对象可能会请求获得一个它们在父对象中的大小和位置,但是父对象对于每个子对象的大小和位置有最终的决定权。

4. LayoutParams:一个子对象如何指定它的位置和大小

     每个viewgroup类都会使用一个继承于Viewgroup.LayoutParams的嵌套类。这个子类包含了包含了定义一个子对象位置和大小的属性类型,并且需适用于view group类。

     要注意的是,每个LayoutParams子类都有它自己赋值的语法。每个子元素必须定义适用于它们父对象的LayoutParams,尽管父对象可能会为子元素定义不同的LayoutParams。

     所有的viewgroup都包括宽和高。很多还包括边界的定义(margin和border)。你可以非常精确地描述宽和高,尽管你并不想经常这么做。更多时候你希望你的view自行调整到适应内容大小,或者适应容器大小。

2.1.2.通用布局对象(最普遍的view groups)

1. FrameLayout (上下压着的那种)

     FrameLayout是最简单的一个布局对象。它被定制为你屏幕上的一个空白备用区域,之后你可以在其中填充一个单一对象 — 比如,一张你要发布的图片。所有的子元素将会固定在屏幕的左上角;你不能为FrameLayout中的一个子元素指定一个位置。后一个子元素将会直接在前一个子元素之上进行覆盖填充,把它们部份或全部挡住(除非后一个子元素是透明的)。

2. LinearLayout

     LinearLayout以你为它设置的垂直或水平的属性值,来排列所有的子元素。所有的子元素都被堆放在其它元素之后,因此一个垂直列表的每一行只会有一个元素,而不管他们有多宽,而一个水平列表将会只有一个行高(高度为最高子元素的高度加上边框高度)。LinearLayout保持子元素之间的间隔以及互相对齐(相对一个元素的右对齐、中间对齐或者左对齐)。

     LinearLayout还支持为单独的子元素指定weight。好处就是允许子元素可以填充屏幕上的剩余空间。这也避免了在一个大屏幕中,一串小对象挤成一堆的情况,而是允许他们放大填充空白。子元素指定一个weight值,剩余的空间就会按这些子元素指定的weight比例分配给这些子元素。默认的weight值为0。例如,如果有三个文本框,其中两个指定了weight值为1,那么,这两个文本框将等比例地放大,并填满剩余的空间,而第三个文本框不会放大。

     Tip:为了在屏幕上创建一个按比例安排大小的layout,需要根据这个屏幕上每个元素将按什么比例显示,创建一个指定fill_parent,子元素的height或width为0,且为每一个子元素分配weight值的容器对象。

      下面的两个窗体采用LinearLayout,包含一组的元素:一个按钮,几个标签,几个文本框。两个窗体都为布局做了一番修饰。文本框的width被设置为FILL_PARENT;其它元素的width被设置为WRAP_CONTENT。默认的对齐方式为左对齐。左边的窗体没有设置weight(默认为0);右边的窗体的comments文本框weight被设置为1。如果Name文本框也被设置为1,那么Name和Comments这两个文本框将会有同样的高度。

      在一个水平排列的LinearLayout中,各项按他们的文本基线进行排列(第一列第一行的元素,即最上或最左,被设定为参考基线)。因此,人们在一个窗体中检索元素时,就不需要七上八下地读元素的文本了。我们可以在layout的XML中设置android:baselineAligned="false",来关闭这个设置。

3. TableLayout

     TableLayout将子元素的位置分配到行或列中。一个TableLayout由许多的TableRow组成,每个TableRow都会定义一个row(事实上,你可以定义其它的子对象,这在下面会解释到)。TableLayout容器不会显示row、cloumns或cell的边框线。每个row拥有0个或多个的cell;每个cell拥有一个View对象。表格由列和行组成许多的单元格。表格允许单元格为空。单元格不能跨列,这与HTML中的不一样。下图显示了一个TableLayout,图中的虚线代表不可视的单元格边框。

      列可以被隐藏,也可以被设置为伸展的从而填充可利用的屏幕空间,也可以被设置为强制列收缩直到表格匹配屏幕大小。对于更详细信息,可以查看这个类的参考文档。

4. AbsoluteLayout

      AbsoluteLayout可以让子元素指定准确的x/y坐标值,并显示在屏幕上。(0,0)为左上角,当向下或向右移动时,坐标值将变大。AbsoluteLayout没有页边框,允许元素之间互相重叠(尽管不推荐)。我们通常不推荐使用AbsoluteLayout,除非你有正当理由要使用它,因为它使界面代码太过刚性,以至于在不同的设备上可能不能很好地工作。

5. RelativeLayout

     RelativeLayout允许子元素指定他们相对于其它元素或父元素的位置(通过ID指定)。因此,你可以以右对齐,或上下,或置于屏幕中央的形式来排列两个元素。元素按顺序排列,因此如果第一个元素在屏幕的中央,那么相对于这个元素的其它元素将以屏幕中央的相对位置来排列。如果使用XML来指定这个layout,在你定义它之前,被关联的元素必须定义。

     这是一个RelativeLayout例子,其中有可视的和不可视的元素。基础的屏幕layout对象是一个RelativeLayout对象。

      这个视图显示了屏幕元素的类名称,下面是每个元素的属性列表。这些属性一部份是由元素直接提供,另一部份是由容器的LayoutParams成员(RelativeLayout的子类)提供。RelativeLayout参数有width,height,below,alignTop,toLeft,padding和marginLeft。注意,这些参数中的一部份,其值是相对于其它子元素而言的,所以才RelativeLayout。这些参数包括toLeft,alignTop和below,用来指定相对于其它元素的左,上和下的位置。

6. Summary of Important View Groups

     重要View Group摘要,这些对象拥有UI子元素。一些提供可视的UI,另一些只处理子元素的布局。

2.1.3 数据绑定

     这部分会提及UI有的一些View groups,些组成对象是经典AdapterView类的子类.例如包括图像,数层结构表现.这些对象有2个通用的任务: 数据层的填充与用户操作选择

1. 数据层填充

   This is typically done by binding the class to an Adapter that gets its datafrom somewhere — either a list that the code supplies, or query results fromthe device's database.

// Get a Spinnerand bind it to an ArrayAdapter that

// references aString array.

private String[]fruit = {"apples", "oranges", "lemons"}

Spinner s1 =(Spinner)findViewById(R.id.fruitlist);

s1.setAdapter(newArrayAdapter<String>(this, mStrings));

 

// Load a Spinnerand bind it to a data query.

private String[]cols={android.provider.Contacts.PeopleColumns.NAME};

private Cursor cur= managedQuery(android.provider.Contacts.People.CONTENT_URL, cols, null, null);

s2.setAdapter(new CursorAdapter(cur,this));

2. 用户操作选择

    设置类的AdapterView.OnItemClickListener方法监听和捕捉用户的操作事件.

// Create a messagehandling object as an anonymous class.
private OnItemClickListener mMessageClickedHandler = new OnItemClickListener(){
     public void onItemClick(AdapterView parent, View v,int position, long id)
     {
         // Display a messagebox.
         showAlert("You've got anevent", "Clicked me!", "ok", false);
     }
};

// Now hook into our object and set its onItemClickListener member
// to our class handler object.
mHistoryView = (ListView)findViewById(R.id.accept_button);
mHistoryView.setOnItemClickListener(mMessageClickedHandler);

2.1.4使用XML设计你的屏幕显示

1.  Android定义了大量的自定义元素,各自代表了特定的Android显示子类。

2. 你可以象创建HTML文档一样,通过保存在应用res/layout/目录下的XML文件中一系列的嵌套标签来设计你的屏幕显示。

3. 每个文档描述一个android.view.View这个元素既可以是

一个简单的显示元素,也可以是一个在子节点中包含了一个集合的版面设计的元素,当Android编译你的应用时,他将每个文件都编译进android系统。你可以在代码Activity.onCreate()实现中通过调用setContentView(R.layout.layout_file_name)方法加载显示资源。

2.1.5 在屏幕元素中设置句柄

1.您可以使用Activity.findViewById来取得屏幕上的元素的句柄. 使用该句柄您可以设置或获取任何该对象外露的值.

TextViewmsgTextView = (TextView)findViewById(R.id.msg);
   msgTextView.setText(R.string.push_me);

2.2 构建组成模块

   Android应用是由各种各样的组件来构成.这些组件大部分都是松散联接,你可以精确的描述它们的联接程度,所以组建的联合比单个程序更有结合力.

显然,所有的组件运行在同一个系统进程里面.在这个进程里面创建多线程是可以允许的,并且是常见的.如果你需要,也可以对刚才那个系统进程创建相互独立的子进程.即使会有很多实例运行,但是他们之间互不干扰,这个是很难得的,因为Android可以确保代码是进程间透明的.

以下部分是很重要的Android APIs;

AndroidManifest.xml 是控制文件,告诉所有由高级组件构成的系统可以做什么.这些高级的组件包括(特殊activities,服务,接收器,第三方提供商).控制文件用来告诉系统如何使用你所创建的组件.

Activity 是一个有生命周期的对象. 一个Activity做一些工作需要相当大的数量的代码;如必要的话,这部分工作还可能包括对用户UI界面的显示,也可能是没有UI界面.代表性地解释Activity就是,你必须标明你应用程序的入口点.

视图(Views)可以将其自身绘制到屏幕(screen)上。Android的接口都是由一组以树的形式出现的视图组成的。开发者可以通过创建一个新的视图的方法来使用自定义的图形处理技术(比如开发游戏,或者是使用了不常用的用户图形(UI)窗口界面(widget))。

Intents是一个消息操作对象.如果一个应用程序想要显示一个网页,那么它表示为Intent,我们可以通过Intent实例创建一个URI视图并且可以手工断开系统.系统设置一些代码(例如浏览器),可以让我们知道如果去操作Intent并且运行它.Intents 也可以被用于广播系统范围内的有效事件(例如播发一则通知).

An Intent is asimple message object that represents an "intention" to do something.For example, if your application wants to display a web page, it expresses its"Intent" to view the URI by creating an Intent instance and handingit off to the system.

The system locatessome other piece of code (in this case, the Browser) that knows how to handlethat Intent, and runs it. Intents can also be used to broadcast interestingevents (such as a notification) system-wide.

服务是运行在后台的一段代码.它可以运行在它自己的进程,也可以运行在其他应用程序的进程里面,这要取决于自身的需要. 其他组件绑定到这个服务上面,并且可以请求远程方法调用.例如媒体播放器的服务,甚至当用户退出媒体用户向导界面,音乐依然可以持续播放.甚至当用户界面关闭,音乐播放依然继续.

A Service is a bodyof code that runs in the background. It can run in its own process, or in thecontext of another application's process, depending on its needs. Othercomponents "bind" to a Service and invoke methods on it via remoteprocedure calls. An example of a Service is a media player; even when the userquits the media-selection UI, she probably still intends for her music to keepplaying. A Service keeps the music going even when the UI has completed.

通知将以小图标的形式呈现在状态栏里.收到消息以后,用户可以与图标进行交互式操作.大部分熟知的通知是以短信息,通话记录,语音邮件的形式创建出来.通知是提请用户注意的重要机制.

A Notification is asmall icon that appears in the status bar. Users can interact with this icon toreceive information. The most well-known notifications are SMS messages, callhistory, and voicemail, but applications can create their own. Notificationsare the strongly-preferred mechanism for alerting the user of something thatneeds their attention.

ContentProvider是访问数据设备的提供者.典型的例子是访问用户联系列表.你的应用程序需要访问的数据可以由ContentProvider来支持.并且你也可以定义自己专用数据的ContentProviders.

A ContentProvideris a data storehouse that provides access to data on the device; the classicexample is the ContentProvider that's used to access the user's list ofcontacts. Your application can access data that other applications have exposedvia a ContentProvider, and you can also define your own ContentProviders toexpose data of your own.

2.2.1 AndroidManifest.xml文件

AndroidManifest.xml is a required file for everyapplication. It sits in the root folder for an application, and describesglobal values for your package, including the application components(activities, services, etc) that the package exposes and the implementationclasses for each component, what kind of data each can handle, and where theycan be launched.

AndroidManifest.xml是每一个应用都需要的文件. 位于应用根目录下, 描述了程序包的一个全局变量, 包括暴露的应用组件(activities, services等等)和为每个组件的实现类, 什么样的数据可以操作, 以及在什么地方运行.

An important aspect of this fileare the intent filters that it includes. These filters describe where and whenthat activity can be started. When an activity (or the operating system) wantsto perform an action such as open a Web page or open a contact picker screen,it creates an Intent object. This object can hold several descriptorsdescribing what you want to do, what data you want to do it to, the type ofdata, and other bits of information. Android compares the information in anIntent object with the intent filter exposed by every application and finds theactivity most appropriate to handle the data or action specified by the caller.More details on intents is given in the Intent reference page.

这个文件的一个重要方面(概念)是其中的intent过滤器. 这个过滤器描述了何时何种情况下让activity 启动. 当一个activity(或是操作系统)想要执行一个动作, 例如打开一个Web页或是打开一个联系人选取屏幕, 会创建一个Intent对象. 该对象包含了很多的描述信息, 描述了你想做什么操作, 你想处理什么数据, 数据的类型, 以及一些其他的重要信息.Android拿这个Intent的信息与所有应用暴露的intent过滤器比较, 找到一个最能恰当处理请求者要求的数据和action的activity. intents的更多信息在Intent页.

Besides declaring yourapplication's Activities, Content Providers, Services, and Intent Receivers,you can also specify permissions and instrumentation (security control andtesting) in AndroidManifest.xml. For a reference of the tags and theirattributes, please see AndroidManifest.

另外还要声明您的应用的Activities,Content Providers, Services, 和 Intent Receivers, 你也可以在AndroidManifest.xml文件中指定权限和instrumentation(安全控制和测试). 请查看AndroidManifest, 了解这个标签和他们的属性.

A simple AndroidManifest.xml lookslike this:

一个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 class=".HelloActivity">
               <intent-filter>
                   <action android:value="android.intent.action.MAIN"/>
                   <category android:value="android.intent.category.LAUNCHER"/>
               </intent-filter>
           </activity>
           
       </application>
       
   </manifest>

Some general items to note:

这里记录了一些通用特性:

·  Almostevery AndroidManifest.xml (as well as many other Android XML files) willinclude the namespace declaration xmlns:android="http://schemas.android.com/apk/res/android"in its first element. This makes a variety of standard Android attributesavailable in the file, which will be used to supply most of the data forelements in that file.

·  几乎所有的AndroidManifest.xml 文件(同其他的Android文件一样)都会包含一个命名空间的声明-xmlns:android="http://schemas.android.com/apk/res/android"-在第一个元素中. 该声明使标准Android属性在该文件中得以使用,该属性为文件的xml元素提供了大部分数据.

·  Mostmanifests include a single <application> element, which defines all ofthe application-level components and properties that are available in thepackage.

·  大多数AndroidManifest.xml 文件仅包含一个<application>元素, 该元素定了这个程序包内所有应用层面上可用的组件和属性.

·  Anypackage that will be presented to the user as a top-level application availablefrom the program launcher will need to include at least one Activity componentthat supports the MAIN action and LAUNCHER category as shown here.

·  TODO

Here is a detailed outline of thestructure of an AndroidManifest.xml file, describing all tags that areavailable.

下面列出了AndroidManifest.xml 这个文件详细的结构大纲, 描述了所有可用标签.

<manifest>

The root node of the file, describing the completecontents of the package. Under it you can place:

文件根节点, 描述了程序包的所有内容. 在其节点下面内可以放置:

<uses-permission>

Requests a security permission that your package mustbe granted in order for it to operate correctly. See the Security Modeldocument for more information on permissions. A manifest can contain zero ormore of these elements.

请求一个安全授权, 必须被授予该权限, 您的程序包才能正确的操作. 查看安全模块文档, 了解有关授权的更多信息. 一个manifest可以包含零个或多个这样的节点.

<permission>

Declares a security permission that can be used torestrict which applications can access components or features in your (oranother) package. See the Security Model document for more information onpermissions. A manifest can contain zero or more of these elements.

声明一个安全授权, 用来限制哪些应用可以访问您的程序包内的组件和特有机制. 查看安全模块文档, 了解有关授权的更多信息. 一个manifest可以包含零个或多个这样的节点.

<instrumentation>

Declares the code of an instrumentation component thatis available to test the functionality of this or another package. SeeInstrumentation for more details. A manifest can contain zero or more of theseelements.

TODO

<application>

Root element containing declarations of theapplication-level components contained in the package. This element can alsoinclude global and/or default attributes for the application, such as a label,icon, theme, required permission, etc. A manifest can contain zero or one ofthese elements (more than one application tag is not allowed). Under it you canplace zero or more of each of the following component declarations:

描述程序包内应用级别组件的根节点. 该节点能够描述应用程序的全局(和/或)默认属性, 例如标签, 图标, 主题, 需要的授权, 等等. 一个manifest可以包含零个或一个这样的节点(多个application 节点是不允许的). 在该节点下, 可以包含零个或多个以下每个组件的声明:

<activity>

An Activity is the primary facility for an applicationto interact with the user. The initial screen the user sees when launching anapplication is an activity, and most other screens they use will be implementedas separate activities declared with additional activity tags.

Activity 是应用于用户交互的最主要机制.当一个应用运行的时候, 用户看到的第一个屏幕就是activity, 并且, 用户所使用的其他绝大多数屏幕(界面)也会是

Note:Every Activity must have an <activity> tag in the manifest whether it isexposed to the world or intended for use only within its own package. If anActivity has no matching tag in the manifest, you won't be able to launch it.

Optionally, to support late runtime lookup of youractivity, you can include one or more <intent-filter> elements todescribe the actions the activity supports:

<intent-filter>

Declares a specific set of Intent values that acomponent supports, in the form of an IntentFilter. In addition to the variouskinds of values that can be specified under this element, attributes can begiven here to supply a unique label, icon, and other information for the actionbeing described.

<action>

An Intent action that the component supports.

<category>

An Intent category that the component supports.

<type>

An Intent data MIME type that the component supports.

<scheme>

An Intent data URI scheme that the component supports.

<authority>

An Intent data URI authority that the componentsupports.

<path>

An Intent data URI path that the component supports.

<receiver>

An IntentReceiver allows an application to be toldabout changes to data or actions that happen, even if it is not currentlyrunning. As with the activity tag, you can optionally include one or more<intent-filter> elements that the receiver supports; see the activity's<intent-filter> description for more information.

一个IntentReceiver可以让应用接收到一次数据变化和一次行为发生的通知, 甚至这个应用没有在运行也可以.同activity 标签一样, 你可以选择包含一个或多个<intent-filter>元素; 查看activity的<intent-filter>标签描述了解更多信息.

<service>

A Service is a component that can run in the backgroundfor an arbitrary amount of time. As with the activity tag, you can optionallyinclude one or more <intent-filter> elements that the receiver supports;see the activity's <intent-filter> description for more information.

Service 是一个在后台任意时刻都可以运行的组件. 同activity 标签一样, 你可以选择包含一个或多个<intent-filter>元素; 查看activity的<intent-filter>标签描述了解更多信息.

<provider>

A ContentProvider is a component that managespersistent data and publishes it for access by other applications.

ContentProvider组件是用来管理数据持久化及数据发布的, 发布的数据可以被其他的应用访问.

2.2.2 Activity

2.2.3 View

android.view

公有类

android.view.View

java.lang.Object

android.view.View Drawable.Callback KeyEvent.Callback

视图(View)类代表了一种基本的用户界面组成模块。一个视图占据了屏幕上的一个矩形区域,并响应绘制图形和事件处理。视图类是窗体类(Widget)的基类,而窗体类用来生成可交互的用户图形接口(interactiveGUI)。

视图类的使用窗口中所有的视图构成一个树形结构。要想增加视图,既可以用直接添加代码的方法,也可以在一个或者多个XML文件中声明新视图构成的树。在视图类的子类中,有的可以用来控制,有的具有显示文字、图片或者其他内容的功能。

当视图树被创建后,以下这若干种通用操作将可以被使用: 1.设置属性(properties):比如,可以设置TextView类的一个实例的文本内容。不同的子类可以用来设置的属性与方法不同。注意:只有编译时能够检测到的属性才可以在XML布局管理(layout)文件中设置。

2.设置输入焦点(focus):为了响应用户输入,整个框架将处理移动的焦点。如果想把焦点强制指向某一个特定的视图,必须调用requestFocus()方法。

3.设置监听器(listener):在视图中,允许设置监听器来捕获用户感兴趣的某些事件。比如说,在所有的视图中,无论视图是获得焦点还是失去焦点,都可以通过设置监听器来捕获。可以通过调用setOnFocusChangeListener(View.OnFocusChangeListener)来注册一个监听器。在其他视图子类中,提供了一些更加特殊的监听器。比如,一个按键(Button)可以触发按键被按下的事件。

4.设置是否可视(visibility):可以通过调用setVisibility(int)来显示或者隐藏视图。

2.2.4 Intent

Intent 介绍

Intent是对被执行操作的抽象描述。调用 startActivity(Intent),可以启动 Activity;调用broadcastIntent(Intent),可以把 Intent 发送给任何相关的IntentReceiver 组件;调用 startService(Intent, Bundle) 以及 bindService(Intent, String, ServiceConnection, int) 可以让应用和后台服务进行通信。

Intent 提供了一个在不同应用的代码之间进行晚绑定 (late runtime binding) 的机制。它主要被用来启动 Activities,因此可以被看作是Activities 之间的粘合剂。Intent 大体上是一个被动数据结构,该数据结构包括被执行动作的抽象描述。Intent 中的主要内容有:

action -- 需要被执行的动作。比如 VIEW_ACTION, EDIT_ACTION, MAIN_ACTION 等。

data -- 执行动作要操作的数据,在 Intent 里用指向数据记录的URI (ContentURI) 表示。比如联系人数据库中的一个联系人记录。

译注:被动数据结构:只能由外部线程或者进程改变的数据结构。与能够通过相关的线程或者进程执行内部操作从而产生外部行为的主动数据结构相对应。

下面是一些 action/data 对的例子:

VIEW_ACTIONcontent://contacts/1 -- 显示标识符为"1"的联系人的信息。

EDIT_ACTIONcontent://contacts/1 -- 编辑标识符为"1"的联系人的信息。

VIEW_ACTIONcontent://contacts/ -- 显示可遍历的联系人列表。这是用来进入联系人应用主界面(顶级入口,top-level entry)的典型方法。在这个界面中察看某个联系人会产生一个新的 Intent:{VIEW_ACTIONcontent://contacts/N},用来启动新的Activity,显示该联系人的详细信息。

PICK_ACTIONcontent://contacts/ -- 先是可遍历的联系人列表,并且允许用户在列表中选择一个联系人,然后把这个联系人返回给"上级活动"(parent activity)。例如:电子邮件客户端可以使用这个 Intent,要求用户在联系人列表中选择一个联系人。

除了 action, data 两个主要属性,Intent还具有一些其它属性,这些属性也可以被用在Intent 里:

category -- 类别,被执行动作的附加信息。例如 LAUNCHER_CATEGORY 表示Intent 的接受者应该在 Launcher 中作为顶级应用出现;而ALTERNATIVE_CATEGORY 表示当前的 Intent 是一系列的可选动作中的一个,这些动作可以在同一块数据上执行。

type -- 数据类型,显式指定 Intent 的数据类型 (MIME)。一般上 Intent 的数据类型能够根据数据本身进行判定,但是通过设置这个属性,可以强制采用显式指定的类型而不再进行推导。

component -- 组件,为使用 Intent 的组件类指定名称。通常会根据 Intent 中包含的其它信息 ——比如 action, data/type, categories ——进行查找,最终找到一个与之匹配的组件。如果这个属性存在的话,将直接使用它指定的组件,不再执行上述查找过程。指定了这个属性以后,Intent 的其它所有属性都是可选的。

extras -- 额外的附加信息,是其它所有附加信息的集合。使用 extras 可以为组件提供扩展信息,比如,如果要发送电子邮件,也就是要执行“发送电子邮件”的动作,可以将电子邮件的标题、正文等保存在 extras 里。

在 Intent 类里定义了多种标准 action 和 category 常量(字符串),同时应用也可以根据自己的需要进行定义。这些字符串使用 JAVA 风格的 scoping,从而保证它们的唯一性。比如标准 VIEW_ACTION 的定义是 “android.app.action.VIEW”。

概括而言,“动作”、“数据类型”、“类别”(译注:Intent的action类型)和“附加数据”一起形成了一种语言。这种语言使得系统能够理解诸如“打john的手机”之类的短语。随着应用不断的加入到系统中,它们可以添加新的“动作”、“数据类型”、“类别”来扩展这种语言。应用也可以提供自己的 activities 来处理已经存在的“短语”,从而改变这些“短语”的行为。

[编辑] Intent 解析

Intent 有两种主要形式:

显式意图(直接意图?)。显式意图是指定了 component 属性的 intents。调用 setComponent(ComponentName) 或者 setClass(Context, Class) 可以为 intents 设定 component属性——指定具体的组件类。这些 intents 一般不包括包括其它任何信息,它们通常只是用来通知应用启动内部的 activities 作为该应用的(当前)用户界面。

隐式意图(含蓄意图?)。隐式意图是没有指明 comonent 的 intents。这些 intents 必须包括足够的信息,这样系统才能确定在所有的可用组件中,对一个 intent 来说运行哪一个组件才是最合适的。

在使用 implicit intents 的时候,对于一个任意的 intent,我们需要知道用它来做什么。“Intent 解析过程”用来处理这个问题。“Intent 解析过程”将 intent 映射到可以处理它的activity, IntentReceiver 或者 service。

Intent 解析机制主要是将已安装应用程序包里的 Intent-Filter 描述和 Intent 进行匹配。如果使用广播发送 Intent,还要在已经注册的IntentReceiver 中尽心匹配。更多的相关描述可以在IntentFilter 中找到。

在解析 Intent 的过程中要用到Intent 的三个属性:动作、数据类型和类别。使用这些属性,就可以 PackageManager 上查询能够处理当前 intent 的合适组件。组件是否合适由 AndroidManifest.xml 文件中提供的 intent 信息决定。判断的方法如下:

The action, if given, must be listed by thecomponent as one it handles.

如果 intent 指明了要执行的action,组件 action 列表中就必须包含着个action,否则不能匹配;

如果 Intent 没有提供数据类型(type),系统从数据 (data) 中得到数据类型。和action 一样,组件的数据类型列表中必须包含 intent 的数据类型,否则不能匹配。

如果 Intent 中的数据不是content: 类型的 URL,而且 Intent 也没有明确指定它的数据类型,将根据 Intent 中数据的 scheme(比如 http: ormailto:) 进行匹配。同上,Intent 的 scheme 必须出现在组件的 scheme 列表中。

如果 Intent 指定了一个或多个类别,这些类别必须全部出现在组建的类别列表中。比如 intent 中包含了两个类别:LAUNCHER_CATEGORY 和 ALTERNATIVE_CATEGORY,解析得到的组件必须至少包含这两个类别。

以一个应用实例作为例子,这个应用可以让用户浏览便笺列表、查看每一个便笺的详细信息:

<manifestxmlns:android="http://schemas.android.com/apk/res/android"

     package="com.google.android.notepad">

    <applicationandroid:icon="@drawable/app_notes"

           android:label="@string/app_name">

 

        <provider class="NotePadProvider"

               android:authorities="com.google.provider.NotePad" />

               

        <activityclass=".NotesList"android:label="@string/title_notes_list">

            <intent-filter>

                <actionandroid:value="android.intent.action.MAIN" />

                <categoryandroid:value="android.intent.category.LAUNCHER" />

            </intent-filter>

            <intent-filter>

                <actionandroid:value="android.intent.action.VIEW" />

                <actionandroid:value="android.intent.action.EDIT" />

                <actionandroid:value="android.intent.action.PICK" />

                <categoryandroid:value="android.intent.category.DEFAULT" />

                <typeandroid:value="vnd.android.cursor.dir/vnd.google.note" />

            </intent-filter>

            <intent-filter>

                <actionandroid:value="android.intent.action.GET_CONTENT" />

                <categoryandroid:value="android.intent.category.DEFAULT" />

                <typeandroid:value="vnd.android.cursor.item/vnd.google.note" />

            </intent-filter>

        </activity>

 

        <activityclass=".NoteEditor" android:label="@string/title_note">

            <intent-filterandroid:label="@string/resolve_edit">

                <action android:value="android.intent.action.VIEW"/>

                <actionandroid:value="android.intent.action.EDIT" />

                <categoryandroid:value="android.intent.category.DEFAULT" />

                <typeandroid:value="vnd.android.cursor.item/vnd.google.note" />

            </intent-filter>

 

            <intent-filter>

                <actionandroid:value="android.intent.action.INSERT" />

                <categoryandroid:value="android.intent.category.DEFAULT" />

                <type android:value="vnd.android.cursor.dir/vnd.google.note"/>

            </intent-filter>

 

        </activity>

       

        <activityclass=".TitleEditor"android:label="@string/title_edit_title"

               android:theme="@android:style/Theme.Dialog">

            <intent-filterandroid:label="@string/resolve_title">

                <actionandroid:value="com.google.android.notepad.action.EDIT_TITLE" />

                <categoryandroid:value="android.intent.category.DEFAULT" />

                <category android:value="android.intent.category.ALTERNATIVE"/>

                <categoryandroid:value="android.intent.category.SELECTED_ALTERNATIVE" />

                <typeandroid:value="vnd.android.cursor.item/vnd.google.note" />

            </intent-filter>

        </activity>

 

    </application>

</manifest>


例子中的第一个 activity 是com.google.android.notepad.NotesList。它是进入应用的主入口(mainentry),具有三种功能,分别由三个 intent 模板进行描述。

第一个功能是进入便笺应用的顶级入口。它的类型是 android.app.category.LAUNCHER,说明这个应用应该在 Launcher 中被列出。

第二个功能用来浏览可用的便笺,或者让用户选择一个特定的便笺并且把这个便笺返回给调用者。当数据类型是 vnd.android.cursor.dir/vnd.google.note (便笺记录的目录) 的时候,执行动作 android.app.action.VIEW 可以浏览可用的便笺;执行动作 android.app.action.PICK 可以让用户选择便笺。

第三个功能返回给调用者一个用户选择的便笺。当数据类型是 vnd.android.cursor.dir/vnd.google.note 的时候,执行动作 android.app.action.GET_COUTENT 调用者不需要知道


有了这些功能,就能够将下列 intents 匹配到NotesList 的 activity:

{action=android.app.action.MAIN }. 如果activities 能够被用作进入应用的顶级入口,就可以和这个 intent 进行匹配。

{action=android.app.action.MAIN, category=android.app.category.LAUNCHER }. 这是目前 Launcher 实际使用的 intent,构成了它的顶级列表。

问题:怎么构成??

{action=android.app.action.VIEW data=content://com.google.provider.NotePad/notes}. 显示"content://com.google.provider.NotePad/notes" 下所有便笺的列表,用户可以遍历这个列表,并且察看便笺的详情。

{action=android.app.action.PICK data=content://com.google.provider.NotePad/notes}. 让用户在"content://com.google.provider.NotePad/notes" 之下的便笺列表中选择一个,然后将这个便笺的 URL 返回给调用者。

{action=android.app.action.GET_CONTENTtype=vnd.android.cursor.item/vnd.google.note }. 这个 intent 和上面的 pick 动作类似,不同的是这个 intent 允许调用者(仅仅)指定它们需要的数据类型(,而不需要了解数据存放的详细位置,即数据的 URI)。系统根据这个数据类型选择恰当的 activity,然后让用户选择某些数据。

第二个 activity 是com.google.android.notepad.NoteEditor,它为用户显示一个单独的便笺,并且允许用户对这个便笺进行修改。它具有两个 intent 模板,所以具有两个功能。第一个操作是主要的操作,允许用户察看和编辑一个便签(执行 android.app.action.VIEW 和 android.app.action.EDIT 动作,数据类型是vnd.android.cursor.item/vnd.google.note)。第二个模板可以让调用者显示创建新便笺的用户界面,并且将新便笺插入到便笺列表中(执行 android.app.action.INSERT 动作,数据类型是 vnd.android.cursor.dir/vnd.google.note)。

有了这两个功能,下列 intents 就能够匹配到NotesList 的 activity:

{action=android.app.action.VIEW data=content://com.google.provider.NotePad/notes/{ID}} 向用户显示标识为 ID 的便笺。将标识为 ID 的便笺缩写为 note{ID},下同。

{action=android.app.action.EDITdata=content://com.google.provider.NotePad/notes/{ID} } 让用户能够编辑 notes{ID}。

{action=android.app.action.INSERT data=content://com.google.provider.NotePad/notes} 创建一个新的便笺,新便笺被创建在“content://com.google.provider.NotePad/notes”所表示的便笺列表中。用户可以编辑这个便签。当用户保存这个便笺后,这个新便笺的 URI 将会返回给调用者。

最后一个 activity 是com.google.android.notepad.TitleEditor,它可以让用户编辑便笺的标题。它可以被实现为一个类,在 intent 中明确设定 component 属性后,应用可以直接调用这个类;不过在这里我们展示的是如何在已有数据上发布可选操作。这个 activity 只有一个单独的intent 模板,它具有一个私有 action:com.google.android.notepad.action.EDIT_TITLE,允许用户编辑便笺的标题。和前面的 view 和 edit 动作一样,调用这个intent 的时候,也必须指定具体的便笺。不一样的是,这里显示和编辑的只是便笺数据中的标题。

除了支持确省类别 (default category, android.intent.category.DEFAULT,原文是 android.intent.category.VIEW,有误),标题编辑器还支持另外两个标准类别:android.intent.category.ALTERNATIVE 和 android.intent.category.SELECTED_ALTERNATIVE。实现了这两个类别之后,其它 activities 可以调用函数queryIntentActivityOptions(ComponentName, Intent[], Intent, int) 查询这个 activity 支持的 actions,而不需要了解它的具体实现;或者调用 addIntentOptions(int, int, ComponentName,Intent[], Intent, int, Menu.Item[]) 建立动态菜单。需要说明的是,这个 intent 模板有一个明确的名称(通过android:label="@string/resolve_title" 指定)。在用户浏览数据的时候,如果这个 activity 是数据的一个可选操作,指定明确的名称可以为用户提供一个更好控制界面。

有了这个功能,下列 intents 就能够匹配到NotesList 的 activity:

{action=com.google.android.notepad.action.EDIT_TITLEdata=content://com.google.provider.NotePad/notes/{ID} } 显示并且允许用户编辑 note{ID} 的标题。

[编辑] Activity 的标准动作 (Actions)

下面是 Intent 为启动activities 定义的标准动作,一般使用 startActivity(Intent) 启动 activities。其中最重要也是最经常使用的是 MAIN_ACTION 和 EDIT_ACTION.

MAIN_ACTION

VIEW_ACTION

EDIT_ACTION

PICK_ACTION

GET_CONTENT_ACTION

DIAL_ACTION

CALL_ACTION

SENDTO_ACTION

ANSWER_ACTION

INSERT_ACTION

DELETE_ACTION

RUN_ACTION

LOGIN_ACTION

CLEAR_CREDENTIALS_ACTION

SYNC_ACTION

PICK_ACTIVITY_ACTION

WEB_SEARCH_ACTION

[编辑] 标准的广播动作 (broadcase actions)

下面是 Intent 为接收广播而定义的动作。可以通过 registerReceiver(IntentReceiver, IntentFilter),或者在 manifest 中增加 receiver标记来注册。

TIME_TICK_ACTION

TIME_CHANGED_ACTION

TIMEZONE_CHANGED_ACTION

BOOT_COMPLETED_ACTION

PACKAGE_ADDED_ACTION

PACKAGE_REMOVED_ACTION

BATTERY_CHANGED_ACTION

[编辑]标准类别

下面是已定义的标准类别。通过 addCategory(String) 可以为 Intent 设置类别。

DEFAULT_CATEGORY

BROWSABLE_CATEGORY

TAB_CATEGORY

ALTERNATIVE_CATEGORY

SELECTED_ALTERNATIVE_CATEGORY

LAUNCHER_CATEGORY

HOME_CATEGORY

PREFERENCE_CATEGORY

GADGET_CATEGORY

TEST_CATEGORY

[编辑]标准附加数据

下面是已定义的标准字段,用来在 putExtra(String, Object) 中为 Intent 设置附加数据。

TEMPLATE_EXTRA

INTENT_EXTRA

[编辑] 启动标记 (launchflags)

下面是 Intent 中可能用到的启动标记,通过setLaunchFlags(int) 和 addLaunchFlags(int) 使用这些标记。

NO_HISTORY_LAUNCH

SINGLE_TOP_LAUNCH

NEW_TASK_LAUNCH

MULTIPLE_TASK_LAUNCH

FORWARD_RESULT_LAUNCH

[编辑]嵌套类


Intent.FilterComparison 持有一个 Intent 对象,并且为过滤实现了Intent 的比较操作。

[编辑]概要

[编辑]常量

Values

String

ADD_SHORTCUT_ACTION

动作:在系统中添加一个快捷方式。.

"android.intent.action.ADD_SHORTCUT"

String

ALL_APPS_ACTION

动作:列举所有可用的应用。
输入:无。

"android.intent.action.ALL_APPS"

String

ALTERNATIVE_CATEGORY

类别:说明 activity 是用户正在浏览的数据的一个可选操作。

"android.intent.category.ALTERNATIVE"

String

ANSWER_ACTION

动作:处理拨入的电话。

"android.intent.action.ANSWER"

String

BATTERY_CHANGED_ACTION

广播:充电状态,或者电池的电量发生变化。

"android.intent.action.BATTERY_CHANGED"

String

BOOT_COMPLETED_ACTION

广播:在系统启动后,这个动作被广播一次(只有一次)。

"android.intent.action.BOOT_COMPLETED"

String

BROWSABLE_CATEGORY

类别:能够被浏览器安全使用的 activities 必须支持这个类别。

"android.intent.category.BROWSABLE"

String

BUG_REPORT_ACTION

动作:显示 activity 报告错误。

"android.intent.action.BUG_REPORT"

String

CALL_ACTION

动作:拨打电话,被呼叫的联系人在数据中指定。

"android.intent.action.CALL"

String

CALL_FORWARDING_STATE_CHANGED_ACTION

广播:语音电话的呼叫转移状态已经改变。

"android.intent.action.CFF"

String

CLEAR_CREDENTIALS_ACTION

动作:清除登陆凭证 (credential)。

"android.intent.action.CLEAR_CREDENTIALS"

String

CONFIGURATION_CHANGED_ACTION

广播:设备的配置信息已经改变,参见 Resources.Configuration.

"android.intent.action.CONFIGURATION_CHANGED"

Creator

CREATOR

String

DATA_ACTIVITY_STATE_CHANGED_ACTION

广播:电话的数据活动(data activity)状态(即收发数据的状态)已经改变。

"android.intent.action.DATA_ACTIVITY"

String

DATA_CONNECTION_STATE_CHANGED_ACTION

广播:电话的数据连接状态已经改变。

"android.intent.action.DATA_STATE"

String

DATE_CHANGED_ACTION

广播:日期被改变。

"android.intent.action.DATE_CHANGED"

String

DEFAULT_ACTION

动作:和 VIEW_ACTION 相同,是在数据上执行的标准动作。

"android.intent.action.VIEW"

String

DEFAULT_CATEGORY

类别:如果 activity 是对数据执行确省动作(点击, center press)的一个选项,需要设置这个类别。

"android.intent.category.DEFAULT"

String

DELETE_ACTION

动作:从容器中删除给定的数据。

"android.intent.action.DELETE"

String

DEVELOPMENT_PREFERENCE_CATEGORY

类别:说明 activity 是一个设置面板 (development preference panel).

"android.intent.category.DEVELOPMENT_PREFERENCE"

String

DIAL_ACTION

动作:拨打数据中指定的电话号码。

"android.intent.action.DIAL"

String

EDIT_ACTION

动作:为制定的数据显示可编辑界面。

"android.intent.action.EDIT"

String

EMBED_CATEGORY

类别:能够在上级(父)activity 中运行。

"android.intent.category.EMBED"

String

EMERGENCY_DIAL_ACTION

动作:拨打紧急电话号码。

"android.intent.action.EMERGENCY_DIAL"

int

FORWARD_RESULT_LAUNCH

启动标记:如果这个标记被设置,而且被一个已经存在的 activity 用来启动新的 activity,已有 activity 的回复目标 (reply target) 会被转移给新的 activity。

16 0x00000010

String

FOTA_CANCEL_ACTION

广播:取消所有被挂起的 (pending) 更新下载。

"android.server.checkin.FOTA_CANCEL"

String

FOTA_INSTALL_ACTION

广播:更新已经被确认,马上就要开始安装。

"android.server.checkin.FOTA_INSTALL"

String

FOTA_READY_ACTION

广播:更新已经被下载,可以开始安装。

"android.server.checkin.FOTA_READY"

String

FOTA_RESTART_ACTION

广播:恢复已经停止的更新下载。

"android.server.checkin.FOTA_RESTART"

String

FOTA_UPDATE_ACTION

广播:通过 OTA 下载并安装操作系统更新。

"android.server.checkin.FOTA_UPDATE"

String

FRAMEWORK_INSTRUMENTATION_TEST_CATEGORY

类别:To be used as code under test for framework instrumentation tests.

"android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"

String

GADGET_CATEGORY

类别:这个 activity 可以被嵌入宿主 activity (activity that is hosting gadgets)。

"android.intent.category.GADGET"

String

GET_CONTENT_ACTION

动作:让用户选择数据并返回。

"android.intent.action.GET_CONTENT"

String

HOME_CATEGORY

类别:主屏幕 (activity),设备启动后显示的第一个 activity。

"android.intent.category.HOME"

String

INSERT_ACTION

动作:在容器中插入一个空项 (item)。

"android.intent.action.INSERT"

String

INTENT_EXTRA

附加数据:和 PICK_ACTIVITY_ACTION 一起使用时,说明用户选择的用来显示的 activity;和 ADD_SHORTCUT_ACTION 一起使用的时候,描述要添加的快捷方式。

"android.intent.extra.INTENT"

String

LABEL_EXTRA

附加数据:大写字母开头的字符标签,和 ADD_SHORTCUT_ACTION 一起使用。

"android.intent.extra.LABEL"

String

LAUNCHER_CATEGORY

类别:Activity 应该被显示在顶级的 launcher 中。

"android.intent.category.LAUNCHER"

String

LOGIN_ACTION

动作:获取登录凭证。

"android.intent.action.LOGIN"

String

MAIN_ACTION

动作:作为主入口点启动,不需要数据。

"android.intent.action.MAIN"

String

MEDIABUTTON_ACTION

广播:用户按下了“Media Button”。

"android.intent.action.MEDIABUTTON"

String

MEDIA_BAD_REMOVAL_ACTION

广播:扩展介质(扩展卡)已经从 SD 卡插槽拔出,但是挂载点 (mount point) 还没解除 (unmount)。

"android.intent.action.MEDIA_BAD_REMOVAL"

String

MEDIA_EJECT_ACTION

广播:用户想要移除扩展介质(拔掉扩展卡)。

"android.intent.action.MEDIA_EJECT"

String

MEDIA_MOUNTED_ACTION

广播:扩展介质被插入,而且已经被挂载。

"android.intent.action.MEDIA_MOUNTED"

String

MEDIA_REMOVED_ACTION

广播:扩展介质被移除。

"android.intent.action.MEDIA_REMOVED"

String

MEDIA_SCANNER_FINISHED_ACTION

广播:已经扫描完介质的一个目录。

"android.intent.action.MEDIA_SCANNER_FINISHED"

String

MEDIA_SCANNER_STARTED_ACTION

广播:开始扫描介质的一个目录。

"android.intent.action.MEDIA_SCANNER_STARTED"

String

MEDIA_SHARED_ACTION

广播:扩展介质的挂载被解除 (unmount),因为它已经作为 USB 大容量存储被共享。

"android.intent.action.MEDIA_SHARED"

String

MEDIA_UNMOUNTED_ACTION

广播:扩展介质存在,但是还没有被挂载 (mount)。

"android.intent.action.MEDIA_UNMOUNTED"

String

MESSAGE_WAITING_STATE_CHANGED_ACTION

广播:电话的消息等待(语音邮件)状态已经改变。

"android.intent.action.MWI"

int

MULTIPLE_TASK_LAUNCH

启动标记:和 NEW_TASK_LAUNCH 联合使用,禁止将已有的任务改变为前景任务 (foreground)。

8 0x00000008

String

NETWORK_TICKLE_RECEIVED_ACTION

广播:设备收到了新的网络 "tickle" 通知。

"android.intent.action.NETWORK_TICKLE_RECEIVED"

int

NEW_TASK_LAUNCH

启动标记:设置以后,activity 将成为历史堆栈中的第一个新任务(栈顶)。

4 0x00000004

int

NO_HISTORY_LAUNCH

启动标记:设置以后,新的 activity 不会被保存在历史堆栈中。

1 0x00000001

String

PACKAGE_ADDED_ACTION

广播:设备上新安装了一个应用程序包。

"android.intent.action.PACKAGE_ADDED"

String

PACKAGE_REMOVED_ACTION

广播:设备上删除了一个应用程序包。

"android.intent.action.PACKAGE_REMOVED"

String

PHONE_STATE_CHANGED_ACTION

广播:电话状态已经改变。

"android.intent.action.PHONE_STATE"

String

PICK_ACTION

动作:从数据中选择一个项目 (item),将被选中的项目返回。

"android.intent.action.PICK"

String

PICK_ACTIVITY_ACTION

动作:选择一个 activity,返回被选择的 activity 的类(名)。

"android.intent.action.PICK_ACTIVITY"

String

PREFERENCE_CATEGORY

类别:activity是一个设置面板 (preference panel)。

"android.intent.category.PREFERENCE"

String

PROVIDER_CHANGED_ACTION

广播:更新将要(真正)被安装。

"android.intent.action.PROVIDER_CHANGED"

String

PROVISIONING_CHECK_ACTION

广播:要求 polling of provisioning service 下载最新的设置。

"android.intent.action.PROVISIONING_CHECK"

String

RUN_ACTION

动作:运行数据(指定的应用),无论它(应用)是什么。

"android.intent.action.RUN"

String

SAMPLE_CODE_CATEGORY

类别:To be used as an sample code example (not part of the normal user experience).

"android.intent.category.SAMPLE_CODE"

String

SCREEN_OFF_ACTION

广播:屏幕被关闭。

"android.intent.action.SCREEN_OFF"

String

SCREEN_ON_ACTION

广播:屏幕已经被打开。

"android.intent.action.SCREEN_ON"

String

SELECTED_ALTERNATIVE_CATEGORY

类别:对于被用户选中的数据,activity 是它的一个可选操作。

"android.intent.category.SELECTED_ALTERNATIVE"

String

SENDTO_ACTION

动作:向 data 指定的接收者发送一个消息。

"android.intent.action.SENDTO"

String

SERVICE_STATE_CHANGED_ACTION

广播:电话服务的状态已经改变。

"android.intent.action.SERVICE_STATE"

String

SETTINGS_ACTION

动作:显示系统设置。输入:无。

"android.intent.action.SETTINGS"

String

SIGNAL_STRENGTH_CHANGED_ACTION

广播:电话的信号强度已经改变。

"android.intent.action.SIG_STR"

int

SINGLE_TOP_LAUNCH

启动标记:设置以后,如果 activity 已经启动,而且位于历史堆栈的顶端,将不再启动(不重新启动) activity。

2 0x00000002

String

STATISTICS_REPORT_ACTION

广播:要求 receivers 报告自己的统计信息。

"android.intent.action.STATISTICS_REPORT"

String

STATISTICS_STATE_CHANGED_ACTION

广播:统计信息服务的状态已经改变。

"android.intent.action.STATISTICS_STATE_CHANGED"

String

SYNC_ACTION

动作:执行数据同步。

"android.intent.action.SYNC"

String

TAB_CATEGORY

类别:这个 activity 应该在 TabActivity 中作为一个 tab 使用。

"android.intent.category.TAB"

String

TEMPLATE_EXTRA

附加数据:新记录的初始化模板。

"android.intent.extra.TEMPLATE"

String

TEST_CATEGORY

类别:作为测试目的使用,不是正常的用户体验的一部分。

"android.intent.category.TEST"

String

TIMEZONE_CHANGED_ACTION

广播:时区已经改变。

"android.intent.action.TIMEZONE_CHANGED"

String

TIME_CHANGED_ACTION

广播:时间已经改变(重新设置)。

"android.intent.action.TIME_SET"

String

TIME_TICK_ACTION

广播:当前时间已经变化(正常的时间流逝)。

"android.intent.action.TIME_TICK"

String

UMS_CONNECTED_ACTION

广播:设备进入 USB 大容量存储模式。

"android.intent.action.UMS_CONNECTED"

String

UMS_DISCONNECTED_ACTION

广播:设备从 USB 大容量存储模式退出。

"android.intent.action.UMS_DISCONNECTED"

String

UNIT_TEST_CATEGORY

类别:应该被用作单元测试(通过 test harness 运行)。

"android.intent.category.UNIT_TEST"

String

VIEW_ACTION

动作:向用户显示数据。

"android.intent.action.VIEW"

String

WALLPAPER_CATEGORY

类别:这个 activity 能过为设备设置墙纸。

"android.intent.category.WALLPAPER"

String

WALLPAPER_CHANGED_ACTION

广播:系统的墙纸已经改变。

"android.intent.action.WALLPAPER_CHANGED"

String

WALLPAPER_SETTINGS_ACTION

动作:显示选择墙纸的设置界面。输入:无。

"android.intent.action.WALLPAPER_SETTINGS"

String

WEB_SEARCH_ACTION

动作:执行 web 搜索。

"android.intent.action.WEB_SEARCH"

String

XMPP_CONNECTED_ACTION

广播:XMPP 连接已经被建立。

"android.intent.action.XMPP_CONNECTED"

String

XMPP_DISCONNECTED_ACTION

广播:XMPP 连接已经被断开。

"android.intent.action.XMPP_DISCONNECTED"

[编辑]公共构造函数

Public Constructor

Intent()

创建空的intent对象。

Intent(Intent o)

拷贝构造函数。

Intent(String action)

用指定的动作创建 intent 对象。

Intent(String action, ContentURI uri)

创建 intent 对象,指定动作和数据 (URI)。

Intent(Context packageContext, Class cls)

创建 intent 对象,指定 component。

Intent(String action, ContentURI uri, Context packageContext, Class cls)

创建 intent 对象,指定动作、数据和组件。

[编辑]公共方法

Public Constructor

Intent addCategory(String category)

向 intent 添加新的类别。

Intent addLaunchFlags(int flags)

向 intent 添加新的启动标记。

boolean filterEquals(Intent other)

判断两个 intent 是否相等:检查他们是否有完全相同的意图(用于过滤)。

int filterHashCode()

生成 intent 的哈希代码,该代码与 filterEquals 有同样的语义,即能用于进行 intent 比较。

String getAction()

获取 intent 要执行的动作,如:VIEW_ACTION。

Set getCategories()

获取 intent 对象所属的所有类别(集合)。

ComponentName getComponent()

获取 intent 关联的具体组件。

ContentURI getData()

获取 intent 对象要操作的数据 (URI)。

Object getExtra(String name, Object def)

获取 intent 的扩展数据。

Object getExtra(String name)

获取 intent 的扩展数据。

Bundle getExtras()

获取 intent 的扩展数据 map。

static Intent getIntent(String uri)

由 URI 创建 Intent。

int getLaunchFlags()

获取 intent 的所有启动标记。

String getScheme()

获取 intent 中数据的 sheme。

String getType()

获取 intent 明确声明的数据类型(显式声明的 MIME 类型,不是推导出来的类型)。

boolean hasCategory(String category)

Intent 是否指定了类别。

Intent putExtra(String name, Object value)

向 intent 添加扩展数据。

void putExtras(Intent src)

将 src 中的所有扩展数据复制到 intent 中。

void putExtras(Bundle extras)

向 intent 添加扩展数据。

void readFromParcel(Parcel in)

无。

void removeCategory(String category)

从 intent 删除一个类别。

void removeExtra(String name)

从 intent 删除扩展数据。

ComponentName resolveActivity(PackageManager pm)

取得用来处理这个 intent 的 activity 组件。

ActivityInfo resolveActivityInfo(PackageManager pm)

取得用来处理这个 intent 的 activity 的信息 (PackageManager.ActivityInfo)。

String resolveType(ContentResolver resolver)

取得 intent 的 MIME 数据类型。(判断顺序:intent 明确指定的类型;intent 数据隐式包含的数据类型)

String resolveType(Context context)

取得 intent 的 MIME 数据类型。(判断顺序:intent 明确指定的类型;intent 数据隐式包含的数据类型)

String resolveTypeIfNeeded(ContentResolver resolver)

如果 resolver 需要,返回 intent 的数据类型,否则返回空。

Intent setAction(String action)

设置 intent 要执行的动作。

Intent setClass(Context packageContext, Class cls)

设置运行 intent 的组件,和 setComponent 功能相同。

Intent setClassName(String packageName, String className)

设置运行 intent 的组件,和 setComponent 功能相同。

Intent setClassName(Context packageContext, String className)

设置运行 intent 的组件,和 setComponent 功能相同。

Intent setComponent(ComponentName component)

设置运行 intent 的组件。

Intent setData(ContentURI data)

设置处理 intent 的时候要操作的数据。

Intent setDataAndType(ContentURI data, String type)

设置 intent 的数据和数据类型 (MIME)。

Intent setLaunchFlags(int flags)

设置启动标记(用来控制 intent 被处理的方式)。

Intent setType(String type)

设置明确的 MIME 数据类型。

String toString()

为 intent 生成一个可读的字符串描述。

String toURI()

void writeToParcel(Parcel out)

[编辑] 从 java.lang.Object 继承的方法

clone, equals,finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

[编辑] 从 android.os.Parcelable 继承的方法

void writeToParcel(Parceldest)

2.2.5 Service

服务是在后台长时间运行的应用组件,不和用户直接进行交互。在每一个服务类的包的 AndroidManifest.xml 文件中,必须有一个相应的 <service> 声明。服务必须用Context.startService() 或者 Context.bindService() 启动。

和其它应用对象一样,服务运行在它们宿主进程的主线程里。这意味着,如果一个服务需要执行阻塞操作(比如网络操作)或者 CPU 敏感的操作(比如 MP3播放器),它应该分离出一个线程来执行这样的操作。

服务类是应用程序的生命周期中的一个重要部分。

在这里要讨论的内容有:

服务的生命周期

访问权限

进程生命周期

[编辑] 服务的生命周期

启动服务有两种方法。

如果客户调用 Context.startService(),系统将获得服务(如果服务不存在,系统创建服务,然后调用它的 onCreate() 方法),然后使用调用者提供的参数调用服务的 onStart(int, Bundle) 方法。从此以后,服务开始持续运行,直到 Context.stopService() 或者 stopSelf() 被调用。注意:多次调用Context.startService() 虽然会导致 onStart() 被多次调用,但是服务本身不会嵌套(原文如此,应该是说服务的实例只有一个,不会启动多个服务)。所以无论调用多少次 Context.startService(),只要调用一次 Context.stopService() 或者stopSelf(),服务就会停止运行。

客户也可以调用 Context.bindService() 获得到服务的永久连接。如果服务之前没有启动,一样会创建服务然后调用它的 onCreate() 方法;但是不会调用它的onStart() 方法。服务调用它的 getBinder() 方法,并且将返回的IBinder 对象传递给客户。连接建立以后,不管客户是否保留这个 IBinder 对象的引用,只要连接还存在,服务都会持续运行。通常返回的 IBinder 对象是一个由 AIDL 实现的复杂接口。

服务可以同时被启动和绑定多个连接。在这种情况下,只要服务被启动,或者存在着到这个服务的连接,服务都会持续运行。当两个条件都不满足时,系统调用服务的 onDestroy() 方法,服务从此被终止。当onDestroy() 返回的时候,所有的清理工作(停止线程,取消已经注册的 receivers)都已经完成。

[编辑]访问权限

对服务的全局访问权限可以通过服务的 manifest 中的<service> 元素指定。这样,其它应用需要在它们的 manifest 中声明对应的<uses-permission> 元素,这样才能启动、停止和绑定到服务。

同时,在执行 IPC 调用之前,服务可以调用checkCallingPermission(String) 对这次 IPC 调用的权限进行检查。

关于权限和安全方面的信息,请参考安全模型文档。

[编辑]进程生命周期

只要服务被启动或者被客户绑定(建立连接),Android 系统就尽可能维护一个进程来作这个服务的宿主。当系统内存不足的时候,系统需要杀死进程来出让内存。这时候在下列情况下,服务的宿主进程具有较高的优先级:

如果服务已经被启动,它的宿主进程比任何在屏幕上对用户可见的进程都具有更低的优先级;但是比其它所有不可见的进程都具有更高的优先级。通常对用户可见的进程的数量非常少,所以正在运行的服务在绝大多数时候不会被杀死 —— 除非系统的可用内存极其匮乏。

如果有客户绑定在服务上,服务的宿主进程的优先级至少和客户的优先级一样(不会比客户更低)。这意味着如果客户对用户可见,那么服务本身也会被系统认为对用户可见。

在服务的宿主进程中运行有其它应用组件,比如 activity,可以提高整个进程的优先级,而不是仅仅提高服务本身的优先级。

[编辑]概要

[编辑]常量

Values

String

TAG

"Service"

[编辑] 从类 android.content.Context 继承的常量:

ALARM_SERVICE,BIND_AUTO_CREATE, CONTEXT_IGNORE_SECURITY, CONTEXT_INCLUDE_CODE,INFLATE_SERVICE, KEYGUARD_SERVICE, LOCATION_SERVICE, MODE_APPEND, MODE_PRIVATE,MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE, NOTIFICATION_SERVICE, POWER_SERVICE,WINDOW_SERVICE

[编辑]公共构造函数

         Service()

Service()

[编辑]公共成员函数

公共成员函数

final

Application getApplication()

返回拥有这个服务的应用。

abstract

IBinder getBinder()

返回到这个服务的通信通道 (communication channel)。

final

void stopSelf(int startId)

停止服务,如果它最后一次启动的 ID 是 startID。

final

void stopSelf()

如果服务已经启动,则停止服务。

 

[编辑]保护成员函数

保护成员函数

void onCreate()

在服务第一次被创建的时候被调用。

void onDestroy()

在服务不再被使用,需要被删除时被调用。

void onStart(int startId, Bundle arguments)

客户调用 startService(Intent, Bundle) 直接启动服务的时候被调用。Bundle 是客户提供的参数,startID 是这次服务启动的唯一标识。

[编辑]从类 android.app.ApplicationContext 继承的方法:

applyThemeResource,bindService, broadcastIntent, broadcastIntent, broadcastIntent,checkCallingOrSelfPermission, checkCallingPermission, checkPermission,clearWallpaper, closeExternalStorageFiles, createDatabase,createPackageContext, deleteDatabase, deleteFile, fileList, getAssets,getClassLoader, getContentResolver, getDataDir, getFileStreamPath,getPackageManager, getPackageName, getPackagePath, getResources, getSharedPreferences,getSystemService, getTheme, getWallpaper, openDatabase, openFileInput,openFileOutput, peekWallpaper, registerExternalStorageListener,registerReceiver, registerReceiver, setTheme, setWallpaper, setWallpaper,showAlert, showAlert, showAlert, showAlert, startActivity,startInstrumentation, startService, stopService, unbindService,unregisterReceiver

[编辑] 从类 android.content.Context 继承的方法:

bindService,broadcastIntent, broadcastIntent, broadcastIntent,checkCallingOrSelfPermission, checkCallingPermission, checkPermission,clearWallpaper, createDatabase, createPackageContext, deleteDatabase,deleteFile, fileList, getAssets, getClassLoader, getContentResolver,getDataDir, getFileStreamPath, getPackageManager, getPackageName, getResources,getSharedPreferences, getString, getSystemService, getText, getTheme,getWallpaper, obtainStyledAttributes, obtainStyledAttributes,obtainStyledAttributes, obtainStyledAttributes, openDatabase, openFileInput,openFileOutput, peekWallpaper, registerReceiver, registerReceiver, setTheme,setWallpaper, setWallpaper, showAlert, showAlert, showAlert, showAlert,startActivity, startInstrumentation, startService, stopService, unbindService,unregisterReceiver

[编辑] 从类 java.lang.Object 继承的方法:

clone, equals,finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

[编辑]详细资料

[编辑]常数

[编辑] public static finalString TAG

常数值: "Service"

[编辑]公共构造函数

[编辑] public Service()

[编辑]公共成员函数

[编辑]public final Application getApplication()

返回拥有这个服务的应用。

[编辑] public abstractIBinder getBinder()

返回到服务的通信通道 (communication channel)。如果客户没有绑定到服务,返回 null。 IBinder 通常是一个复杂接口(参见 AIDL)。

返回值 返回一个 IBinder 对象,客户可以通过它调用服务(的功能)。

[编辑] public final voidstopSelf(int startId)

如果服务最后一次启动的 ID 是 startID,则停止服务。这个函数和调用 stopService(Intent) 有同样的结果,但是如果客户发起了新的启动请求,而请求还没有进入到 onStart(int, Bundle),这个函数可以避免停止服务。

参数 startId: 最后一次启动时在onStart(int, Bundle) 中收到的启动标识 (ID)。

参考 stopSelf()

[编辑] public final voidstopSelf()

如果服务已经被启动,则停止服务,和 stopService(Intent) 有同样的效果。 参见 stopSelf(int)

[编辑]保护成员函数

[编辑] protected voidonCreate()

在服务第一次被创建时被调用。

[编辑] protected voidonDestroy()

服务不再被使用而且需要被删除的时候被调用。服务在这里清理所有它占有的资源,包括线程、已经注册的 receivers 等。函数返回后,不应该再调用这个服务的任何方法。事实上,服务已经中止。

[编辑]protected void onStart(int startId, Bundle arguments)

在客户每次调用 startService(Intent, Bundle) 直接启动服务的时候被调用。

参数 startId: 表示这次启动请求的整数标识。可以用在 stopSelf(int) 中。 arguments: 客户在startService(Intent, Bundle) 中提供的 Bundle。可以为空。

参见 stopSelf(int)

2.2.6 NotificationManager

2.2.7 ContentProvider

 

2.3 数据存储与检索(共享数据的方法)

    典型的桌面操作系统提供一种公共文件系统——任何应用软件可以使用它来存储和读取文件,该文件也可以被其它的应用软件所读取(也许会有一些权限控制设定)。Andorid采用了一种不同的系统:在Android,所有的应用软件数据(包括文件)为该应用软件所私有。然而,Android同样也提供了一种标准方式供应用软件将私有数据开放给其它应用软件。这一章节描述一个应用软件存储和获取数据、开放数据给其它应用软件、从其他应用软件请求数据并且开放它们的多种方式

Android提供如下机制以存储和获取数据。

参数

一个轻量级的存储和获取机制,采用一对简单的数据类型:key和value。它的典型应用是存储应用软件参数。

文件

你可以将文件存储在手机或可移动存储媒介中。默认其他程序不可访问这些文件。

数据库

Android API包含有对SQLite的支持。你的应用程序可以创建并使用一个私有SQLite数据库。每个数据库为创建它的包所私有。

内容提供器

内容提供器是一个开放私有数据读写权限的可选组件。受限于该私有数据想施加的任何约束。内容提供器执行标准的数据请求语句、通过标准的数据访问机制返回数据。Android 提供了多种针对标准数据类型的内容提供器,例如个人通讯录。

网络

不要忘记你同样可以使用网络来存储和获取数据

2.3.1 参数

你可以存储应用软件启动时需要载入的参数,例如默认问候语或文本字体。调用Context.getSharedPreferences()读取和写入参数值,如果你想将参数共享给包内的其它组件,请为参数分配一个名字。或者使用Activity.getPreferences()和无名参数以对调用保持私有。你不能跨越包将参数共享。

这里是一个为计算器静音按键模式设置用户参数的例子。

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();
   }

}

 

2.3.2 使用文件

    Android 提供接口去读写一个软件的本地文件的streams。调用Context.openFileOutput() 和Context.openFileInput(),使用本地文件名和路径去读写文件。另一个软件调用这些方法时使用同样的文件名和路径字符串将无法工作。你只能访问本地文件。如果你有静态文件需要在编译时同软件一起打包,你可以把文件保存在工程的res/raw/<mydatafile>下,并且可以用Resources.openRawResource(R.raw.mydatafile)来获取它。

取自"http://www.androidcn.net/wiki/index.php/Devel/data/files"

2.3.3 SQLite数据库

Android支持SQLite数据库系统并开放数据库管理函数,这使你可以将复杂的数据集合储存到有用的对象中。例如,Android定义了一种由字符串型姓名、字符串型地址、数字型电话号码、一个位图图像和多种其它个人信息描述字段组成的通讯录数据类型。使用Context.createDatabase()和Context.openDatabase()创建读写数据库和适当的读写数据(注意:位图这样的文件数据与本地文件路径一样,以文件路径字符串值形式存放在数据库中)

Android整合sqlite3数据库工具,允许你在SQLite数据库中浏览表内容、运行SQL命令并执行其它有用的函数。

SQLite及其它的所有的数据库,被储存于/data/data/<package_name>/databases

创建多少表、包含多少字段、如何连接,已经超越了这篇文档的讨论范围,但是Android没有施加任何越过SQLiteconcepts的限制。我们极力推荐包含一个如唯一ID的自增字段以快速查找记录。对于私有数据,这并不需要。但是如果你使用一个内容提供器,你必须包括一个类似唯一ID的字段。请参考NotePad示例程序中的示例类NotePadProvider.java,那是一个创建和组装新数据库的例子。任何数据库可以凭借数据库名被该软件中的任何一个类访问,但不能在该软件范围外访问。

2.3.4 内容提供器

访问内容提供器

如果希望公开你的数据,你可以创建(或调用)一个内容提供器。这是一个可以从所有应用软件中存储和获取数据的对象。这也是穿越包共享数据的唯一方式——没有供所有包共享数据的公共存储区域。Android整合了基于多种公共数据类型(音频、视频、图像、个人通讯录信息等等)的内容提供器。你可以在provider包中看到许多Android自带的内容提供器。

实际数据储存的方式取决于它对接口的具体实现。但是所有内容提供器必须执行公共协议去请求数据和返回结果。内容提供器可以使用自定义帮助器功能使被开放的指定数据更容易被存储/获取。

使用内容提供器存储和获取数据

这一节阐述你或其他任何人如何使用内容提供器存储和获取数据。Adnroid为广泛的数据类型开放了多种数据提供器,从音乐文件、图像文件到电话号码。你可以从有用的android.provider包中看到一个开放的内容提供器列表。

Android的内容提供器被客户端宽松地连接。每一个内容提供器开放一个唯一的字符串(URI)来识别将要操作的数据类型,客户端必须使用该字符串来存储和获取相应类型的数据。在《数据请求》章节中我们将对此做更多解释。

这一节请求下列内容

请求数据

制作请求

请求的返还值

请求文件

读取获得的数据

修改数据

添加记录

删除记录

数据请求

每一种内容提供器开放一个唯一公共URI(由ContentURI封装),它将被客户端用于从内容提供器请求/添加/更新/删除数据。URI有2种形式:一是指出该类型数据的所有值(例如所有个人通讯录),二是指出该类型数据的特定记录(例如乔•史密斯的联络信息)

content://contacts/people/从设备返回通讯录姓名列表

content://contacts/people/23返回通讯录中ID=23的单行记录

当应用将请求发送到设备,要求获取整体数据(所有电话号码)或指定数据(鲍勃的电话号码)。Android将返回一个包含指定行的记录集游标。让我们来看一个假定的请求字符串和结果集(结果已被调整的更清晰一些)。

请求字符串 = 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由3部分组成:字符串“content://”;一个描述数据类型的段;一个可选的在特定内容范围内某特定记录的ID。这里有一些请求字符串的例子:

content://media/images从设备返回所有图像的URI

content://contacts/people/从设备返回通讯录中所有姓名列表的URI

content://contacts/people/23返回通讯录中ID=23的单行记录

2.3.5 Android网络存储

除了选择基于设备的存储,你还可以从可用网络存储和恢复数据。要操作网络,你需要使用下列包:

·  java.net.*

·  android.net.*

3 布局学习

http://code.google.com/android/reference/view-gallery.html

API编程举例

1 SQLite

1、   创建数据库

SQLiteDatabase db =Context.createDatabase(DATABASE_NAME,DATABASE_VERSION, 0,null);

2、   打开数据库

Context.openDatabase(DATABASE_NAME,null);

3、   建表

Db.execSQL();

4、   插入一行

    ContentValues initialValues = newContentValues();

    initialValues.put("title",title);

    initialValues.put("body", body);

     db.insert(DATABASE_TABLE, null,initialValues);

5、   删除一行

Db.delete(DATABASE_TABLE,”rowed=”+rowed,null);

6、   查询所有

  Cursor c =

                db.query(DATABASE_TABLE, newString[] {

                    "rowid","title", "body"}, null, null, null, null, null);

            int numRows = c.count();

            c.first();

            for (int i = 0; i < numRows;++i) {

                Row row = new Row();

                row.rowId = c.getLong(0);

                row.title = c.getString(1);

                row.body = c.getString(2);

                ret.add(row);

                c.next();

            }

7、   查询一行

 Cursor c =

            db.query(true, DATABASE_TABLE,new String[] {

                "rowid", "title", "body"}, "rowid=" + rowId, null, null,

                null, null);

        if (c.count() > 0) {

            c.first();

            row.rowId = c.getLong(0);

            row.title = c.getString(1);

            row.body = c.getString(2);

            return row;

        } else {

            row.rowId = -1;

            row.body = row.title = null;

        }

8、   更新

       ContentValues args = new ContentValues();

        args.put("title", title);

        args.put("body", body);

        db.update(DATABASE_TABLE, args, "rowid=" + rowId,null);

9、   调用

可在Activity中如此调用:dbHelper = new DBHelper(this)

学习过程中遇到的问题

1,  eclipse中debug与DDMS的关系?DDMS怎么用?

2,每次应用启动时,模拟器要模拟连网,速度挺慢,能不能省去这步?

3, 有时不能Debug ? 难道有2个的上限吗?

4,书写资源文件时如何提示?

5,  DEBUG时表现怪异,不能跟踪源代码?

       Android学习笔记-让我们快速上手吧

Google的Android SDK发布也有一段时间了,一直想研究一下却苦于找不到时间。利用这个周未,开始强迫自己再次进入学习状态,原因很简单:我看好开放的gPhone。

SDK的下载与安装并不复杂,网上也有不少同学已经进入状态了,我就不再重复了吧。

今天主要讨论的,还是永远不变的话题:Hello World.
1.最简单的HelloWorld

安装了SDK后,直接生成一个Android Project,一句代码不用写,就能跑出一个最简单的HelloWorld例程。我们看一下它的代码:

public void onCreate(Bundle icicle) ...{
        super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.main);    
}

看上去实在很简单,只有两句话而已。关键在这个R.layout.main上,凭直觉,这应该是定义的资源。的确,在R.java中只是定义了一个static int 而已,真正的资源描述在res/layout/main.xml文件里(注意:这里的R.java不要手工编辑,每次build project时它都会根据res下的资源描述被自动修改)。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView id="@+id/txt"  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Hello World"
    />
</LinearLayout>

这个文件很好读,一个描述了这是一个线性排列的布局,android:orientation=vertical表示所有组件将纵向排布。而经典的Hello World是用一个TextView来展示的。

由此,我们知道,Android的程序从一个Activity派生出来,并且从它的onCreate开始启动;Android里要显示的组件用XML文件描述而不用在代码中硬编码(这是一个好的习惯,我们应该从一开始就坚持下去);

2.让Button来说HelloWorld
上面的例子是ADT自动生成的代码,似乎与我们一点关系也没有。那我们来改一下代码,因为在windows平台上的Helloworld经常是由一个按钮触发的,所以,我们想第二个Helloworld应该是这样的:加一个按钮和文本输入框,单击按钮后在原来的TextView后面加上输入框中输入的文字。
第一步是,增加一个Button和一个EditText,与TextView一样,它们也在main.xml里描述一下:

 <EditText id="@+id/edt"  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text=""
    />   
<Button id="@+id/go"
        android:layout_width="wrap_content" android:layout_height="wrap_content" 
        android:text="@string/go">
        <requestFocus />
    </Button>

这里有两个地方要注意:id=@+id/go,这表示需要一个唯一的UID来作为Button的ID,它的引用名是go。还有一个是android:text=@string/go表示这个按钮的文本不是直接写有main.xml里了,而是来源于另一个资源描述文件strings.xml里,本例中的strings.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">helloTwo</string>
    <string name="tit_dialog">提示</string>
    <string name="msg_dialog">你好,中国</string>
    <string name="ok_dialog">确定</string>
    <string name="go">浏览</string> 
</resources>

然后,在代码里(onCreate函数中)我们加上以下代码(简单起见,用了嵌套类):

Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener(new View.OnClickListener()
        ...{
            public void onClick(View v)
            ...{
                EditText edt=(EditText)helloTwo.this.findViewById(R.id.edt);
                TextView txt= (TextView)helloTwo.this.findViewById(R.id.txt);
                txt.setText(getString(R.string.msg_dialog)+edt.getText());    
            }
        });

为铵钮增加一个onClick事件处理器,在点击事件中,设置txt的文本为R.string.msg_dialgo+edt.getText()。

这里的关键是两个函数的使用: findViewById(R.id.go)可以根据资源的名称加载View类型的资源,同样用函数getString(R.string.msg_dialog)可以加载字符串资源。

编译,run一下看看效果。
3. 再让菜单Say Hello

从API文档中我们看到Activity中有两个函数:onCreateOptionsMenu和onOptionsItemSelected,显示,这个OptionsMenu就是所谓的上下文菜单(在GPhone的模拟器上,有个键专用于弹出这个菜单)。下面我们就为这个HelloWorld例子加上一个菜单,并且让它可以Say hello。

这次,我们不涉及到资源的描述文件了,而是直接使用这两个函数来实现,其实代码也很简单,所以,我们再增加一个退出应用的功能(否则每次都是按取消键退出应用显示太不专业了)。

代码如下:

  public boolean onCreateOptionsMenu(Menu menu)
    ...{
        super.onCreateOptionsMenu(menu);
        menu.add(0,1,"say hello");
        menu.add(0,2,"exit");
        return true;
    }
    public boolean onOptionsItemSelected(Item item) 
    ...{
        super.onOptionsItemSelected(item);
        
        int id = item.getId();
        switch(id)...{
        case 1:
            AlertDialog.show(this,getString(R.string.app_name), 
                           getString(R.string.msg_dialog), getString(R.string.ok_dialog), true);
            break;
        case 2:
            finish();
            break;
        }

在CreateOptionsMenu时,我们简单地增加两个菜单项,menu.add(组ID,项ID,显示文本),(注意:这里我直接将文字写在代码里,这并不提倡)。然后,在OptionsItemSelected事件中,我们根据选中的菜单项做相应处理,如果选中1,则弹出一个对话框显示资源文件中的“你好,中国”,如果选中2则退出应用。

AlertDialog.show是一个静态方法,类似于我们在WIN平台上经常使用的MessageBox一样,很方便的。

来源:http://www.sf.org.cn/Android/lumen/20976.html

Android学习笔记(2)-初识Activity

根据文档的解释,Activity是Android开发中非常重要的一个基础类。我把它想像成J2ME中的Display类,或者是Win32平台上的Form类,也许不准确,但是它的重要性我觉得应该是一样的(当然,如果我们写的是一个没有界面的应用,例如后台运行的服务之类的,可以不用Display的)。

1.    在一个Activity中使用多个View

如果把Activity看作MVC中的Control?它负责管理UI和接受事件(包括用户的输入),虽然说一个Activity通常对应一个屏幕,但事实上,我们是可以只用一个Activity管理多个不同的View来实现简单的逻辑。
首先,我们增加一个新的资源描述layout/second.xml。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView id="@+id/txt"  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Hello 中国"
    />
 <Button id="@+id/go2"
        android:layout_width="wrap_content" android:layout_height="wrap_content" 
        android:text="back">
        <requestFocus />
    </Button>   
</LinearLayout>

除了一个“Hello中国”以外,增加一个按钮可以返回前一个界面。然后,在代码中我们要为helloTwo增加两个方法,setViewOneCommand和setViewTwoCommand,分别处理一下在不同界面时,从资源里加载组件并为组件绑定一个事件处理器。

  public void setViewOneCommand()
    ...{
        Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener(new View.OnClickListener()
        ...{
            public void onClick(View v)
            ...{
                helloTwo.this.setContentView(R.layout.second);
                helloTwo.this.setViewTwoCommand();                
            }
        });       
        Button btnExit=(Button)findViewById(R.id.exit);
        btnExit.setOnClickListener(new View.OnClickListener()...{
            public void onClick(View v)...{
                helloTwo.this.finish();
            }
        });    
    }
    public void setViewTwoCommand()
    ...{
        Button btnBack=(Button)findViewById(R.id.go2);
        btnBack.setOnClickListener(new View.OnClickListener()...{
            public void onClick(View v)...{
                helloTwo.this.setContentView(R.layout.main);
                helloTwo.this.setViewOneCommand();
            }                          
        });
    }

最后,我们需要在onCreate的时候,也就是启动后的main界面上设置一下按钮事件处理器。新的onCreate方法如下:

    public void onCreate(Bundle icicle) ...{
        super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.main);    
        setViewOneCommand();        
    }

编译,运行,OK。

2.    还是回到正道上,多个Activity之间的跳转

Android中提供一个叫Intent的类来实现屏幕之间的跳转,按文档的说法,似乎他们也建议采用这种方法,Intent的用法比较复杂,现在我先看看它最简单的用法。

先在应用中增加两个Activity,这需要修改AndroidManifest.xml文件了,如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.sharetop.android.hello.three">
    <application android:icon="@drawable/icon">
        <activity class=".HelloThree" android:label="@string/app_name">
            <intent-filter>
                <action android:value="android.intent.action.MAIN" />
                <category android:value="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity class=".HelloThreeB" android:label="@string/app_name">
        </activity>
    </application>
</manifest> 

很简单,就是加一个标签而已,新标签的class是.HelloThreeB,显示的应用标题与前一个Activity一样而已,然后第二步就是修改一个HelloThree类的实现,在onCreate方法中绑定按钮的事件处理器:

    public void onCreate(Bundle icicle) ...{
        super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.main);
        setViewOneCommand();
    }
    public void setViewOneCommand()
    ...{
        Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener(new View.OnClickListener()
        ...{
            public void onClick(View v)
            ...{
                Intent intent = new Intent();
                intent.setClass(HelloThree.this, HelloThreeB.class);
                startActivity(intent);
                finish();            
            }
        });       
        Button btnExit=(Button)findViewById(R.id.exit);
        btnExit.setOnClickListener(new View.OnClickListener()...{
            public void onClick(View v)...{
                HelloThree.this.finish();
            }
        });    
    } 

这里的跳转功能用Intent来操作,它的最简单用法就是用函数setClass()设置跳转前后两个Activity类的实例,然后调用Activity自己的startActivity(intent)即可。最后一句finish()表示将当前Activity关掉(如果不关掉会如何?你可以自己试一下看效果,事实上有时我们是不需要关掉当前Activity的)。

然后,我们同样弄一个Activity类HelloThreeB,代码与前面的差不多,只是将setClass的两个参数反一下,这样就可以简单地实现在两个Activity界面中来回切换的功能了。

3.    如果我想在两个Activity之间进行数据交换,怎么办?

前例中的startActivity()只有一个参数,如果需要向新打开的Activity传递参数,我们得换一个函数了, Android提供了startSubActivity(Intent,int)这个函数来实现这个功能。

函数原型为: public voidstartSubActivity(Intent intent, int requestCode)

这里的requestCode用来标识某一个调用,一般由我们定义一个常量。

如何把参数传过去呢?Intent类在提供setClass()函数的同时也提供了一个setData()函数。

函数原型为:public Intent setData(ContentURI data)

参数类型是ContentURI,它的详细内容下回再分析,现在就把它当成一个String类型来用吧。

参数带到新的Activity后,同样用Activity.getIntent()函数可以得到当前过来的Intent对象,然后用getData()就取到参数了。

把参数带回来的方法是Activity.setResult(),它有几个形式,现在先看最简单的一个吧。

函数原型是:public final void setResult(intresultCode, String data)

resultCode是返回代码,同样用来标识一个返回类型,而data则是它要返回的参数。

在原来的Activity中的事件处理回调函数onActivityResult,会被系统调用,从它的参数里可以得到返回值。

函数原型为:protected void onActivityResult(intrequestCode, int resultCode,String data, Bundle extras)

这里的requestCode就是前面启动新Activity时的带过去的requestCode,而resultCode则关联上了setResult中的resultCode,data是参数,extras也是一个很重要的东西,后面再研究一下它的作用。

下面,我们来看一下代码吧,先看看HelloThree中的代码:

    public void setViewOneCommand()
    ...{
        Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener(new View.OnClickListener()
        ...{
            public void onClick(View v)
            ...{
                try
                ...{                    
                    Intent intent = new Intent();
                    intent.setClass(HelloThree.this, HelloThreeB.class);
                    intent.setData(new ContentURI("One"));                   
                    startSubActivity(intent,REQUEST_TYPE_A);
                }
                catch(Exception ex)...{}
            }
        });       
        Button btnExit=(Button)findViewById(R.id.exit);
        btnExit.setOnClickListener(new View.OnClickListener()...{
            public void onClick(View v)...{
                HelloThree.this.finish();
            }
        });    
    } 
    protected void onActivityResult(int requestCode, int resultCode,
            String data, Bundle extras)
    ...{
        if (requestCode == REQUEST_TYPE_A) ...{
            if (resultCode == RESULT_OK) ...{
                Log.v(TAG,data);
                TextView txt = (TextView)findViewById(R.id.txt);
                txt.setText(data);                
            }
        }
    }

这里的REQUEST_TYPE_A是我们定义的一个常量。在onActivityResult中用它与RESULT_OK一起作为条件判断如何处理返回值,这里只是简单将TextView显示值换成传来的字串。

再来看看另一个HelloThreeB类的实现代码:

    private Intent i;
    protected void onCreate(Bundle icicle) ...{
        super.onCreate(icicle);
        setContentView(R.layout.second);    
        
        i = getIntent();
        
        android.util.Log.v(TAG,"onCreate");
        Button btn = (Button)findViewById(R.id.go);
        btn.setOnClickListener(new View.OnClickListener()...{
            public void onClick(View v)...{
                String result=HelloThreeB.this.i.getData().toString()+" And Two";
                HelloThreeB.this.setResult(RESULT_OK,result);
                finish();
            }            
        });
        
        TextView v = (TextView)findViewById(R.id.txt);
        v.setText("Param is "+i.getData().toString());
        
    }

在按钮处理事件中,从Intent取出参数,处理一下再用setResult返回给前一个Activity即可。

编译运行即可。

来源:http://www.sf.org.cn/Android/lumen/20977.html

Android学习笔记(3)-Activity的生命周期

注意到在Activity的API中有大量的onXXXX形式的函数定义,除了我们前面用到的onCreate以外,还有onStart,onStop以及onPause等等。从字面上看,它们是一些事件回调,那么次序又是如何的呢?其实这种事情,自己做个实验最明白不过了。在做这个实验之前,我们先得找到在Android中的Log是如何输出的。

显然,我们要用的是android.util.log类,这个类相当的简单易用,因为它提供的全是一些静态方法:

Log.v(String tag, String msg);       //VERBOSE
Log.d(String tag, String msg);      //DEBUG   
Log.i(String tag, String msg);       //INFO
Log.w(String tag, String msg);    //WARN
Log.e(String tag, String msg);     //ERROR

前面的tag是由我们定义的一个标识,一般可以用“类名_方法名“来定义。

输出的LOG信息,如果用Eclipse+ADT开发,在LogCat中就可以看到,否则用adb logcat也行,不过我是从来都依赖于IDE环境的。

好了,现在我们修改前面的HelloThree代码:

   public void onStart()
    ...{
        super.onStart();
        Log.v(TAG,"onStart");
    }
    public void onStop()
    ...{
        super.onStop();
        Log.v(TAG,"onStop");
    }
    public void onResume()
    ...{
        super.onResume();
        Log.v(TAG,"onResume");
    }
    public void onRestart()
    ...{
        super.onRestart();
        Log.v(TAG,"onReStart");
    }
    public void onPause()
    ...{
        super.onPause();
        Log.v(TAG,"onPause");
    }
    public void onDestroy()
    ...{
        super.onDestroy();
        Log.v(TAG,"onDestroy");
    }
    public void onFreeze(Bundle outState)
    ...{
        super.onFreeze(outState);
        Log.v(TAG,"onFreeze");
    }

在HelloThreeB中也同样增加这样的代码,编译,运行一下,从logcat中分析输出的日志。
在启动第一个界面Activity One时,它的次序是:

onCreate (ONE) - onStart (ONE) -onResume(ONE)
虽然是第一次启动,也要走一遍这个resume事件。然后,我们点goto跳到第二个Activity Two中(前一个没有关闭),这时走的次序是:

onFreeze(ONE) - onPause(ONE) -onCreate(TWO) - onStart(TWO) - onResume(TWO) - onStop(ONE)

说明,第二个Activity Two在启动前,One会经历一个:冻结、暂停的过程,在启动Two后,One才会被停止?

然后,我们再点back回到第一个界面,这时走的次序是:

onPause(TWO) - onActivityResult(ONE) -onStart(ONE) - onRestart(ONE) - onResume(ONE) - onStop(TWO) - onDestroy(TWO)

说明,返回时,Two没有经历冻结就直接暂停了,在One接收参数,重启后,Two就停止并被销毁了。

最后,我们点一下Exit退出应用,它的次序是:

onPause(ONE) - onStop(ONE) - onDestroy(ONE)

说明如果我们用了finish的话,不会有freeze,但是仍会经历pause - stop才被销毁。
这里有点疑问的是:为什么回来时先是Start才是Restart?可是文档中的图上画的却是先restart再start的啊?不过,后面的表格中的描述好象是正确的,start后面总是跟着resume(如果是第一次)或者restart(如果原来被stop掉了,这种情况会在start与resume中插一个restart)。

下面不跑例子了,看看文档吧。

1.Android用Activity Stack来管理多个Activity,所以呢,同一时刻只会有最顶上的那个Activity是处于active或者running状态。其它的Activity都被压在下面了。

2.如果非活动的Activity仍是可见的(即如果上面压着的是一个非全屏的Activity或透明的Activity),它是处于paused状态的。在系统内存不足的情况下,paused状态的Activity是有可被系统杀掉的。只是不明白,如果它被干掉了,界面上的显示又会变成什么模样?看来下回有必要研究一下这种情况了。
3.几个事件的配对可以比较清楚地理解它们的关系。Create与Destroy配成一对,叫entrie lifetime,在创建时分配资源,则在销毁时释放资源;往上一点还有Start与Stop一对,叫visible lifetime,表达的是可见与非可见这么一个过程;最顶上的就是Resume和Pause这一对了,叫foregroundlifetime,表达的了是否处于激活状态的过程。

4.因此,我们实现的Activity派生类,要重载两个重要的方法:onCreate()进行初始化操作,onPause()保存当前操作的结果。

除了Activity Lifecycle以外,Android还有一个Process Lifecycle的说明:

在内存不足的时候,Android是会主动清理门户的,那它又是如何判断哪个process是可以清掉的呢?文档中也提到了它的重要性排序:

1.最容易被清掉的是empty process,空进程是指那些没有Activity与之绑定,也没有任何应用程序组件(如Services或者IntentReceiver)与之绑定的进程,也就是说在这个process中没有任何activity或者service之类的东西,它们仅仅是作为一个cache,在启动新的Activity时可以提高速度。它们是会被优先清掉的。因此建议,我们的后台操作,最好是作成Service的形式,也就是说应该在Activity中启动一个Service去执行这些操作。

2.接下来就是background activity了,也就是被stop掉了那些activity所处的process,那些不可见的Activity被清掉的确是安全的,系统维持着一个LRU列表,多个处于background的activity都在这里面,系统可以根据LRU列表判断哪些activity是可以被清掉的,以及其中哪一个应该是最先被清掉。不过,文档中提到在这个已被清掉的Activity又被重新创建的时候,它的onCreate会被调用,参数就是onFreeze时的那个Bundle。不过这里有一点不明白的是,难道这个Activity被killed时,Android会帮它保留着这个Bundle吗?

3.然后就轮到service process了,这是一个与Service绑定的进程,由startService方法启动。虽然它们不为用户所见,但一般是在处理一些长时间的操作(例如MP3的播放),系统会保护它,除非真的没有内存可用了。

4.接着又轮到那些visible activity了,或者说visible process。前面也谈到这个情况,被Paused的Activity也是有可能会被系统清掉,不过相对来说,它已经是处于一个比较安全的位置了。

5.最安全应该就是那个foreground activity了,不到迫不得已它是不会被清掉的。这种process不仅包括resume之后的activity,也包括那些onReceiveIntent之后的IntentReceiver实例。

在Android Application的生命周期的讨论中,文档也提到了一些需要注意的事项:因为Android应用程序的生存期并不是由应用本身直接控制的,而是由Android系统平台进行管理的,所以,对于我们开发者而言,需要了解不同的组件Activity、Service和IntentReceiver的生命,切记的是:如果组件的选择不当,很有可能系统会杀掉一个正在进行重要工作的进程。

来源:http://www.sf.org.cn/Android/lumen/20978.html

Android学习笔记(4)-学习Intent的使用

刚看到Intent的时候,我的确有点困惑:从字面上来说,它表示一种意图和目的;从使用上看,它似乎总是用于Activity之间的切换;而从它所在包android.content来看,它似乎与内容有关。所以,我想或许可以这样理解它: Intent类绑定一次操作,它负责携带这次操作所需要的数据以及操作的类型等。

如果是这样的话,是否可以将它与事件处理联想起来?即一个Intent类似于一个Event。从Intent的两个最重要的成员操作类型(Action)和数据(Data)来看,似乎是有道理的。文档中说,Intent的Action的取值主要是一些定义好了的常量,例如PICK_ACTION,VIEW_ACTION,EDIT_ACTION之类的,而Data则是一个ContentURI类型的变量,这一点,我们前面提到过。

而且文档中说Intent分为两大类,显性的(Explicit )和隐性的(Implicit)。在前面的例子中,我们在两个Activity之间跳转时初步使用了Intent类,当时是用setClass来设置 Intent的发起方与接收方,它被称为显性的Intent,而隐性的Intent则不需要用setClass或setComponent来指定事件处理器,利用AndroidMenifest.xml中的配置就可以由平台定位事件的消费者。

一般来说,intent要定位事件的目的地,无外乎需要以下几个信息:

1.种类(category),比如我们常见的 LAUNCHER_CATEGORY 就是表示这是一类应用程序。

2.类型(type),在前面的例子中没用过,表示数据的类型,这是隐性Intent定位目标的重要依据。

3.组件(component),前面的例子中用的是setClass,不过也可以用setComponent来设置intent跳转的前后两个类实例。

4.附加数据(extras),在ContentURI之外还可以附加一些信息,它是Bundle类型的对象。
Implicit Intent的使用相对有点麻烦,我们来做一个例子。首先,我们需要增加一个类:HelloThreeProvider,它必须实现于ConentProvider接口,所以代码如下:

public class HelloThreeProvider extends ContentProvider ...{

    public boolean onCreate() ...{
        return true;
    }
    
    public int delete(ContentURI url, String where, String[] whereArgs) ...{
        return 0;
    }
    public ContentURI insert(ContentURI url, ContentValues initialValues)...{
        return url;
    }
    public Cursor query(ContentURI url, String[] projection, String selection,
            String[] selectionArgs, String groupBy, String having, String sort) ...{
        return null;
    }

    public int update(ContentURI url, ContentValues values, String where, String[] whereArgs) ...{
        return 0;
    }
    
    public String getType(ContentURI url) ...{
        return "vnd.sharetop.hello.three/vnd.hello.three";
    }

}

这里面有一堆方法要实现,因为它们都是ContentProvider中的abstract方法,但是今天的例子中它们多半没有什么用处,只是一个getType方法我们让它不管什么url都返回一个表示Intent所携带的数据类型是我们定义的一个长字串:vnd.sharetop.hello.three/vnd.hello.three。
然后,在AndroidMenifest.xml中我们将上面这个HelloThreeProvider类加入应用程序:

<application android:icon="@drawable/icon">
        <provider class="HelloThreeProvider" android:authorities="cn.sharetop.android.hello" />                
        <activity class="HelloThree" android:label="@string/app_name">
            <intent-filter>
                <action android:value="android.intent.action.MAIN" />
                <category android:value="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity class="HelloThreeB" android:label="bbb">
        <intent-filter>
             <action android:value="android.intent.action.VIEW" />
             <category android:value="android.intent.category.DEFAULT" />
             <type android:value="vnd.sharetop.hello.three/vnd.hello.three" />
        </intent-filter>
        </activity>
    </application>

相对于前面的例子,主要修改了HelloThreeB的配置,包括增加了一个<category>标签表示这是一个一般性的activity而已。增加了<action>标签,定义它负责处理VIEW_ACTION类型的操作。增加了<type>标签给出一个数据类型的定义串vnd.sharetop.hello.three/vnd.hello.three。最主要的是在<application>下增加的那个<provider>标签,有个authorities属性,我们给的值是cn.sharetop.android.hello,待一会我们再说它的用处。

最后就是修改以前的跳转代码如下:

Intent intent = new Intent();
  intent.setData(new ContentURI("content://cn.sharetop.android.hello/one"));
  intent.setAction(intent.VIEW_ACTION);
  startActivity(intent);

现在我们的setData里的东西可与以前不一样的,是吧?注意到它的格式了吗?content://是个协议头,固定这样写就行了。然后就是那个authorities中定义的串了,再后面就是我们自定义的东西了,我这里很简单的写个one,其它还可以更长一点,如one/101之类的。它负责去关联上那个provider类。另外,增加了setAction的调用设置操作为VIEW_ACTION,与Menifest中的<action>又挂上了。Android平台负责根据Intent的Data信息中的authorities,找到ContentProvider,然后getType,用type和intent中的Action两个信息,再找到可以处理这个intent的消费者。

OK,编译运行。

其实,如果是在一个应用内部,这种隐性的intent实在有点别扭,个人觉得,这种松藕合的实现方法,只适用于那些较大的系统或者多个不同的应用之间的调用,可手机上又有什么“较大”的系统呢?无非是可以与不同来源的多个应用之间方便地互操作而已,那么会是什么样的场景呢?比如,给QQ好友发送gmail邮件,用GoogleMap查找QQ好友所在的位置?看上去挺不错的。

关于这个ContentProvider,其实还有话说,它主要是的那些看似数据库操作的方法我们都没真正去实现呢。不过今天就到这里了,等下回再去研究吧。

来源:http://www.sf.org.cn/Android/lumen/20979.html

 

Android学习笔记(5)-关于ListActivity的简单体验

今天学习点轻松的内容吧,看看android.app包里的几个类。首先是这个在平台自的例子中被广泛使用的ListActivity。这个类其实就是一个含有一个ListView组件的Activity类。也就是说,如果我们直接在一个普通的Activity中自己加一个ListView也是完全可以取代这个ListActivity的,只是它更方便而已,方便到什么程度呢?来做个例子瞧瞧。

public class HelloTwoB extends ListActivity...{    
    public void onCreate(Bundle icicle) ...{
        super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.mainb);        
        List<String> items = fillArray();        
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,R.layout.list_row,items);
               
        this.setListAdapter(adapter);
    }
    private List<String> fillArray()    ...{
        List<String> items = new ArrayList<String>();
        items.add("日曜日");
        items.add("月曜日");
        items.add("火曜日");
        items.add("水曜日");
        items.add("木曜日");
        items.add("金曜日");
        items.add("土曜日");
        return items;
    }
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id)
    ...{
        TextView txt = (TextView)this.findViewById(R.id.text);
        txt.setText("あすは "+l.getSelectedItem().toString()+"です。");
    }
}

的确可以简单到只需准备一个List对象并借助Adapter就可以构造出一个列表。重载onListItemClick方法可以响应选择事件,利用第一个参数可以访问到这个ListView实例以得到选中的条目信息。这里有一点要说明的,就是如果更简单的话,其实连那个setContentView都可以不要了,Android也会自动帮我们构造出一个全屏的列表。但是本例中我们需要一个TextView来显示选中的条目,所以我们需要一个layout.mainb描述一下这个列表窗口。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView id="@+id/text"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text=""
    />
<ListView id="@id/android:list"
    android:layout_width="fill_parent"
    android:layout_height="0dip"
    android:layout_weight="1"
    android:drawSelectorOnTop="false"
    />
</LinearLayout>

这里需要注意的是那个ListView的ID,是系统自定义的android:list,不是我们随便取的,否则系统会说找不到它想要的listview了。然后,在这个listview之外,我们又增加了一个TextView,用来显示选中的条目。

再来说说这里用到的ArrayAdapter,它的构造函数中第二个参数是一个资源ID,ArrayAdapter的API文档中说是要求用一个包含TextView的layout文件,平台用它来显示每个选择条目的样式,这里的取值是R.layout.list_row,所以,我们还有一个list_row.xml文件来描述这个布局,相当简单。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView id="@+id/item"
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"/>
<TextView id="@+id/item2"
          xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"/>
</LinearLayout>

从ArrayAdapter上溯到BaseAdapter,发现还有几个同源的Adapter也应该可以使用,象SimpleAdapter和CursorAdapter,还是做个例子来实验一下吧。

先看看SimpleAdapter,说是simple却不simple。

首先看看这个fillMaps方法,基本上就明白这个simpleAdapter是怎么回事了,在有些场合它还是挺有用的,可以为每个条目绑定一个值:

private List<HashMap<String, String>> fillMaps()
    ...{
        List<HashMap<String, String>> items = new ArrayList<HashMap<String,String>>();
       
        HashMap<String,String> i = new HashMap<String,String>();
        i.put("name","日曜日");
        i.put("key", "SUN");
        items.add(i);
        HashMap<String,String> i1 = new HashMap<String,String>();
        i1.put("name","月曜日");
        i1.put("key", "MON");
        items.add(i1);
        HashMap<String,String> i2 = new HashMap<String,String>();
        i2.put("name","火曜日");  
        i2.put("key", "TUE");
        items.add(i2);
        HashMap<String,String> i3 = new HashMap<String,String>();
        i3.put("name","水曜日");
        i3.put("key", "WED");
        items.add(i3);
        HashMap<String,String> i4= new HashMap<String,String>();
        i4.put("name","木曜日");
        i4.put("key", "THU");
        items.add(i4);
        HashMap<String,String> i5 = new HashMap<String,String>();
        i5.put("name","金曜日");
        i5.put("key", "FRI");
        items.add(i5);
        HashMap<String,String> i6 = new HashMap<String,String>();
        i6.put("name","土曜日");
        i.put("key", "SAT");
        items.add(i6);

        return items;
    }

然后,在HelloTwoB中的onCreate函数中,修改代码,有几个不同:items的元素是HashMap实例,这是一点变化,然后构造函数除了要求items以外,还要求提供一个string[]来说明用hash表中的哪个字段显示在列表中,而后是一个资源ID的数组。我的代码是这样的:

//SimpleAdapter demo
List<HashMap<String, String>> items = fillMaps();
SimpleAdapter adapter=new SimpleAdapter(this,items,R.layout.list_row,new String[]...{"name"},new int[]...{R.id.item});

编译跑一下可以看到结果了,是吧?只是显示的文字不太对,再改一下:

protected void onListItemClick(ListView l, View v, int position, long id)
    ...{
        TextView txt = (TextView)this.findViewById(R.id.text);
        txt.setText("あすは "+((HashMap)l.obtainItem(position)).get("key").toString()+"です。");
    }

这样就好多了,其实一般情况下我们都是用ListView中的obtainItem取得当前选中的条目,然后转成List中的对应类型来使用的。

上面的例子中只显示name对应的值,其实你也可以试一下这样:

SimpleAdapter adapter=new SimpleAdapter(this,items,R.layout.list_row,new String[]...{"name","key"},new int[]...{R.id.item,R.id.item2});

看看是什么效果。

再看看那个CursorAdapter吧,它的列表中元素要求是Cursor,这东西与DB有关,不过最简单的DB就是通讯簿。先从Contacts.People入手吧,同样修改代码:

//CursorAdapter demo

Cursor mCursor = this.getContentResolver().query(Contacts.People.CONTENT_URI, null, null, null, null);
SimpleCursorAdapter adapter=new SimpleCursorAdapter(this,R.layout.list_row,mCursor,new String[]...{Contacts.People.NAME},new int[]...{R.id.item});

因为单纯的CursorAdapter是抽象类,所以我用的是它的子类SimpleCursorAdapter,很好理解,先用ContentResolver查询通讯簿得到一个游标,然后告诉SimpleCursorAdapter要用其中的People.NAME作为显示项来构造出一个adapter即可。

现在的onListItemClick也不一样了,如下:

    protected void onListItemClick(ListView l, View v, int position, long id)
    ...{
        TextView txt = (TextView)this.findViewById(R.id.text);
        Cursor c = (Cursor)l.obtainItem(position);
        txt.setText("SEL = "+c.getString(c.getColumnIndex(Contacts.People.NUMBER)));
    }

这里同样是先用obtainItem取到游标,然后用从记录中取出想要的字段显示即可。在做这个例子时,因为权限的问题我们还得修改一下AndroidManifest.xml文件,让我们的应用可以访问到通讯簿:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.sharetop.android.hello.two">
    <uses-permission id="android.permission.READ_CONTACTS" />
    <application android:icon="@drawable/icon">
 ... ...

来源:http://www.sf.org.cn/Android/lumen/20980.html

Android学习笔记(6)—关于Dialog的简单体验

继续android.app中的几个类的学习,今天的内容是那几个Dialog的体验。

注意到android.app包下除了Dialog(可用于制作复杂的对话框)以外,还包括了几个系统定义好的对话框类,如DatePickerDialog、TimePickerDialog及AlertDialog。

其中AlertDialog我上回用过一次,基本上就那样子了,今天看看另外两个对话框的使用吧。

首先是DatePickerDialog类,修改代码如下:

public class HelloTwoC extends Activity implements OnClickListener, OnDateSetListener ...{
 public HelloTwoC() ...{
   super();
 }
 public void onCreate(Bundle icicle) ...{
     super.onCreate(icicle);
     setTheme(android.R.style.Theme_Dark);
     setContentView(R.layout.mainc);
        
     Button btn = (Button)findViewById(R.id.date);
     btn.setOnClickListener(this);        
 }
 @Override
 public void onClick(View v) ...{
   Calendar d = Calendar.getInstance(Locale.CHINA);
   d.setTime(new Date());
   DatePickerDialog dlg=new DatePickerDialog(this,this,d.get(Calendar.YEAR),d.get(Calendar.MONTH),d.get(Calendar.DAY_OF_MONTH),d.get(Calendar.DAY_OF_WEEK));
   dlg.show(); 
 }
 @Override
 public void dateSet(DatePicker dp, int y, int m, int d) ...{
  TextView txt = (TextView)findViewById(R.id.text);
  txt.setText(Integer.toString(y)+"-"+Integer.toString(m)+"-"+Integer.toString(d));
 }
}

很简单的,无非是需要一个OnDateSetListener接口的实现而已,在它里面的dateSet方法中就可以得到选择的日期了。而TimePickerDialog与DatePickerDialog使用如出一辙,就不多说了。

看看另一个ProgressDialog的用法吧,这个类与AlertDialog一样包含了多个static的方法,所以使用起来是非常方便的。比如说,如果我们需要用它来表示一个长时间的操作,很简单的用一句话就可以了:

ProgressDialog.show(this,null, "operation running...",true,true);

URL:http://www.sf.org.cn/Android/lumen/20981.html

Android学习笔记(7)—关于Service和Notification的体验

大略地看了一下android.app下的Service类,觉得它与Activity非常相似,只是要注意几个地方:

1.生命周期,Service的从onCreate()->onStart(int,Bundle)->onDestroy()显得更为简单。但是它的onStart是带参数的,第一个ID可用来标识这个service,第二个参数显示是用来传递数据的了。比较Activity,传递数据的Bundle是在onCreate就带进入的。

2.Service的启动由Context.startService开始,其实Activity或者Service都是Context的派生类。结束于Context.stopService()或者它自己的stopSelf()。

3.Service还有一个与Activity不一样的是它可以由另一个Context去绑定一个已存在的Service。就是这个方法Context.bindService(),被绑定的Service要求是已经onCreate了但可以没有onStart。在Service类中有个抽象方法getBinder()可以得到这个IBinder对象。关于这方面的细节,以后再看,这里只做个记录罢。

4.与Service有关的还有一个安全的问题,可以在AndroidManifest.xml中用<uses-permission>标签来声明一个Service的访问权限,关于Android的安全问题也留待以后再解决吧。

我一直相信一种水到渠成的学习方法,先从最简单的东西入手,就不会觉得学习很枯燥了。

下面来做个例子。

修改AndroidManifest.xml文件,增加一个Activity和一个Service:

        <activity class=".HelloTwoD" android:label="hello_two_d">
        </activity>
        <service class=".HelloTwoDService" />

HelloTwoD.java的代码比较简单,如下:

public class HelloTwoD extends Activity implements OnClickListener
...{ 
 public HelloTwoD()
    ...{
  super();
    }
 public void onCreate(Bundle icicle) ...{
        super.onCreate(icicle);
        setTheme(android.R.style.Theme_Dark);
        setContentView(R.layout.maind);
        
        Button btn = (Button)findViewById(R.id.btnTest);
        btn.setOnClickListener(this);
    }
 
 @Override
 public void onClick(View arg0) ...{
  // 用一个显式的Intent来启动服务 
  Intent i = new Intent();
  i.setClass(this, HelloTwoDService.class);
  //带上我的名字
  Bundle b= new Bundle();
  b.putString("name", "sharetop");
  this.startService(i,b);
 }
 
}

当然要启动这个HelloTwoD,也需要在我最初的那个HelloTwo中加一点代码(我就不罗嗦了)。再看看那个HelloTwoDService是如何实现的:

public class HelloTwoDService extends Service ...{
 public Timer timer;
 public final String TAG="HelloTwoDService_TAG";
 public void onCreate() ...{
        super.onCreate();
        
        Log.d(TAG,"onCreate");
        
        timer = new Timer(true);

    }
 @Override
 public IBinder getBinder() ...{
  // TODO Auto-generated method stub
  return null;
 }
 public void onStart(int startId, Bundle arg)
 ...{ 
  //看看startId是什么内容 
  if(arg!=null)
   Log.d(TAG,"onStart "+Integer.valueOf(startId).toString()+" from "+arg.getString("name"));
  else
   Log.d(TAG,"onStart with null Bundle");
  
        timer.schedule(new TimerTask()...{
         public void run()...{
          //表示一下我的存在
          Log.d(TAG,"say from a timer.");
          //停掉自己这个服务
          HelloTwoDService.this.stopSelf();
         }         
        },5000);
 }
 public void onDestroy()
 ...{
  Log.d(TAG,"onDestroy");
 }
}

这里我用一个定时器timer来延时5秒钟显示消息,否则立即就显示出来觉得不象一个后台服务了。用日志输出那个onStart中的startId看看,原来只是一个标识而已。

下面来个简单的NotificationManager吧,看了看API文档,觉得最简单地恐怕就是那个NotificationManager.notifyWithText()了,修改上面的run方法如下:

        timer.schedule(new TimerTask()...{
         public void run()...{

          NotificationManager manager=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);
          manager.notifyWithText(1001, "わたしはSHARETOPです。", NotificationManager.LENGTH_LONG, null);

          HelloTwoDService.this.stopSelf();
         }         
        },5000);

再试试看效果。太简单了,Notification主要是用于后台服务用来通知前台,所以,Android提供了三类不同的通知方式,notifyWithText可以简单地显示一个字串,而notifyWithView稍复杂点,可以有一个view来构造这个显示信息框,而最灵活的就是那个notify(int id, Notification notification)了,参数notification是类Notification的实例。

修改一下刚才的那个run方法,如下:

        timer.schedule(new TimerTask()...{
         public void run()...{
          
          NotificationManager manager=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);          
          Notification nf = new Notification(R.drawable.icon,"这是信息的详细描述",null,"信息的标题",null);
          manager.notify(0,nf);          
          
          HelloTwoDService.this.stopSelf();
         }         
        },5000);

这里创建一个Notification的实例nf,构造函数的第一个参数是那个显示在状态栏(也就是Android手机上面的那一条显示信号强度、电池电量等信息的位置)的图标。后面可以有

一个标题和点击以后的详细信息,这是字串形式,还可以有一个Intent用来表示点击后可以发生一个跳转行为。

URL:http://www.sf.org.cn/Android/lumen/20982.html

Android学习笔记(8) — GridView与Image View

简单一点吧,就瞧瞧那个Grid的效果,Android提供了一个Grid View,不过从Epidemic中看来,它似乎与PC上的GRID差别还是挺大的,更像那个IconView的感觉。不知道Android中如何实现表格界面?虽然在移动终端上,表格一般不会有谁使用,大家似乎更倾向于使用List View,而Android对于ListView则有更简单的实现List Activity。

废话不说,还是自己写几句代码来实验一下。

<Grid View id="@+id/grid"
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent"
    android:padding="10dip"
    android:verticalSpacing="10"    
    android:horizontalSpacing="10"
    android:numColumns="auto_fit"
    android:columnWidth="60"
    android:stretchMode="columnWidth"    
    android:gravity="center"
    />     

 从描述文件中的这些属性来看,与表格非常类似,除了padding和spacing以外,它还多了那个gravity,这里是center表示单元格中的内容居中放,在类Grid View中也提供了方法setGravity(int)来实现这个效果。

接着,我们沿用以前那个fillMaps方法来构造SimpleAdapter,以前将这个adapter赋给List Activity,现在同样的Adapter,却是赋给了Grid View,效果又会是怎样呢?

List<HashMap<String, String>> items = fillMaps();

Grid View grd=(Grid View)this.findViewById(R.id.grid);
SimpleAdapter adapter=new SimpleAdapter(this,items,R.layout.list_row,new String[]...{"name"},new int[]...{R.id.item});
grd.setAdapter(adapter);

我觉得Grid View并不象表格,倒更象Icon View,下面试试用图像作为Grid View的内容。现在,不能用简单Adapter了,得自己弄一个ImageAdapter,就让它衍生于BaseAdapter类吧。

public class ImageAdapter extends BaseAdapter ...{
        //这是资源ID的数组
        private Integer[] mThumbIds = ...{
                R.drawable.a,R.drawable.b,R.drawable.c,
                R.drawable.d,R.drawable.e,R.drawable.f,
                R.drawable.g,R.drawable.h,R.drawable.i
                };

        public ImageAdapter(Context c) ...{
            mContext = c;
        }

        public int getCount() ...{
            return mThumbIds.length;
        }

        public Object getItem(int position) ...{
            return position;
        }

        public long getItemId(int position) ...{
            return position;
        }

        public View getView(int position, View convertView, ViewGroup parent) ...{
            ImageView i = new Image View(mContext);
            //设置图像源于资源ID。
      i.setImageResource(mThumbIds[position]);
            i.setAdjustViewBounds(true);            
            i.setBackground(android.R.drawable.picture_frame);

            return i;
        }

        private Context mContext;

    }

很简单,只要重载几个方法就可以了,关键是那个getView方法,它负责构建出每个单元格中的对象实例。这里我们构造的是一个Image View实例。

然后就是同样的将这个Adapter赋给Grid View即可,大家可以看看效果,注意在做这个例子前,先放几个小图片到res/drawable目录下,buildproject一下就可以得到那个R.drawable.a了(这里的a是图像文件名,如a.png)。

在getView方法中我们使用了ImageView类,这又是一个widget。除了上面用到的几个方法以外,还有以下几个方法值得注意:

与图像来源有关的方法,我们只用了资源文件的方式。

    //不同的图像来源
public void setImageBitmap(Bitmap bm) 
public void setImageDrawable(Drawable drawable) 
public void setImageResource(int resid) 
public void setImageURI(ContentURI uri) 

图像效果的操作。

//颜色过滤
public void setColorFilter(int color, Mode mode) 
//矩阵变换
public void setImageMatrix(Matrix matrix) 
//透明度
public void setAlpha(int alpha) 

具体的使用可以参考API,动手试一下就差不多了。

URL:http://www.sf.org.cn/Android/lumen/20983.html

Android学习笔记(9)-开始做一个数独游戏[上]

不想再写Hello123了,今天开始做一个数独小游戏,因为这个游戏比较简单应该容易上手,就作为我学习Android之后的第一个程序比较合适。

初步的设计是只有一个界面(如下图),然后用绿色字体表示题目中有的固定的数字,黄色字体显示玩家输入的数字,而红色字体则是程序判断输入错误后的显示。另外模式分为三种:普通写入、标记(玩家用蓝色小方块标记当前单元格可以输入的数字)、排除模式(玩家指定数字,游戏自动判断一下这个数字肯定不能输入的单元格,将它反相显示出来)。

准备工作就是做一张背景图(棋盘)和三套数字小图片(红、绿、黄)即可。

首先建立工程sudo,程序主体类 MainActivity以后,再修改一下那个main.xml文件,去掉TextView标签即可。因为我们会自己定义一个View,所以不再需要它了。程序不大,所以不打算做过多的类,下面把几个类的分工描述一下:
1、MainActivity,主体类,负责处理键盘事件和维护一个题库。
2、MainView,显示类,负责维护并显示当前棋局,它的核心在于它的onDraw函数。
3、GridCell和Question两个实体类,分别描述了棋盘单元格的信息和题目信息。
4、Helper类,助手类,封装一些函数,如读写记录、自动解题函数等。在MainActivity中的onCreate中,加几句话就可以让游戏全屏显示了。如下:
 

setTheme(android.R.style.Theme_Dark);
requestWindowFeature(Window.FEATURE_NO_TITLE);         getWindow().setFlags(WindowManager.LayoutParams.NO_STATUS_BAR_FLAG,WindowManager.LayoutParams.NO_STATUS_BAR_FLAG);  
    

主要来看看MainView类的代码吧,它的onDraw负责显示当前棋局,涉及到的API主要是android.graphics中的Canvas和Paint。一是显示图片的方法,因为图片来源于资源,所以显示它的代码如:

Bitmap bmp = BitmapFactory.decodeResource(this.getResources(),R.drawable.grid);
canvas.drawBitmap(bmp, 0, 0, null);

这是显示背景,如果是数字呢,如何将数字1与R.drawable.a1资源关联呢?
private int[] thumbNormal=new int[]...{0,
        R.drawable.a1,R.drawable.a2,R.drawable.a3,R.drawable.a4,R.drawable.a5,    
        R.drawable.a6,R.drawable.a7,R.drawable.a8,R.drawable.a9
    };

然后就简单地加载即可了。

Bitmap b = BitmapFactory.decodeResource(this.getResources(),this.thumbNormal[this.grid[i].value]);
canvas.drawBitmap(b, xx, yy, null);

二是显示文本的方法,刚才显示图像的drawBitmap中最后一个参数直接给了null,因为我们实在没有什么效果需要给图像的,但是文本则不同,我们用Paint来控制文本的样式。

Paint paintText=new Paint();
paintText.setFlags(Paint.ANTI_ALIAS_FLAG);
paintText.setColor(Color.WHITE);
... ...
canvas.drawText(Long.toString(this.ti.code), xx, yy, paintText);


三是画一下框的方法,同样是用Paint来做的。
 

Paint paintRect = new Paint();
paintRect.setColor(Color.RED);
paintRect.setStrokeWidth(2);
paintRect.setStyle(Style.STROKE);
         Rect r=new Rect();
r.left=this.curCol*CELL_WIDTH+GRID_X;
r.top=this.curRow*CELL_WIDTH+GRID_Y;
r.bottom=r.top+CELL_WIDTH;
r.right=r.left+CELL_WIDTH;
          canvas.drawRect(r, paintRect);

如果不setStyle为Style.STROKE,则缺省为填充模式。

四是反相显示的方法,更简单了,就是一句话了:

Paint paintHint=new Paint();
paintHint.setXfermode(new PixelXorXfermode(Color.WHITE));

URL:http://www.sf.org.cn/Android/lumen/20984.html

Android学习笔记(10)-开始做一个数独游戏[中]

继续,今天讨论的是记录文件的读写。因为原来在Brew平台上实现的数独将题库是一个二进制文件,所以在Android就直接拿那个文件来用了。

 计划实现两个函数,先是LoadTiList(),加载题库,先装题库文件放在资源里,然后从资源里加载它作为一个DataInputStream即可。代码也没几行,如下:

     public static boolean LoadTiList(MainActivity me) ...{
        DataInputStream in = null;
        try
        ...{
            in = new DataInputStream(me.getResources().openRawResource(R.raw.ti));
                      byte[] bufC4=new byte[4];
            byte[] bufC81=new byte[81];
                      //总个数            
            in.read(bufC4,0,4);
            int len=((int)bufC4[3]<<24)+((int)bufC4[2]<<16)+((int)bufC4[1]<<8)+(int)bufC4[0];                        for(int i=0;i<len;i++)
            ...{
                Question ti = new Question();
                                
                //代码
                in.read(bufC4,0,4);
                ti.code=(long) (((long)bufC4[3]<<24)+((long)bufC4[2]<<16)+((long)bufC4[1]<<8)+(long)bufC4[0]);
                //时间
                in.read(bufC4,0,4);                
                SharedPreferences sp = me.getPreferences(Context.MODE_WORLD_READABLE);
                ti.time=sp.getLong(Long.toString(ti.code), 0);
                //数据
                in.read(bufC81,0,81);
                for(int j=0;j<81;j++)ti.data[j]=bufC81[j];
                me.tiList.add(ti);
            }
            in.close();
        }
        catch(Exception ex)...{
            return false;
        }
        finally...{
            try...{in.close();}catch(Exception e)...{}
        }
        return true;
    }

这里最麻烦的是因为java里没有unsigned类型,所以会溢出,比较郁闷,这个没有解决,只能是生成题库文件里注意一下了,不能与brew平台共用那个题库文件了。

二是保存记录,在brew平台我直接用一个文件搞定,读写它,但是android不能这样了,因为ti.dat是从资源中加载的,所以只能是静态的,不可修改,那记录只能放入preferences中了,代码如下:

    public static boolean SaveTiList(MainActivity me)
    ...{
        try
        ...{
            SharedPreferences sp=me.getPreferences(Context.MODE_WORLD_WRITEABLE);
            Question ti = me.gridView.ti;
            sp.edit().putLong(Long.toString(ti.code),ti.time);
            sp.edit().commit();

        }
        catch(Exception ex)...{            
            return false;
        }
        return true;
    }

SharePreferences可以按key-value来保存记录,所以key用题目的code,则value就是解它所用的时间了。

Android不能直接访问app目录下的文件,所以不能够象brew那样将数据文件放在程序目录下供它读写,而在Activity中提供的两个函数 openFileOutput和openFileInput,虽可用来读写文件,但是总是不太方便。

另外,用SQLite其实也不方便,因为手机中弄这些东西,事实上用处不大。

URL: http://www.sf.org.cn/Android/lumen/20985.html

Android学习笔记(11)-开始做一个数独游戏[下]

继续,最后再讨论一下定时器的实现。

本来很简单的一件事,直接用java.util.timer应该就够用了,但是发现在它的task中无法去invalidate我们的MainView,很郁闷。这一点的处理说明Android还是相对线程安全的。

折腾良久,明白了非得再做一个Handler,才能在线程中操作界面元素。所以,代码比brew复杂了一点。

先还是用Timer和TimerTask来做,如下:

public TimerHandler timerHandler;

public Timer timer;
public MyTimerTask    task;
... ...
timer=new Timer(true);
task=new MyTimerTask(this);
... ...

那个MyTimerTask是MainActivity的一个内嵌类,实现如下:

    private class MyTimerTask extends TimerTask
    ...{
        private MainActivity me;
        private int a=0;
              public MyTimerTask(MainActivity p)...{
            me=p;
        }
        public void run()...{
            me.gridView.time++;                
            Log.d("MyTask",Integer.toString(me.gridView.time));    
            timerHandler.sendEmptyMessage(0);            
        }
    }    

这里做两件事,一是将gridView中的time加一,二是发送一个消息通知timerHandler。原来我在这里直接让MainView去刷新屏幕,发现不行,所以就改成这样处理了。

然后就是如何实现TimerHandler类的,也不复杂,就是让它去刷新一下屏幕即可。

public class TimerHandler extends Handler ...{
    private MainView me;
    public TimerHandler(MainView m)...{
        me=m;
    }
        @Override
    public void handleMessage(Message msg) ...{
        Log.d("Ti",msg.toString());
        me.invalidate();
    }
}

如此一来,就顺了。

在MainView中的onDraw,根据当前的time值显示成00:00:00的格式即可。

另外,发现Android的模拟器运算速度不如BREW的模拟器,相当的慢。

URL: http://www.sf.org.cn/Android/lumen/20986.html

Android学习笔记(12)-开始做一个数独游戏[补充]

再补充一点吧,如果需要给游戏加上背景音乐,其实也是非常容易的事情。因为Android提供了一个MediaPlayer类可以方便的播放音乐文件。

android.media.MediaPlayer类没有构造函数,一般是用它的静态方法create生成实例,简单地告诉它音乐文件的资源ID即可(支持mp3/wav/midi等)。

首先,我们得建立一个Service,就叫MediaService吧,它的代码如下:

Android/UploadFiles_8448/200804/20080419152842539.gif"align=top>Android/UploadFiles_8448/200804/20080419152843671.gif"align=top>public class MediaService extends Service implements MediaPlayer.OnErrorListener,MediaPlayer.OnCompletionListener,MediaPlayer.OnPreparedListener ...{
      ... ...    
    private MediaPlayer player;
       @Override
    protected void onDestroy() ...{
        // TODO Auto-generated method stub
        super.onDestroy();
        if(player!=null)...{
            player.stop();
            player.release();
        }
    }
    @Override
    protected void onStart(int startId, Bundle arguments) ...{
        // TODO Auto-generated method stub
        Log.d("Media","onStart");
                   player=MediaPlayer.create(this.getApplication(),R.raw.tonhua);
        if(player!=null)...{
            player.setAudioStreamType(AudioSystem.STREAM_MUSIC);
                     player.setOnCompletionListener(this);
            player.setOnPreparedListener(this);
            player.setOnErrorListener(this);
            
            player.prepareAsync();
        }
    }

    @Override
    public void onCompletion(MediaPlayer arg0) ...{
        // TODO Auto-generated method stub
        Log.d("Media","finished.");
    }
    @Override
    public void onPrepared(MediaPlayer arg0) ...{
        // TODO Auto-generated method stub
        Log.d("Media","prepared.");
        player.start();
    }
        @Override
    public void onError(MediaPlayer arg0,int what, int extra) ...{
                        Log.d("Media","onError");
        player.stop();
    }
}

这个服务主要就是用一个MediaPlayer去播放资源中的tonghua(一首MP3音乐)。次序一般是先create出这个实例,然后prepare一下(如果是文件直接prepare,如果是流则最好异步parepareAsync),接着就可以start了,同步可以直接start,异步则必须放到onPrepared中再start。

在MainActivity中启动这个服务即可。

mediaServiceIntent= new Intent();
mediaServiceIntent.setClass(this, MediaService.class);
      this.startService(mediaServiceIntent, new Bundle());

当前,在Activity停止时也别忘了将这个Service停掉,而在Service停止时关掉MediaPlayer。

在模拟器上试了,效果不是太好,声音有点断断续续,不知道是不是我的解码器的问题(Vista系统)。

URL: http://www.sf.org.cn/Android/lumen/20987.html

 

消息机制,异步和多线程

有了framework后,我们不用面对赤裸裸的OS API,做一些重复而繁杂的事情。但天下没有免费的午餐,我们还是需要学会高效正确的使用不同的framework,很多处理某一特定问题的手法在不同的framework中,用起来都会有所不同的。

在Android中,下层是Linux的核,但上层的java做的framework把这一切封装的密不透风。以消息处理为例,在MFC中,我们可以用PreTranslateMessage等东东自由处理消息,在C#中,Anders Hejlsberg老大说了,他为我们通向底层开了一扇“救生窗”,但很遗憾,在Android中,这扇窗户也被关闭了(至少我现在没发现...)。

在Android中,你想处理一些消息(比如:Keydown之类的...),你必须寻找Activity为你提供的一些重载函数(比如 onKeyDown之类的...)或者是各式各样的listener(比如OnKeyDownListner之类的...)。这样做的好处是显而易见的,越多的自由就会有越多的危险和越多的晦涩,条条框框画好了,用起来省心看起来省脑,这是一个设计良好的framework应该提供的享受。对于我目前的工程而言,我没有什么BT的需求在当前API下做不到的,google的设计ms还是很nice的。

但世界是残酷的,有的时候我们还是必须有机制提供消息的分发和处理的,因为有的工作是不能通过直接调用来同步处理的,同时也不能通过Activity中内嵌的消息分发和接口设定来做到,比如说事件的定时触法,异步的循环事件的处理,高耗时的工作等等。在Android中,它提供了一些蛮有意思的方式来做这件事情(不好意思,我见不多识不广,我没见过类似玩法,有见过的提个醒 && 嘴下超生^_^),它有一个android.os.Handler的类,这个类接受一个Looper参数,顾名思义,这是一个封装过的,表征消息循环的类。默认情况下,Handler接受的是当前线程下的消息循环实例,也就是说一个消息循环可以被当前线程中的多个对象来分发,来处理(在UI线程中,系统已经有一个Activity来处理了,你可以再起若干个Handler来处理...)。在实例化一个 handlerInstance之后,你可以通过sendMessage等消息发送机制来发送消息,通过重载handleMessage等函数来分发消息。但是!该handlerInstance能够接受到的消息,只有通过handlerInstance.obtainMessage构造出来的消息(这种说法是不确切的,你也可以手动new一个Message,然后配置成该handlerInstance可以处理的,我没有跟进去分析其识别机制,有兴趣的自己玩吧^_^)。也就是说A, B, C, D都可以来处理同一线程内的消息分发,但各自都只能处理属于自己的那一份消息,这抹杀了B想偷偷进入A领地,越俎代庖做一些非份之事的可能(从理论上看,B还是有可能把消息伪装的和A他们家的一样,我没有尝试挑战一下google的智商,有BT需求的自行研究^_^)。这样做,不但兼顾了灵活性,也确保了安全性,用起来也会简单,我的地盘我做主,不用当心伤及无辜,左拥右抱是一件很开心的事情。。。

很显然,消息发送者不局限于自己线程,否者只能做一些定时,延时之类的事情,岂不十分无趣。在实例化Handler的时候,Looper可以是任意线程的,只要有Handler的指针,任何线程也都可以sendMessage(这种构造方式也很有意思,你可以在A线程里面传B线程的Looper来构造 Handler,也可以在B线程里构造,这给内存管理的方法带来很大的变数...)。但有条规则肯定是不能破坏的,就是非UI线程,是不能触碰UI类的。在不同平台上有很多解决方式(如果你有多的不能再多的兴趣,可以看一下很久很久以前我写的一个,不SB不要钱)。我特意好好跟了一下android中的AsyncQueryHandler类,来了解google官方的解决方案。

AsyncQueryHandler是Handler的子类,文档上说,如果处理ContentProvider相关的内容,不用需要自行定义一套东西,而可以简单的使用async方式。我想指代的就应该是AsyncQueryHandler类。该类是一个典型的模板类,为ContentProvider的增删改查提供了很好的接口,提供了一个解决架构,final了一些方法,置空了一些方法。通过派生,实例化一些方法(不是每个对 ContentProvider的处理多需要全部做增删改查,我想这也是该类默认置空一些方法而不是抽象一些方法的原因),来达到这个目的。在内部,该类隐藏了多线程处理的细节,当你使用时,你会感觉异常便利。以query为例,你可以这么来用:

// 定义一个handler,采用的是匿名类的方式,只处理query,因此只重写了onQueryComplete函数:

queryHandler = newAsyncQueryHandler(this.getContentResolver()){

// 传入的是一个ContentResolver实例,所以必须在OnCreate后实例化该Handler类

@Override

protected void onQueryComplete(int token,Object cookie, Cursor cursor) {
        // 在这里你可以获得一个cursor和你传入的附加的token和cookie。
        // 该方法在当前线程下(如果传入的是默认的Looper话),可以自由设定UI信息

}
};

// 调用时只需要调用startQuery(int token, Object cookie, ContentURI uri, String[]projection, String selection, String[] selectionArgs, String sortOrder)函数即可:

queryHandler.startQuery(token, cookie, uri,projection, selection, selectionArgs, sortBy);

可见,该类的使用是多么简单(其实现可不会很容易,因为我尝试做了一次造车轮的工作*_*),比直接用Handler简单无数倍。但让我倍感孤独的是,不知道是没人做异步的ContentProvider访问,还是这个类使用太过于弱智(这个使用方法可是我摸索了半天的啊,难道我真的如此的弱@_@),抑或是大家都各有高招,从SDK到网上,没有任何关于该类的有点用的说明。而我又恰巧悲伤的发现,这个类其实有很多的问题,比如他吃掉异常,有错误时只是简单的返回null指针(这个其实不能怪他,你可以看看这里...);当你传一个null的ContentResolver进去的时候,没有任何异常,只是莫名其妙的丢弃所有消息,使你陷入苦苦的等待而不知其因;更愤慨的是,他的token传递竟然有Bug(难道还是我使用不对&_&),从startXX传入的token,到了onXXComplete里面一律变成1,而文档上明明写着两个是一个东西(我的解决方法是用cookie做token,这个不会丢*_*)。不过我暂时还没有遗弃它的打算,虽然没人理睬,虽然有一堆问题,虽然我按图索骥造了个新轮子,但为了节省剩下的一些无聊的工作,我决定苟且偷生了。。。

还是习惯性跑题了,其实,我是想通过我对这个类的无数次Debugger跟进,说说它的多线程异步处理的解决策略的。他的基本策略如下:

1. 当你实例化一个AsyncQueryHandler类时(包括其子类...),它会单件构造一个线程(后面会详述...),这个线程里面会构建一个消息循环。

2. 获得该消息循环的指针,用它做参数实例化另一个Handler类,该类为内部类。至此,就有了两个线程,各自有一个Handler来处理消息。

3. 当调用onXXX的时候,在XXX函数内部会将请求封装成一个内部的参数类,将其作为消息的参数,将此消息发送至另一个线程。

4. 在该线程的Handler中,接受该消息,并分析传入的参数,用初始化时传入的ContentResolver进行XXX操作,并返回Cursor或其他返回值。

5. 构造一个消息,将上述返回值以及其他相关内容绑定在该消息上,发送回主线程。

6. 主线程默认的AsyncQueryHandler类的handleMessage方法(可自定义,但由于都是内部类,基本没有意义...)会分析该消息,并转发给对应的onXXXComplete方法。

7. 用户重写的onXXXComplete方法开始工作。

这就是它偷偷摸摸做过的事情,基本还是很好理解的。我唯一好奇的是它的线程管理方式,我猜测他是用的单件模式。第一个AsyncQueryHandler的实例化会导致创建一个线程,从此该线程成为不死老处男,所有的ContentResolver相关的工作,都由该线程统一完成。个人觉得这种解决方式很赞。本来这个线程的生命周期就很难估量,并且,当你有一个ContentProvider的请求的时候,判断你会做更多的类似操作并不过分。就算错了,花费的也只是一个不死的线程(与进程同生死共存亡...),换来的却是简单的生命周期管理和无数次线程生死开销的节约。同时另外一个很重要的问题,他并会涉及到单件中数据同步的问题,每个类都有各自的Handler类,彼此互不干扰,分发可以分别进行。当多个数据请求的时候,在同一个ContentResolver上进行的可能微乎其微,这就避免了堵塞。总而言之,这套解决办法和Android的整体设计算是天作之合了。
所以建议,如果你有什么非ContentProvider操作,却需要异步多线程执行的话,模拟一套,是个不错的策略,当然,具体情况具体分析,生搬硬套是学不好马列主义的。。。

URL: http://www.sf.org.cn/Android/lumen/21075.html

显示控件使用

Android的界面显示同样也是基于控件的。通常是用View(包括ViewGroup)控件配上XML的样式来做的。具体细节不想说了,可以参考 Samples里的ApiDemos/View,和View的Doc,以及Implementinga UI这篇Doc。其他还有很多,感觉算是SDK讲述的最多的内容。

从控件的使用上,和网页的设计类似,尽量用parent_width之类的抽象长度,用Theme来做风格,抽取所有的字串等信息做本地化设计。相关内容参看Implementinga UI就好。

一类比较重要的是数据绑定控件。如果做过ASP.Net会从中看到很多类似的地方。一个支持数据绑定的控件,比如List View。可以通过一个 ListAdapter绑定到一个数据源上。ListAdapter是一个抽象类,主要的实现类包括SimpleAdapter和 SimpleCursorAdapter。前者是绑定一个静态的Array,后者是绑定一个动态的Cursor。Cursor前面说过,是一个指向数据源的随机迭代器,将View绑定到Cursor通常要设置这样几个参数。一个是每一行的样式,称作Row Layout,其实就是一个普通的Layout的XML文件。还有就是一个列和现实控件的对应关系。那个控件显示哪个列的值,这是需要配置的。为了定制一个良好的数据显示控件,最简单你可以定制很PP的Row Layout,复杂一点就是可以重载绑定控件View,或者是适配器ListAdapter。如果是一个数据显示密集的应用,且你对UI有些追求,这个工作估计是必不可少的。

一个主要用于显示数据内容的Activity,可以选择派生自List Activity。它提供了一个具有List View 的Layout,还有simple_list_item_1,simple_list_item_2, two_line_list_item等默认的Row Layout,还有一些比较不错的API,和可供响应选择Item的事件。可以满足你比较基础的需求。如果你觉得只有一个List View的界面太突兀,你可以为这个List Activity指定一个Layout,需要注意的是,你需要提供一个id为@android:id/list的ListView控件,避免Activity在内部偷偷寻找该控件的时候失败。

除了这些要求,做好UI还有注意易用性和效率。快捷键是一个比较不错的选择,在 Activity中调用setDefaultkeyMode(SHORTCUT_DEFAULT_KEYS),可以开启快捷键模式,然后你可以将菜单绑定到指定快捷键上就OK了。个人觉得Tip也是一个比较重要的东西,但目前观察看来,这个东西只能够自己提供了。界面的动态性有时候是不可避免的,比如说菜单就是一个需要经常根据光标位置提供不同的选项。这个东西Android很人道的考虑到了,你可以参看NodeList这个Sample。它采取的应该是一个静态模拟动态的方式,这样有助于提高速度。你也可以利用ViewInflate,动态从一个XML创建一个控件。成本据Doc说很大,不到万不得已不要使用。

URL: http://www.sf.org.cn/Android/lumen/21073.html

Intent消息传递

在前面写Android的ContentProvider时候,可以看到那是基于观察者模式的一个消息传递方法。每一个Cursor、ContentResolver做为一个小的注册中心,相关观察者可以在这个中心注册,更新消息由注册中心分发给各个观察者。而在MFC或Winform中,都会形成一个消息网,让消息在网中流动,被各节点使用、吃掉或者在出口死掉。

相比之下,我个人觉得基于Intent的Android核心消息传递机制是有所不同的。它应该会有一个全局性的注册中心,这个注册中心是隐性的,整个Android系统中就那么一个。所有的消息接收者,都被隐形的注册到这个中心。包括Activity,Service和IntentReceiver。其实说隐形注册是不确切的,所有注册都还是我们手动告诉注册中心的,只是与传统的方式不一样,我们通常不是通过代码,而是通过配置文件来做。在应用的Manifest中,我们会为一些Activity或Service添加上Intent-filter,或在配置文件中添加<receiver></receiver>项。这其实就相当于向系统的注册中心,注册了相关的Intent-filter和receiver(这个事情完全可以通过代码来做,只是这样就失去了修改的灵活性)。

当程序有一个消息希望发出去的时候,它需要将消息封装成一个Intent,并发送。这时候,应该是有一个统一的中心(恩,有可能Android底层实现的时候不是,但简单这样看是没问题的...)接受到这个消息,并对它进行解析、判定消息类型(这个步骤降低了耦合...),然后检查注册了相匹配的filter或receiver,并创建或唤醒接收者,将消息分发给它。这样做有很多好处。虽然这种传递有的时候不如点对点的传递快(这有些需要速度的地方,我们看到Android会通过直接通信来做),但有时候又因为它只经过一跳(姑且这么叫吧...),比复杂的流动又要更快。更重要的是,它耦合性低,在手机平台这种程序组件多变的条件下使用十分适合。并且它可以很容易实现消息的精确或模糊匹配,弹性很大。(我个人曾想在开发一个C++二次平台的时候引入这样的机制,但在C++中,建立一套完整的数据marshal机制不容易,相比之下,用java来做会简单很多...)
恩,废话说了很多,具体讲讲Android中Intent的使用。当你有一个消息需要传递,如果你明确知道你需要哪个Activity或者其他Class来响应的话,你可以指定这个类来接受该消息,这被称为显性发送。你需要将Intent的class属性设置成目标。这种情况很常见,比如startActivity的时候,会清楚当前Activity完了应该是哪个Activity,那就明确的发送这个消息。

但是,有的时候你并不确定你的消息是需要具体哪个类来执行,而只是知道接收者该符合哪些条件。比如你只需要有一个接收者能显示用户所选的数据,而不想制定某个具体的方法,这时候你就需要用到隐形发送(传统上,我们可能会考虑用多态,但显然这种方式更为灵活...)。在Android中,你可以为Intent指定一个action,表示你这个指令需要处理的事情。系统为我们定义了很多Action类型,这些类型使系统与我们通信的语言(比如在Activity里面加一个Main的filter,该activity就会做成该应用的入口点),当然你也可以用于你自己的应用之间的通信(同样当然,也可以自定义...)。强烈建议,在自己程序接收或发出一个系统action的时候,要名副其实。比如你响应一个view动作,做的确实edit的勾当,你发送一个pick消息,其实你想让别人做edit的事,这样都会造成混乱。当然只有Action有时候是不够的,在Android中我们还可以指定catalog信息和type/data信息,比如所有的显示数据的Activity,可能都会响应View action。但很多与我们需要显示的数据类型不一样,可以加一个type信息,明确的指出我们需要显示的数据类型,甚至还可以加上一个catalog信息,指明只有你只有按的是“中键”并发出这样的消息才响应。

从上面可以看出,Android的Intent可以添加上class, action, data/type, catalog等消息,注册中心会根据这些信息帮你找到符合的接收者。其中class是点对点的指示,一旦指明,其他信息都被忽略。Intent中还可以添加key/value的数据,发送方和接收方需要保持统一的key信息和value类型信息,这种数据的marshal在java里做,是不费什么力气的。

Android的Intent发送,可以分成单播和广播两种。广播的接收者是所有注册了的符合条件的IntentReceiver。在单播的情况下,即使有很多符合条件的接收者,也只要有一个出来处理这个消息就好(恩,个人看法,没找到确切条款或抉择的算法,本来想实验一下,没来得及...),这样的情况很容易理解,当你需要修改某个数据的时候,你肯定不会希望有十个编辑器轮流让你来处理。当广播不是这样,一个receiver没有办法阻止其他receiver进行对广播事件的处理。这种情况也很容易理解,比如时钟改变了,闹钟、备忘录等很多程序都需要分别进行处理。在自己的程序的使用中,应该分清楚区别,合理的使用。

URL: http://www.sf.org.cn/Android/lumen/21072.html

ContentProvider数据模型概述

Android的数据(包括files, database等...)都是属于应用程序自身,其他程序无法直接进行操作。因此,为了使其他程序能够操作数据,在Android中,可以通过做成 ContentProvider提供数据操作的接口。其实对本应用而言,也可以将底层数据封装成ContentProvider,这样可以有效的屏蔽底层操作的细节,并且是程序保持良好的扩展性和开放性。

ContentProvider,顾名思义,就是数据内容的供应者。在Android中它是一个数据源,屏蔽了具体底层数据源的细节,在ContentProvider内部你可以用Android支持的任何手段进行数据的存储和操作,可能比较常用的方式是基于Android的SQLite数据库(恩,文档中和示例代码都是以此为例)。无论如何,ContentProvider是一个重要的数据源,可以预见无论是使用和定制ContentProvider都会很多。于是花了点时间仔细看了看。

数据库操作

从我目前掌握的知识来看,SQLite比较轻量(没有存储过程之类的繁杂手段),用起来也比较简单。实例化一个SQLiteDatabase类对象,通过它的APIs可以搞定大部分的操作。从sample中看,Android中对db的使用有一种比较简单的模式,即派生一个 ContentProviderDatabaseHelper类来进行SQLiteDatabase对象实例的获取工作。基本上, ContentProviderDatabaseHelper类扮演了一个singleton的角色,提供单一的实例化入口点,并屏蔽了数据库创建、打开升级等细节。在ContentProvider中只需要调用ContentProviderDatabaseHelper的openDatabase方法获取SQLiteDatabase的实例就好,而不需要进行数据库状态的判断。

URI

像进行数据库操作需要用SQL一样,对ContentProivder进行增删改查等操作都是通过一种特定模式的URI来进行的(ig:content: //provider/item/id),URI的能力与URL类似,具体细节可以查看SDK。建立自己的ContentProvider,只需要派生 ContentProivder类并实现insert, delete, update等抽象函数即可。在这些接口中比较特殊的是getType(uri)。根据传入的uri,该方法按照MIME格式返回一个字符串(==!没听过的诡异格式...)唯一标识该uri的类型。所谓uri的类型,就是描述这个uri所进行的操作的种类,比如content://xx/a与 content://xx/a/1不是一个类型(前者是多值操作,后者是单值),但content://xx/a/1和content://xx/a/2 就会是一个类型(只是id号不同而已)。

在ContentProvider通常都会实例化一个ContentURIPraser来辅助解析和操作传入的URI。你需要事先(在static域内)为该ContentURIPraser建立一个uri的语法树,之后就可以简单调用 ContentURIPraser类的相关方法进行uri类型判断(match方法),获取加载在uri中的参数等操作。但我看来,这只是在使用上简化了相关操作(不然就需要自己做人肉解析了...),但并没有改变类型判定的模式。你依然需要用switch...case...对uri的类型进行判断,并进行相关后续的操作。从模式来看,这样无疑是具有强烈的坏味道,类似的switch...case...代码要出现N此,每次一个 ContentProvider做uri类型的增减都会需要遍历修改每一个switch...case...,当然,如果你使用模式(策略模式...)进行改造对手机程序来说无疑是崩溃似的(类型膨胀,效率降低...),所以,只能是忍一忍了(恩,还好不会扩散到别的类中,维护性上不会有杀人性的麻烦...)。

增删改查

ContentProvider 和所有数据源一样,向外提供增删改查操作接口,这些都是基于uri的指令。进行insert操作的时候,你需要传入一个uri和 ContentValues。uri的作用基本就限于指明增减条目的类型(从数据库层面来看就是table名),ContentValues是一个 key/value表的封装,提供方便的API进行插入数据类型和数据值的设置和获取。在数据库层面上来看,这应该是column name与value的对应。但为了屏蔽ContentProvider用户涉及到具体数据库的细节,在Android的示例中,用了一个小小的模式。它为每一个表建一个基于BaseColumn类的派生类(其实完全可以不派生自BaseColumn,特别当你的表不基于默认的自动id做主键的时候),这个类通常包括一个描述该表的ContentURI对象和形如 public static final TITLE = "title"这样的column到类数据的对应。从改变上角度来看,你可以修改column的名字而不需要更改用户上层代码,增加了灵活性。 insert方法如果成功会返回一个uri,该uri会在原有的uri基础上增加有一个rowid。对于为什么使用row id而不是key id我想破了脑袋。到最后,我发现我傻了,因为ContentProvider不一定需要使用数据库,使用数据库对应的表也可以没有主键,只有row id,才能在任何底层介质下做索引标识。

但,基于row id在删除和修改操作是会造成一定的混乱。删除和修改操作类似。删除操作需要传入一个uri,一个where字串,一组where的参数(做条件判定...),而修改操作会多一个ContentValues做更新值。着两个操作的uri都支持在末尾添加一个row id。于是混乱就出现了。当在where参数中指明了key id,而在uri中提供了row id,并且row id和keyid所指函数不一致的时候,你听谁的?示例代码中的做法是完全无视row id(无语...),如此野蛮的方式我估计也只能在示例中出现,在实际中该如何用,恩,我也不知道。幸运的是,我看了下上层对 ContentProvider的删除操作,其实都不会直接进行,而是通过调用Cursor的delete方法进行,在这前提下,我想Cursor会处理好这些东西吧。

最后一个操作是查询操作,可以想见,查询的参数是最多的,包括uri和一组条件参数。条件参数类型和标准的sql类似,包括 sort, projection 之类的。从这些参数到sql语句的生成,可以寻求QueryBuilder类的帮助,它提供了一组操作接口,简化了参数到sql的生成工作,哪怕你不懂 sql都完全没有问题(这话说的我自己都觉得有点悬...)。查询返回一个Cursor。Cursor是一个支持随机读写的指针,不仅如此,它还提供了方便的删除和修改的API,是上层对ContentProvider进行操作一个重要对象,需要仔细掌握(Cursor还可以绑定到view上,直接送显,并与用户进行交互,真是程序越往上,封装越好,工作越机械没有复杂性了...)。

数据模型

在与界面打交道的Cursor、ContentResolver等数据操作层中,大量采用观察者模式建立数据层与显示层的联系。一个显示层的视图,可以做成某一种观察者注册到Cursor或ContentResolver等数据中间层中,在实现底层ContentProvider中,我们需要特别注意在对数据进行修改操作(包括增删改...)后,调用相应类型的notify函数,帮助表层对象进行刷新(还有一种刷新方式是从一个view发起的)。可以看到 Android的整体数据显示框架有点像MVC的方式(贫瘠了...叫不出名)。Cursor、ContentResolver相当于控制层,数据层和显示层的交互通过控制层来掌管,而且控制层很稳定不需要特别定制,通常工作只在定制数据层和显示层空间,还是比较方便和清晰的。

一个设计问题

现在有个设计问题,比如我要扩充一个已有的ContentProvider(第三方提供),我是建立一个ContentProvider,只保留第三方 ContentProvider的key信息,并为其添加更多的信息,在表层维护这两个ContentProvider的联系好;还是建议一个 ContentProvider,以第三方的ContentProvider做一部分底层数据源,像表层提供一个ContentProvider好。

前者无疑在实现上简单一些,如果第三方改变,灵活性也更好,只是需要仔细维护表层的相关代码。后者实现上需要付出大量的苦力劳动,当表层使用会简单多了。我举棋不定,期待你的意见。。。

自定义ContentProvider的语义

ContentProvider中,最重要的就是query操作。query根据输入返回一个符合条件的Cursor。这就可能出现以下几种情况:1. 查询成功,包含几个正确的结果;2. 查询失败,没有符合的结果;3. 输入错误, 触发了某个异常;4. 没能查询到结果,但无法确定是输入错误还是查询失败。第一种情况是我们最需要的,当然是需要正确维系的,而最后一种情况在大部分应用中应该不会出现(但在我的应用中会的*_#),而第二种第三种是比较常见的。

经过我的测试,系统的ContentProvider维持这样的语义:如果是情况2,返回正常的Cursor,并且,其count为0,相当于empty cursor;如果是情况3,不抛出任何异常,返回null的Cursor。这样的话明明白白写出来是很好理解的,但由于没有官方的文档说明,在自定义的时候经常会误用。比如在某些情况下,用null表征查询失败,用抛出异常来描述错误的输入。

返回empty cursor,如果是通过databasecursor自然会有db帮你维护,但是如果返回ArrayListCursor,MergeCursor或其他自定义的Cursor,就需要自己维系了。ArrayListCursor可以通过new ArrayListCursor(Columns, new ArrayList(){})来提供。其中Columns一定不为null。MergeCursor不能以new MergeCursor(new Cursor[]{})来创建,而需要通过newMergeCursor(new Cursor[]{aEmptyCursor, ...}来维系(其实很好理解,我呆了...)。自定义的Cursor也一定要提供生成empty cursor的方式。

如果将ContentProvider作为一个单独的module来理解,不通过异常而是通过null来返回MS是有好处的。在module的出口吃掉所有异常,虽然不能提供足够的信息(异常信息全部写入日志),但可能会使上层使用更简单。但在Android中,我并没有感觉到这一点。作为ContentProvider的上层函数,ListActivity.managedQuery、ListView.setListAdapter等,根本不能处理一个null的Cursor,在ListView中这会触发一个异常。更无语的是,当你把一个null Cursor设置为manage的后。它不会立即抛异常,而是在OnFreeze等生命周期函数的时候,因无法处理null Cursor而抛出一个异常。这使得你根本无法在当地catch该异常,换句话,ListActivity的manageCursor根本是个无法使用的函数。你必须用getContext().query()获得Cursor,然后判定该Cursor是否null,在进行startManagingCursor进行绑定。这远不如直接用异常进行错误路径的处理来的统一和方便。
当然,有些东西我们是不能改变的,只能去适应。对于自定义的cursor, ContentProvider,最重要的,是在无人造错误输入的情况下返回empty cursor,而不是null。至于使用null响应还是异常响应上,我个人觉得还是和系统同步为好,虽然别扭,但至少统一不容易有歧义。

此外,ContentProvider还有很多细致的语义。比如返回的Cursor需要绑定一个URI,以便自动响应更新。自定义的更新需要支持deleteRow等操作语义等等。

PS:而上层的ListView,更是陷阱重重。首先绑定到ListView的Cursor必须有_id项,否则会有异常抛出。如果做过.net的开发,这一点是可以想到的,但是,这种问题应该在文档中写明。另外,在ListView中,如果你不绑定一个数据源,你一定不能在layout中添加涉及内容的属性。比如android:height="wrap_content",这会在onMeasure的时候抛出异常。


原创粉丝点击