Android学习笔记。
android学习笔记Android 学习笔记(1)-永远不变的Hello World
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下的资源描述被自动修
改)。
android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Hello World" />
这个文件很好读,一个描述了这是一个线性排列的布局,android:orientation=vertical表示所有组件将纵向排布。而经典的Hello World是用一个
TextView来展示的。
由此,我们知道,Android 的程序从一个Activity 派生出来,并且从它的onCreate 开始启动;Android 里要显示的组件用XML 文件描述而不用
在代码中硬编码(这是一个好的习惯,我们应该从一开始就坚持下去);
2.让Button 来说Hello World
上面的例子是ADT 自动生成的代码,似乎与我们一点关系也没有。那我们来改一下代码,因为在windows 平台上的Helloworld 经常是由一个按钮
触发的,所以,我们想第二个Helloworld 应该是这样的:加一个按钮和文本输入框,单击按钮后在原来的TextView 后面加上输入框中输入的文
字。
第一步是,增加一个Button 和一个EditText,与TextView 一样,它们也在main.xml 里描述一下:
android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="" /> 这里有两个地方要注意:id=@+id/go,这表示需要一个唯一的UID 来作为Button 的ID,它的引用名是go。还有一个是android:text=@string/go 表示这个按钮的文本不是直接写有main.xml 里了,而是来源于另一个资源描述文件strings.xml 里,本例中的strings.xml 如下: 然后,在代码里(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。 android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Hello 中国" /> 除了一个“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文件了,如下: package="cn.sharetop.android.hello.three"> 很简单,就是加一个标签而已,新标签的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 void startSubActivity(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(int resultCode, String data) resultCode 是返回代码,同样用来标识一个返回类型,而data 则是它要返回的参数。 在原来的Activity中的事件处理回调函数onActivityResult,会被系统调用,从它的参数里可以得到返回值。 函数原型为:protected void onActivityResult(int requestCode, 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这一对了,叫foreground lifetime,表达的了是否处于激活状态的过程。 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 类加入应用程序: 相对于前面的例子,主要修改了HelloThreeB 的配置,包括增加了一个 标签,定义它负责处理VIEW_ACTION 类型的操作。增加了 最主要的是在 说它的用处。 最后就是修改以前的跳转代码如下: 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 中的 的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 ArrayAdapter this.setListAdapter(adapter); } private List List 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 描述一下这个列表窗口。 android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="" /> android:layout_width="fill_parent" android:layout_height="0dip" android:layout_weight="1" android:drawSelectorOnTop="false" /> 这里需要注意的是那个ListView 的ID,是系统自定义的android:list,不是我们随便取的,否则系统会说找不到它想要的listview 了。然 后,在这个listview 之外,我们又增加了一个TextView,用来显示选中的条目。 再来说说这里用到的ArrayAdapter,它的构造函数中第二个参数是一个资源ID,ArrayAdapter 的API 文档中说是要求用一个包含TextView 的layout 文件,平台用它来显示每个选择条目的样式,这里的取值是R.layout.list_row,所以,我们还有一个list_row.xml 文件来描述这个布 局,相当简单。 android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"/> xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"/> 从ArrayAdapter 上溯到BaseAdapter,发现还有几个同源的Adapter 也应该可以使用,象SimpleAdapter 和CursorAdapter,还是做个例子 来实验一下吧。 先看看SimpleAdapter,说是simple 却不simple。 首先看看这个fillMaps 方法,基本上就明白这个simpleAdapter 是怎么回事了,在有些场合它还是挺有用的,可以为每个条目绑定一个值: private List ...{ List HashMap i.put("name","日曜日"); i.put("key", "SUN"); items.add(i); HashMap i1.put("name","月曜日"); i1.put("key", "MON"); items.add(i1); HashMap i2.put("name","火曜日"); i2.put("key", "TUE"); items.add(i2); HashMap i3.put("name","水曜日"); i3.put("key", "WED"); items.add(i3); HashMap i4.put("name","木曜日"); i4.put("key", "THU"); items.add(i4); HashMap i5.put("name","金曜日"); i5.put("key", "FRI"); items.add(i5); HashMap i6.put("name","土曜日"); i.put("key", "SAT"); items.add(i6); return items; } 然后,在HelloTwoB 中的onCreate 函数中,修改代码,有几个不同:items 的元素是HashMap 实例,这是一点变化,然后构造函数除了要求 items 以外,还要求提供一个string[]来说明用hash 表中的哪个字段显示在列表中,而后是一个资源ID 的数组。我的代码是这样的: //SimpleAdapter demo List 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 文件,让我们的应用可以访问到通讯簿: package="cn.sharetop.android.hello.two"> ... ... 来源: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_MONT H),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中用 于Android的安全问题也留待以后再解决吧。 我一直相信一种水到渠成的学习方法,先从最简单的东西入手,就不会觉得学习很枯燥了。 下面来做个例子。 修改AndroidManifest.xml文件,增加一个Activity和一个Service: 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 与ImageView 简单一点吧,就瞧瞧那个Grid的效果,Android提供了一个GridView,不过从APIDemo中看来,它似乎与PC上的GRID差别还是挺大的,更像那 个IconView的感觉。不知道Android中如何实现表格界面?虽然在移动终端上,表格一般不会有谁使用,大家似乎更倾向于使用ListView,而Android 对于ListView则有更简单的实现ListActivity。 废话不说,还是自己写几句代码来实验一下。 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 表示单元格中的内 容居中放,在类GridView 中也提供了方法setGravity(int)来实现这个效果。 接着,我们沿用以前那个fillMaps 方法来构造SimpleAdapter,以前将这个adapter 赋给ListActivity,现在同样的Adapter,却是赋给了 GridView,效果又会是怎样呢? List GridView grd=(GridView)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); 我觉得GridView 并不象表格,倒更象IconView,下面试试用图像作为GridView 的内容。现在,不能用简单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 ImageView(mContext); //设置图像源于资源ID。 i.setImageResource(mThumbIds[position]); i.setAdjustViewBounds(true); i.setBackground(android.R.drawable.picture_frame); return i; } private Context mContext; } 很简单,只要重载几个方法就可以了,关键是那个getView 方法,它负责构建出每个单元格中的对象实例。这里我们构造的是一个ImageView 实例。 然后就是同样的将这个Adapter 赋给GridView 即可,大家可以看看效果,注意在做这个例子前,先放几个小图片到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 { 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,Me diaPlayer.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 = new AsyncQueryHandler(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,以及Implementing a UI这篇Doc。其他还有很多,感觉算是SDK讲述的最多的内容。 从控件的使用上,和网页的设计类似,尽量用parent_width 之类的抽象长度,用Theme 来做风格,抽取所有的字串等信息做本地化设计。相 关内容参看Implementing a UI 就好。 一类比较重要的是数据绑定控件。如果做过ASP.Net 会从中看到很多类似的地方。一个支持数据绑定的控件,比如ListView。可以通过一个 ListAdapter 绑定到一个数据源上。ListAdapter 是一个抽象类,主要的实现类包括SimpleAdapter 和 SimpleCursorAdapter。前者是绑定一个静 态的Array,后者是绑定一个动态的Cursor。Cursor 前面说过,是一个指向数据源的随机迭代器,将View 绑定到Cursor 通常要设置这样几个参 数。一个是每一行的样式,称作Row Layout,其实就是一个普通的Layout 的XML 文件。还有就是一个列和现实控件的对应关系。那个控件显示哪 个列的值,这是需要配置的。为了定制一个良好的数据显示控件,最简单你可以定制很PP 的Row Layout,复杂一点就是可以重载绑定控件View, 或者是适配器ListAdapter。如果是一个数据显示密集的应用,且你对UI 有些追求,这个工作估计是必不可少的。 一个主要用于显示数据内容的Activity,可以选择派生自ListActivity。它提供了一个具有ListView 的Layout,还有simple_list_item_1, simple_list_item_2, two_line_list_item 等默认的Row Layout,还有一些比较不错的API,和可供响应选择Item 的事件。可以满足你比较基础 的需求。如果你觉得只有一个ListView 的界面太突兀,你可以为这个ListActivity 指定一个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,或在配置文件中添加 注册中心,注册了相关的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 基础上增加有一个row id。对于为什么使用row id 而 不是key id 我想破了脑袋。到最后,我发现我傻了,因为ContentProvider 不一定需要使用数据库,使用数据库对应的表也可以没有主键,只有 row id,才能在任何底层介质下做索引标识。 但,基于row id 在删除和修改操作是会造成一定的混乱。删除和修改操作类似。删除操作需要传入一个uri,一个where 字串,一组where 的参数(做条件判定...),而修改操作会多一个ContentValues 做更新值。着两个操作的uri 都支持在末尾添加一个row id。于是混乱就出现了。 当在where 参数中指明了key id,而在uri 中提供了row id,并且row id 和key id 所指函数不一致的时候,你听谁的?示例代码中的做法是完 全无视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[]{})来创建,而需要通过new MergeCursor(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 的时候抛出异常。