【网学网提醒】:网学会员为大家收集整理了Android(经典实例)提供大家参考,希望对大家有所帮助!
Android
学习笔记-让我们快速上手吧
Google的AndroidSDK发布也有一段时间了,一直想研究一下却苦于找不到时间。利用这个周未,开始强迫自己再次进入学习状态,原因很简单:我看好开放的gPhone。
SDK的
下载与安装并不复杂,网上也有不少同学已经进入状态了,我就不再重复了吧。
今天主要讨论的,还是永远不变的话题:HelloWorld.
1.最简单的HelloWorld
安装了SDK后,直接生成一个AndroidProject,一句代码不用写,就能跑出一个最简单的HelloWorld例程。我们看一下它的代码:
publicvoidonCreate(Bundleicicle){
super.onCreate(icicle);
setTheme(android.R.style.Theme_Dark);
setContentView(R.layout.main);
}
看上去实在很简单,只有两句话而已。关键在这个R.layout.main上,凭直觉,这应该是定义的资源。的确,在R.java中只是定义了一个staticint而已,真正的资源描述在res/layout/main.xml文件里(注意:这里的R.java不要手工编辑,每次buildproject时它都会根据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="HelloWorld"
/>
这个文件很好读,一个描述了这是一个线性排列的布局,android:orientation=vertical表示所有组件将纵向排布。而经典的HelloWorld是用一个TextView来展示的。
由此,我们知道,Android的程序从一个Activity派生出来,并且从它的onCreate开始启动;Android里要显示的组件用XML文件描述而不用在代码中硬编码(这是一个好的习惯,我们应该从一开始就坚持下去);
2.让Button来说HelloWorld
上面的例子是ADT自动生成的代码,似乎与我们一点关系也没有。那我们来改一下代码,因为在windows平台上的Helloworld经常是由一个按钮触发的,所以,我们想第二个Helloworld应该是这样的:加一个按钮和文本输入框,单击按钮后在原来的TextView后面加上输入框中输入的文字。
第一步是,增加一个Button和一个EditText,与TextView一样,它们也在main.xml里描述一下:
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text=""
/>
android:layout_width="wrap_content"android:layout_height="wrap_content"
android:text="@string/go">
这里有两个地方要注意:id=@+id/go,这表示需要一个唯一的UID来作为Button的ID,它的引
用名是go。还有一个是android:text=@string/go表示这个按钮的文本不是直接写有main.xml里了,而是来源于另一个资源描述文件strings.xml里,本例中的strings.xml如下:
helloTwo
提示
你好,中国
确定
浏览
然后,在代码里(onCreate函数中)我们加上以下代码(简单起见,用了嵌套类):
Buttonbtn=(Button)findViewById(R.id.go);
btn.setOnClickListener(newView.OnClickListener()
{
publicvoidonClick(Viewv)
{
EditTextedt=(EditText)helloTwo.this.findViewById(R.id.edt);
TextViewtxt=(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.再让菜单SayHello
从API文档中我们看到Activity中有两个函数:onCreateOptionsMenu和onOptionsItemSelected,显示,这个OptionsMenu就是所谓的上下文菜单(在GPhone的模拟器上,有个键专用于弹出这个菜单)。下面我们就为这个HelloWorld例子加上一个菜单,并且让它可以Sayhello。
这次,我们不涉及到资源的描述文件了,而是直接使用这两个函数来实现,其实代码也很简单,所以,我们再增加一个退出应用的功能(否则每次都是按取消键退出应用显示太不专业了)。
代码如下:
publicbooleanonCreateOptionsMenu(Menumenu)
{
super.onCreateOptionsMenu(menu);
menu.add(0,1,"sayhello");
menu.add(0,2,"exit");
returntrue;
}
publicbooleanonOptionsItemSelected(Itemitem)
{
super.onOptionsItemSelected(item);
intid=item.getId();
switch(id){
case1:
AlertDialog.show(this,getString(R.string.app_name),
getString(R.string.msg_dialog),getString(R.string.ok_dialog),true);
break;
case2:
finish();
break;
}
在CreateOptionsMenu时,我们简单地增加两个菜单项,menu.add(组ID,项ID,显示文本),(注意:这里我直接将文字写在代码里,这并不提倡)。然后,在OptionsItemSelected事件中,我们根据选中的菜单项做相应处理,如果选中1,则弹出
一个对话框显示资源文件中的“你好,中国”,如果选中2则退出应用。
AlertDialog.show是一个静态方法,类似于我们在WIN平台上经常使用的MessageBox一样,很方便的。
来源:sf.org/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中国"
/>
android:layout_width="wrap_content"android:layout_height="wrap_content"
android:text="back">
除了一个“Hello中国”以外,增加一个按钮可以返回前一个界面。然后,在代码中我们要为helloTwo增加两个方法,setViewOneCommand和setViewTwoCommand,分别处理一下在不同界面时,从资源里加载组件并为组件绑定一个事件处理器。
publicvoidsetViewOneCommand()
{
Buttonbtn=(Button)findViewById(R.id.go);
btn.setOnClickListener(newView.OnClickListener()
{
publicvoidonClick(Viewv)
{
helloTwo.this.setContentView(R.layout.second);
helloTwo.this.setViewTwoCommand();
}
});
ButtonbtnExit=(Button)findViewById(R.id.exit);
btnExit.setOnClickListener(newView.OnClickListener(){
publicvoidonClick(Viewv){
helloTwo.this.finish();
}
});
}
publicvoidsetViewTwoCommand()
{
ButtonbtnBack=(Button)findViewById(R.id.go2);
btnBack.setOnClickListener(newView.OnClickListener(){
publicvoidonClick(Viewv){
helloTwo.this.setContentView(R.layout.main);
helloTwo.this.setViewOneCommand();
}
});
}
最后,我们需要在onCreate的时候,也就是启动后的main界面上设置一下按钮事件处理器。新的onCreate方法如下:
publicvoidonCreate(Bundleicicle){
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方法中绑定按钮的事件处理器:
publicvoidonCreate(Bundleicicle){
super.onCreate(icicle);
setTheme(android.R.style.Theme_Dark);
setContentView(R.layout.main);
setViewOneCommand();
}
publicvoidsetViewOneCommand()
{
Buttonbtn=(Button)findViewById(R.id.go);
btn.setOnClickListener(newView.OnClickListener()
{
publicvoidonClick(Viewv)
{
Intentintent=newIntent();
intent.setClass(HelloThree.this,HelloThreeB.class);
startActivity(intent);
finish();
}
});
ButtonbtnExit=(Button)findViewById(R.id.exit);
btnExit.setOnClickListener(newView.OnClickListener(){
publicvoidonClick(Viewv){
HelloThree.this.finish();
}
});
}
这里的跳转功能用Intent来操作,它的最简单用法就是用函数setClass()设置跳转前后两个Activity类的实例,然后调用Activity自己的startActivity(intent)即可。最后一句finish()表示将当前Activity关掉(如果不关掉会如何?你可以自己试一下看效果,事实上有时我们是不需要关掉当前Activity的)。
然后,我们同
样弄一个Activity类HelloThreeB,代码与前面的差不多,只是将setClass的两个参数反一下,这样就可以简单地实现在两个Activity界面中来回切换的功能了。
3. 如果我想在两个Activity之间进行数据交换,怎么办?
前例中的startActivity()只有一个参数,如果需要向新打开的Activity传递参数,我们得换一个函数了,Android提供了startSubActivity(Intent,int)这个函数来实现这个功能。
函数原型为:publicvoidstartSubActivity(Intentintent,intrequestCode)
这里的requestCode用来标识某一个调用,一般由我们定义一个常量。
如何把参数传过去呢?Intent类在提供setClass()函数的同时也提供了一个setData()函数。
函数原型为:publicIntentsetData(ContentURIdata)
参数类型是ContentURI,它的详细内容下回再分析,现在就把它当成一个String类型来用吧。
参数带到新的Activity后,同样用Activity.getIntent()函数可以得到当前过来的Intent对象,然后用getData()就取到参数了。
把参数带回来的方法是Activity.setResult(),它有几个形式,现在先看最简单的一个吧。
函数原型是:publicfinalvoidsetResult(intresultCode,Stringdata)
resultCode是返回代码,同样用来标识一个返回类型,而data则是它要返回的参数。
在原来的Activity中的事件处理回调函数onActivityResult,会被系统调用,从它的参数里可以得到返回值。
函数原型为:protectedvoidonActivityResult(intrequestCode,intresultCode,Stringdata,Bundleextras)
这里的requestCode就是前面启动新Activity时的带过去的requestCode,而resultCode则关联上了setResult中的resultCode,data是参数,extras也是一个很重要的东西,后面再研究一下它的作用。
下面,我们来看一下代码吧,先看看HelloThree中的代码:
publicvoidsetViewOneCommand()
{
Buttonbtn=(Button)findViewById(R.id.go);
btn.setOnClickListener(newView.OnClickListener()
{
publicvoidonClick(Viewv)
{
try
{
Intentintent=newIntent();
intent.setClass(HelloThree.this,HelloThreeB.class);
intent.setData(newContentURI("One"));
startSubActivity(intent,REQUEST_TYPE_A);
}
catch(Exceptionex){}
}
});
ButtonbtnExit=(Button)findViewById(R.id.exit);
btnExit.setOnClickListener(newView.OnClickListener(){
publicvoidonClick(Viewv){
HelloThree.this.finish();
}
});
}
protectedvoidonActivityResult(intrequestCode,intresultCode,
Stringdata,Bundleextras)
{
if(requestCode==
REQUEST_TYPE_A){
if(resultCode==RESULT_OK){
Log.v(TAG,data);
TextViewtxt=(TextView)findViewById(R.id.txt);
txt.setText(data);
}
}
}
这里的REQUEST_TYPE_A是我们定义的一个常量。在onActivityResult中用它与RESULT_OK一起作为条件判断如何处理返回值,这里只是简单将TextView显示值换成传来的字串。
再来看看另一个HelloThreeB类的实现代码:
privateIntenti;
protectedvoidonCreate(Bundleicicle){
super.onCreate(icicle);
setContentView(R.layout.second);
i=getIntent();
android.util.Log.v(TAG,"onCreate");
Buttonbtn=(Button)findViewById(R.id.go);
btn.setOnClickListener(newView.OnClickListener(){
publicvoidonClick(Viewv){
Stringresult=HelloThreeB.this.i.getData().toString()+"AndTwo";
HelloThreeB.this.setResult(RESULT_OK,result);
finish();
}
});
TextViewv=(TextView)findViewById(R.id.txt);
v.setText("Paramis"+i.getData().toString());
}
在按钮处理事件中,从Intent取出参数,处理一下再用setResult返回给前一个Activity即可。
编译运行即可。
来源:sf.org/Android/lumen/20977.html
Android学习笔记(3)-Activity的生命周期
注意到在Activity的API中有大量的onXXXX形式的函数定义,除了我们前面用到的onCreate以外,还有onStart,onStop以及onPause等等。从字面上看,它们是一些事件回调,那么次序又是如何的呢?其实这种事情,自己做个实验最明白不过了。在做这个实验之前,我们先得找到在Android中的Log是如何输出的。
显然,我们要用的是android.util.log类,这个类相当的简单易用,因为它提供的全是一些静态方法:
Log.v(Stringtag,Stringmsg);//VERBOSE
Log.d(Stringtag,Stringmsg);//DEBUG
Log.i(Stringtag,Stringmsg);//INFO
Log.w(Stringtag,Stringmsg);//WARN
Log.e(Stringtag,Stringmsg);//ERROR
前面的tag是由我们定义的一个标识,一般可以用“类名_方法名“来定义。
输出的LOG信息,如果用Eclipse+ADT开发,在LogCat中就可以看到,否则用adblogcat也行,不过我是从来都依赖于IDE环境的。
好了,现在我们修改前面的HelloThree代码:
publicvoidonStart()
{
super.onStart();
Log.v(TAG,"onStart");
}
publicvoidonStop()
{
super.onStop();
Log.v(TAG,"onStop");
}
publicvoidonResume()
{
super.onResume();
Log.v(TAG,"onResume");
}
publicvoidonRestart()
{
super.onRestart();
Log.v(TAG,"onR
eStart");
}
publicvoidonPause()
{
super.onPause();
Log.v(TAG,"onPause");
}
publicvoidonDestroy()
{
super.onDestroy();
Log.v(TAG,"onDestroy");
}
publicvoidonFreeze(BundleoutState)
{
super.onFreeze(outState);
Log.v(TAG,"onFreeze");
}
在HelloThreeB中也同样增加这样的代码,编译,运行一下,从logcat中分析输出的日志。
在启动第一个界面ActivityOne时,它的次序是:
onCreate(ONE)-onStart(ONE)-onResume(ONE)
虽然是第一次启动,也要走一遍这个resume事件。然后,我们点goto跳到第二个ActivityTwo中(前一个没有关闭),这时走的次序是:
onFreeze(ONE)-onPause(ONE)-onCreate(TWO)-onStart(TWO)-onResume(TWO)-onStop(ONE)
说明,第二个ActivityTwo在启动前,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用ActivityStack来管理多个Activity,所以呢,同一时刻只会有最顶上的那个Activity是处于active或者running状态。其它的Activity都被压在下面了。
2.如果非活动的Activity仍是可见的(即如果上面压着的是一个非全屏的Activity或透明的Activity),它是处于paused状态的。在系统内存不足的情况下,paused状态的Activity是有可被系统杀掉的。只是不明白,如果它被干掉了,界面上的显示又会变成什么模样?看来下回有必要研究一下这种情况了。
3.几个事件的配对可以比较清楚地理解它们的关系。Create与Destroy配成一对,叫entrielifetime,在创建时分配资源,则在销毁时释放资源;往上一点还有Start与Stop一对,叫visiblelifetime,表达的是可见与非可见这么一个过程;最顶上的就是Resume和Pause这一对了,叫foregroundlifetime,表达的了是否处于激活状态的过程。
4.因此,我们实现的Activity派生类,要重载两个重要的方法:onCreate()进行初始化操
作,onPause()保存当前操作的结果。
除了ActivityLifecycle以外,Android还有一个ProcessLifecycle的说明:
在内存不足的时候,Android是会主动清理门户的,那它又是如何判断哪个process是可以清掉的呢?文档中也提到了它的重要性排序:
1.最容易被清掉的是emptyprocess,空进程是指那些没有Activity与之绑定,也没有任何应用程序组件(如Services或者IntentReceiver)与之绑定的进程,也就是说在这个process中没有任何activity或者service之类的东西,它们仅仅是作为一个cache,在启动新的Activity时可以提高速度。它们是会被优先清掉的。因此建议,我们的后台操作,最好是作成Service的形式,也就是说应该在Activity中启动一个Service去执行这些操作。
2.接下来就是backgroundactivity了,也就是被stop掉了那些activity所处的process,那些不可见的Activity被清掉的确是安全的,系统维持着一个LRU列表,多个处于background的activity都在这里面,系统可以根据LRU列表判断哪些activity是可以被清掉的,以及其中哪一个应该是最先被清掉。不过,文档中提到在这个已被清掉的Activity又被重新创建的时候,它的onCreate会被调用,参数就是onFreeze时的那个Bundle。不过这里有一点不明白的是,难道这个Activity被killed时,Android会帮它保留着这个Bundle吗?
3.然后就轮到serviceprocess了,这是一个与Service绑定的进程,由startService方法启动。虽然它们不为用户所见,但一般是在处理一些长时间的操作(例如MP3的播放),系统会保护它,除非真的没有内存可用了。
4.接着又轮到那些visibleactivity了,或者说visibleprocess。前面也谈到这个情况,被Paused的Activity也是有可能会被系统清掉,不过相对来说,它已经是处于一个比较安全的位置了。
5.最安全应该就是那个foregroundactivity了,不到迫不得已它是不会被清掉的。这种process不仅包括resume之后的activity,也包括那些onReceiveIntent之后的IntentReceiver实例。
在AndroidApplication的生命周期的讨论中,文档也提到了一些需要注意的事项:因为Android应用程序的生存期并不是由应用本身直接控制的,而是由Android系统平台进行管理的,所以,对于我们开发者而言,需要了解不同的组件Activity、Service和IntentReceiver的生命,切记的是:如果组件的选择不当,很有可能系统会杀掉一个正在进行重要工作的进程。
来源:sf.org/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类型的对象。
ImplicitIntent的使用相对有点麻烦,我们来做一个例子。首先,我们需要增加一个类:HelloThreeProvider,它必须实现于ConentProvider接口,所以代码如下:
publicclassHelloThreeProviderextendsContentProvider{
publicbooleanonCreate(){
returntrue;
}
publicintdelete(ContentURIurl,Stringwhere,String[]whereArgs){
return0;
}
publicContentURIinsert(ContentURIurl,ContentValuesinitialValues){
returnurl;
}
publicCursorquery(ContentURIurl,String[]projection,Stringselection,
String[]selectionArgs,StringgroupBy,Stringhaving,Stringsort){
returnnull;
}
publicintupdate(ContentURIurl,ContentValuesvalues,Stringwhere,String[]whereArgs){
return0;
}
publicStringgetType(ContentURIurl){
return"vnd.sharetop.hello.three/vnd.hello.three";
}
}
这里面有一堆方法要实现,因为它们都是ContentProvider中的abstract方法,但是今天的例子中它们多半没有什么用处,只是一个getType方法我们让它不管什么url都返回一个表示Intent所携带的数据类型是我们定义的一个长字串:vnd.sharetop.hello.three/vnd.hello.three。
然后,在AndroidMenifest.xml中我们将上面这个HelloThreeProvider类加
入应用程序:
相对于前面的例子,主要修改了HelloThreeB的配置,包括增加了一个标签表示这是一个一般性的activity而已。增加了标签,定义它负责处理VIEW_ACTION类型的操作。增加了标签给出一个数据类型的定义串vnd.sharetop.hello.three/vnd.hello.three。最主要的是在下增加的那个标签,有个authorities属性,我们给的值是cn.sharetop.android.hello,待一会我们再说它的用处。
最后就是修改以前的跳转代码如下:
Intentintent=newIntent();
intent.setData(newContentURI("content://cn.sharetop.android.hello/one"));
intent.setAction(intent.VIEW_ACTION);
startActivity(intent);
现在我们的setData里的东西可与以前不一样的,是吧?注意到它的格式了吗?content://是个协议头,固定这样写就行了。然后就是那个authorities中定义的串了,再后面就是我们自定义的东西了,我这里很简单的写个one,其它还可以更长一点,如one/101之类的。它负责去关联上那个provider类。另外,增加了setAction的调用设置操作为VIEW_ACTION,与Menifest中的又挂上了。Android平台负责根据Intent的Data信息中的authorities,找到ContentProvider,然后getType,用type和intent中的Action两个信息,再找到可以处理这个intent的消费者。
OK,编译运行。
其实,如果是在一个应用内部,这种隐性的intent实在有点别扭,个人觉得,这种松藕合的实现方法,只适用于那些较大的系统或者多个不同的应用之间的调用,可手机上又有什么“较大”的系统呢?无非是可以与不同来源的多个应用之间方便地互操作而已,那么会是什么样的场景呢?比如,给QQ好友发送gmail邮件,用GoogleMap查找QQ好友所在的位置?看上去挺不错的。
关于这个ContentProvider,其实还有话说,它主要是的那些看似数据库操作的方法我们都没真正去实现呢。不过今天就到这里了,等下回再去研究吧。
来源:www
.sf.org/Android/lumen/20979.html
Android学习笔记(5)-关于ListActivity的简单体验
今天学习点轻松的内容吧,看看android.app包里的几个类。首先是这个在平台自的例子中被广泛使用的ListActivity。这个类其实就是一个含有一个ListView组件的Activity类。也就是说,如果我们直接在一个普通的Activity中自己加一个ListView也是完全可以取代这个ListActivity的,只是它更方便而已,方便到什么程度呢?来做个例子瞧瞧。
publicclassHelloTwoBextendsListActivity...{
publicvoidonCreate(Bundleicicle)...{
super.onCreate(icicle);
setTheme(android.R.style.Theme_Dark);
setContentView(R.layout.mainb);
Listitems=fillArray();
ArrayAdapteradapter=newArrayAdapter(this,R.layout.list_row,items);
this.setListAdapter(adapter);
}
privateListfillArray()...{
Listitems=newArrayList();
items.add("日曜日");
items.add("月曜日");
items.add("火曜日");
items.add("水曜日");
items.add("木曜日");
items.add("金曜日");
items.add("土曜日");
returnitems;
}
@Override
protectedvoidonListItemClick(ListViewl,Viewv,intposition,longid)
...{
TextViewtxt=(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="schemas.android/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
xmlns:android="schemas.android/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
从ArrayAdapter上溯到BaseAdapter,发现还有几个同源的Adapter也应该可以使用,象SimpleAdapter和CursorAdapter,还是做个例子来实验一下吧。
先看看SimpleAdapter,说是simple却不simple。
首先看看这个fillMaps方法,基本上就明白这个simpleAdapter是怎么回事了,在有些场合它还是挺有用的,可以为每个条目绑定一个值:
privateList>fillMaps()
...{
List>items=newArrayList>();
HashMapi=newHashMap();
i.put("name","日曜日");
i.put("key","SUN");
items.add(i);
HashMapi1=newHashMap();
i1.put("name","月曜日");
i1.put("key","MON");
items.add(i1);
HashMapi2=newHashMap();
i2.put("name","火曜日");
i2.put("key","TUE");
items.add(i2);
HashMapi3=newHashMap();
i3.put("name","水曜日");
i3.put("key","WED");
items.add(i3);
HashMapi4=newHashMap();
i4.put("name","木曜日");
i4.put("key","THU");
items.add(i4);
HashMapi5=newHashMap();
i5.put("name","金曜日");
i5.put("key","FRI");
items.add(i5);
HashMapi6=newHashMap();
i6.put("name","土曜日");
i.put("key","SAT");
items.add(i6);
returnitems;
}
然后,在HelloTwoB中的onCreate函数中,修改代码,有几个不同:items的元素是HashMap实例,这是一点变化,然后构造函数除了要求items以外,还要求提供一个string[]来说明用hash表中的哪
个字段显示在列表中,而后是一个资源ID的数组。我的代码是这样的:
//SimpleAdapterdemo
List>items=fillMaps();
SimpleAdapteradapter=newSimpleAdapter(this,items,R.layout.list_row,newString[]{"name"},newint[]{R.id.item});
编译跑一下可以看到结果了,是吧?只是显示的文字不太对,再改一下:
protectedvoidonListItemClick(ListViewl,Viewv,intposition,longid)
{
TextViewtxt=(TextView)this.findViewById(R.id.text);
txt.setText("あすは"+((HashMap)l.obtainItem(position)).get("key").toString()+"です。");
}
这样就好多了,其实一般情况下我们都是用ListView中的obtainItem取得当前选中的条目,然后转成List中的对应类型来使用的。
上面的例子中只显示name对应的值,其实你也可以试一下这样:
SimpleAdapteradapter=newSimpleAdapter(this,items,R.layout.list_row,newString[]{"name","key"},newint[]{R.id.item,R.id.item2});
看看是什么效果。
再看看那个CursorAdapter吧,它的列表中元素要求是Cursor,这东西与DB有关,不过最简单的DB就是通讯簿。先从Contacts.People入手吧,同样修改代码:
//CursorAdapterdemo
CursormCursor=this.getContentResolver().query(Contacts.People.CONTENT_URI,null,null,null,null);
SimpleCursorAdapteradapter=newSimpleCursorAdapter(this,R.layout.list_row,mCursor,newString[]{Contacts.People.NAME},newint[]{R.id.item});
因为单纯的CursorAdapter是抽象类,所以我用的是它的子类SimpleCursorAdapter,很好理解,先用ContentResolver查询通讯簿得到一个游标,然后告诉SimpleCursorAdapter要用其中的People.NAME作为显示项来构造出一个adapter即可。
现在的onListItemClick也不一样了,如下:
protectedvoidonListItemClick(ListViewl,Viewv,intposition,longid)
{
TextViewtxt=(TextView)this.findViewById(R.id.text);
Cursorc=(Cursor)l.obtainItem(position);
txt.setText("SEL="+c.getString(c.getColumnIndex(Contacts.People.NUMBER)));
}
这里同样是先用obtainItem取到游标,然后用从记录中取出想要的字段显示即可。在做这个例子时,因为权限的问题我们还得修改一下AndroidManifest.xml文件,让我们的应用可以访问到通讯簿:
package="cn.sharetop.android.hello.two">
......
来源:sf.org/Android/lumen/20980.html
Android学习笔记(6)—关于Dialog的简单体验
继续android.app中的几个类的学习,今天的内容是那几个Dialog的体验。
注意到android.app包下除了Dialog(可用于制作复杂的对话框)以外,还包括了几个系统定义好的对话框类,如DatePickerDialog、TimePicker
Dialog及AlertDialog。
其中AlertDialog我上回用过一次,基本上就那样子了,今天看看另外两个对话框的使用吧。
首先是DatePickerDialog类,修改代码如下:
publicclassHelloTwoCextendsActivityimplementsOnClickListener,OnDateSetListener{
publicHelloTwoC(){
super();
}
publicvoidonCreate(Bundleicicle){
super.onCreate(icicle);
setTheme(android.R.style.Theme_Dark);
setContentView(R.layout.mainc);
Buttonbtn=(Button)findViewById(R.id.date);
btn.setOnClickListener(this);
}
@Override
publicvoidonClick(Viewv){
Calendard=Calendar.getInstance(Locale.CHINA);
d.setTime(newDate());
DatePickerDialogdlg=newDatePickerDialog(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
publicvoiddateSet(DatePickerdp,inty,intm,intd){
TextViewtxt=(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,"operationrunning...",true,true);
URL:sf.org/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中用标签来声明一个Service的访问权限,关于Android的安全问题也留待以后再解决吧。
我一直相信一种水到渠成
的学习方法,先从最简单的东西入手,就不会觉得学习很枯燥了。
下面来做个例子。
修改AndroidManifest.xml文件,增加一个Activity和一个Service:
HelloTwoD.java的代码比较简单,如下:
publicclassHelloTwoDextendsActivityimplementsOnClickListener
{
publicHelloTwoD()
{
super();
}
publicvoidonCreate(Bundleicicle){
super.onCreate(icicle);
setTheme(android.R.style.Theme_Dark);
setContentView(R.layout.maind);
Buttonbtn=(Button)findViewById(R.id.btnTest);
btn.setOnClickListener(this);
}
@Override
publicvoidonClick(Viewarg0){
//用一个显式的Intent来启动服务
Intenti=newIntent();
i.setClass(this,HelloTwoDService.class);
//带上我的名字
Bundleb=newBundle();
b.putString("name","sharetop");
this.startService(i,b);
}
}
当然要启动这个HelloTwoD,也需要在我最初的那个HelloTwo中加一点代码(我就不罗嗦了)。再看看那个HelloTwoDService是如何实现的:
publicclassHelloTwoDServiceextendsService{
publicTimertimer;
publicfinalStringTAG="HelloTwoDService_TAG";
publicvoidonCreate(){
super.onCreate();
Log.d(TAG,"onCreate");
timer=newTimer(true);
}
@Override
publicIBindergetBinder(){
//TODOAuto-generatedmethodstub
returnnull;
}
publicvoidonStart(intstartId,Bundlearg)
{
//看看startId是什么内容
if(arg!=null)
Log.d(TAG,"onStart"+Integer.valueOf(startId).toString()+"from"+arg.getString("name"));
else
Log.d(TAG,"onStartwithnullBundle");
timer.schedule(newTimerTask(){
publicvoidrun(){
//表示一下我的存在
Log.d(TAG,"sayfromatimer.");
//停掉自己这个服务
HelloTwoDService.this.stopSelf();
}
},5000);
}
publicvoidonDestroy()
{
Log.d(TAG,"onDestroy");
}
}
这里我用一个定时器timer来延时5秒钟显示消息,否则立即就显示出来觉得不象一个后台服务了。用日志输出那个onStart中的startId看看,原来只是一个标识而已。
下面来个简单的NotificationManager吧,看了看API文档,觉得最简单地恐怕就是那个NotificationManager.notifyWithText()了,修改上面的run方法如下:
timer.schedule(newTimerTask(){
publicvoidrun(){
NotificationManagermanager=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);
manager.notifyWithText(1001,"わたしはSHARETOPです。",NotificationManager.LENGTH_LONG,null);
HelloTwoDService.this.stopSelf();
}
},5000);
再试试看效果。太简单了,Notification主要是用于后台服务用来通知前台,所以,Android提供了三类不同的通知方式,notifyWithText可以简单地显示一个字串,而notifyWithView稍复杂点,可以有一个view来构造这个显示信息框,而最灵活的就是那个notify(intid,Notificationnotification)了,参数notification是类Notification的实例。
修改一下刚才的那个run方法,如下:
timer.schedule(newTimerTask(){
publicvoidrun(){
NotificationManagermanager=(NotificationManager)getSystemService(NOTIFICATION_SERVICE);
Notificationnf=newNotification(R.drawable.icon,"这是信息的详细描述",null,"信息的标题",null);
manager.notify(0,nf);
HelloTwoDService.this.stopSelf();
}
},5000);
这里创建一个Notification的实例nf,构造函数的第一个参数是那个显示在状态栏(也就是Android手机上面的那一条显示信号强度、电池电量等信息的位置)的图标。后面可以有
一个标题和点击以后的详细信息,这是字串形式,还可以有一个Intent用来表示点击后可以发生一个跳转行为。
URL:sf.org/Android/lumen/20982.html
Android学习笔记(8)—GridView与ImageView
简单一点吧,就瞧瞧那个Grid的效果,Android提供了一个GridView,不过从Epidemic中看来,它似乎与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>items=fillMaps();
GridViewgrd=(GridView)this.findViewById(R.id.grid);
SimpleAdapteradapter=newSimpleAdapter(this,items,R.layout.list_row,newString[]{"name"},newint[]{R.id.item});
grd.setAdapter(adapter);
我
觉得GridView并不象表格,倒更象IconView,下面试试用图像作为GridView的内容。现在,不能用简单Adapter了,得自己弄一个ImageAdapter,就让它衍生于BaseAdapter类吧。
publicclassImageAdapterextendsBaseAdapter{
//这是资源ID的数组
privateInteger[]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
};
publicImageAdapter(Contextc){
mContext=c;
}
publicintgetCount(){
returnmThumbIds.length;
}
publicObjectgetItem(intposition){
returnposition;
}
publiclonggetItemId(intposition){
returnposition;
}
publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
ImageViewi=newImageView(mContext);
//设置图像源于资源ID。
i.setImageResource(mThumbIds[position]);
i.setAdjustViewBounds(true);
i.setBackground(android.R.drawable.picture_frame);
returni;
}
privateContextmContext;
}
很简单,只要重载几个方法就可以了,关键是那个getView方法,它负责构建出每个单元格中的对象实例。这里我们构造的是一个ImageView实例。
然后就是同样的将这个Adapter赋给GridView即可,大家可以看看效果,注意在做这个例子前,先放几个小图片到res/drawable目录下,buildproject一下就可以得到那个R.drawable.a了(这里的a是图像文件名,如a.png)。
在getView方法中我们使用了ImageView类,这又是一个widget。除了上面用到的几个方法以外,还有以下几个方法值得注意:
与图像来源有关的方法, ,我们只用了资源文件的方式。
//不同的图像来源
publicvoidsetImageBitmap(Bitmapbm)
publicvoidsetImageDrawable(Drawabledrawable)
publicvoidsetImageResource(intresid)
publicvoidsetImageURI(ContentURIuri)
图像效果的操作。
//颜色过滤
publicvoidsetColorFilter(intcolor,Modemode)
//矩阵变换
publicvoidsetImageMatrix(Matrixmatrix)
//透明度
publicvoidsetAlpha(intalpha)
具体的使用可以参考API,动手试一下就差不多了。
URL:sf.org/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。
一是显示图片的方法,因为图片来源于资源,所以显示它的代码如:
Bitmapbmp=BitmapFactory.decodeResource(this.getResources(),R.drawable.grid);
canvas.drawBitmap(bmp,0,0,null);
这是显示背景,如果是数字呢,如何将数字1与R.drawable.a1资源关联呢?
privateint[]thumbNormal=newint[]{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
};
然后就简单地加载即可了。
Bitmapb=BitmapFactory.decodeResource(this.getResources(),this.thumbNormal[this.grid[i].value]);
canvas.drawBitmap(b,xx,yy,null);
二是显示文本的方法,刚才显示图像的drawBitmap中最后一个参数直接给了null,因为我们实在没有什么效果需要给图像的,但是文本则不同,我们用Paint来控制文本的样式。
PaintpaintText=newPaint();
paintText.setFlags(Paint.ANTI_ALIAS_FLAG);
paintText.setColor(Color.WHITE);
......
canvas.drawText(Long.toString(this.ti.code),xx,yy,paintText);
三是画一下框的方法,同样是用Paint来做的。
PaintpaintRect=newPaint();
paintRect.setColor(Color.RED);
paintRect.setStrokeWidth(2);
paintRect.setStyle(Style.STROKE);
Rectr=newRect();
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,则缺省为填充模式。
四是反相显示的方法,更简单了,就是一句话了:
PaintpaintHint=newPaint();
paintHint.setXfermode(newPixelXorXfermode(Color.WHITE));
URL:sf.org/Android/lumen/20984.html
Android学习笔记(10)-开始做一个数独游戏[中]
继续,今天讨论的是记录文件的读写。因为原来在Brew平台上实现的数独将题库是一个二进制文件,所以在Android就直接拿那个文件来用了。
计划实现两个函数,先是LoadTiList(),加载题库,先装题库文件放在资源里,然后从资源里加载它作为一个DataInputStream即可。代码也没几行,如下:
publicstaticbooleanLoadTiList(MainActivityme){
DataInputStreamin=null;
try
{
in=newDataInputStream(me.getResources().openRawResource(R.raw.ti));
byte[]bufC4=newbyte[4];
byte[]bufC81=newbyte[81];
//总个数
in.read(bufC4,0,4);
intlen=((int)bufC4[3]<<24)+((int)bufC4[2]<<16)+((int)bufC4[1]<<8)+(int)bufC4[0];
for(inti=0;i {
Questionti=newQuestion();
//代码
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);
SharedPreferencessp=me.getPreferences(Context.MODE_WORLD_READABLE);
ti.time=sp.getLong(Long.toString(ti.code),0);
//数据
in.read(bufC81,0,81);
for(intj=0;j<81;j++)ti.data[j]=bufC81[j];
me.tiList.add(ti);
}
in.close();
}
catch(Exceptionex){
returnfalse;
}
finally{
try{in.close();}catch(Exceptione){}
}
returntrue;
}
这里最麻烦的是因为java里没有unsigned类型,所以会溢出,比较郁闷,这个没有解决,只能是生成题库文件里注意一下了,不能与brew平台共用那个题库文件了。
二是保存记录,在brew平台我直接用一个文件搞定,读写它,但是android不能这样了,因为ti.dat是从资源中加载的,所以只能是静态的,不可修改,那记录只能放入preferences中了,代码如下:
publicstaticbooleanSaveTiList(MainActivityme)
{
try
{
SharedPreferencessp=me.getPreferences(Context.MODE_WORLD_WRITEABLE);
Questionti=me.gridView.ti;
sp.edit().putLong(Long.toString(ti.code),ti.time);
sp.edit()mit();
}
catch(Exceptionex){
returnfalse;
}
returntrue;
}
SharePreferences可以按key-value来保存记录,所以key用题目的code,则value就是解它所用的时间了。
Android不能直接访问app目录下的文件,所以不能够象brew那样将数据文件放在程序目录下供它读写,而在Activity中提供的两个函数openFileOutput和openFileInput,虽可用来读写文件,但是总是不太方便。
另外,用SQLite其实也不方便,因为手机中弄这些东西,事实上用处不大。
URL:sf.org/Android/lumen/20985.html
Android学习笔记(11)-开始做一个数独游戏[下]
继续,最后再讨论一下定时器的实现。
本来很简单的一件事,直接用java.util.timer应该就够用了,但是发现在它的task中无法去invalidate我们的MainView,很郁闷。这一点的处理说明Android还是相对线程安全的。
折腾良久,明白了非得再做一个Handler,才能在线程中操作界面元素。所以,代码比brew复杂了一点。
先还是用Timer和TimerTask来做,如下:
publicTimerHandlertimerHandler;
publicTimertimer;
publicMyTimerTasktask;
......
timer=newTimer(true);
task=newMyTimerTask(this);
......
那个MyTimerTask是MainActivity的一个内嵌类,实现如下:
privateclassMyTimerTaskextendsTimerTask
{
privateMainActivityme;
privateinta=0;
publicMyTimerTask(MainActivityp){
me=p;
}
publicvoidrun(){
me.gridView.time++;
Log.d("MyTask",Integer.toString(me.gridView.time));
timerHandler.sendEmptyMessage(0);
}
}
这里做两件事,一是将gridView中的time加一,二是发送一个消息通知timerHandler。原来我在这里直接让MainView去刷新屏幕,发现不行,所以就改成这样处理了。
然后就是如何实现TimerHandler类的,也不复杂,就是让它去刷新一下屏幕即可。
publicclassTimerHandlerextendsHandler{
privateMainViewme;
publicTimerHandler(MainViewm){
me=m;
}
@Override
publicvoidhandleMessage(Messagemsg){
Log.d("Ti",msg.toString());
me.invalidate();
}
}
如此一来,就顺了。
在MainView中的onDraw,根据当前的time值显示成00:00:00的格式即可。
另外,发现Android的模拟器运算速度不如BREW的模拟器,相当的慢。
URL:sf.org/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>publicclassMediaServiceextendsServiceimplementsMediaPlayer.OnErrorListener,MediaPlayer.OnCompletionListener,MediaPlayer.OnPreparedListener{
......
privateMediaPlayerplayer;
@Override
protectedvoidonDestroy(){
//TODOAuto-generatedmethodstub
super.onDestroy();
if(player!=null){
player.stop();
player.release();
}
}
@Override
protectedvoidonStart(intstartId,Bundlearguments){
//TODOAuto-generatedmethodstub
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
publicvoidonCompletion(MediaPlayerarg0){
//TODOAuto-generatedmethodstub
Log.d("Media","finished.");
}
@Override
publicvoidonPrepared(MediaPlayerarg0){
//TODOAuto-generatedmethodstub
Log.d("Media","prepared.");
player.start();
}
@Override
publicvoidonError(MediaPlayerarg0,intwhat,intextra){
Log.d("Media","onError");
player.stop();
}
}
这个服务主要就是用一个MediaPlayer去播放资源中的tonghua(一首MP3音乐)。次序一般是先create出这个实例,然后prepare一下(如果是文件直接prepare,如果是流则最好异步parepareAsync),接着就可以start了,同步可以直接start,异步则必须放到onPrepared中再start。
在MainActivity中启动这个服务即可。
mediaServiceIntent=newIntent();
mediaServiceIntent.setClass(this,MediaService.class);
this.startService(mediaServiceIntent,newBundle());
当前,在Activity停止时也别忘了将这个Service停掉,而在Service停止时关掉MediaPlayer。
在模拟器上试了,效果不是太好,声音有点断断续续,不知道是不是我的解码器的问题(Vista系统)。
URL:sf.org/Android/lumen/20987.html
消息机制,异步和多线程
有了framework后,我们不用面对赤裸裸的OSAPI,做一些重复而繁杂的事情。但天下没有免费的午餐,我们还是需要学会高效正确的使用不同的framework,
很多处理某一特定问题的手法在不同的framework中,用起来都会有所不同的。
在Android中,下层是Linux的核,但上层的java做的framework把这一切封装的密不透风。以消息处理为例,在MFC中,我们可以用PreTranslateMessage等东东自由处理消息,在C#中,AndersHejlsberg老大说了,他为我们通向底层开了一扇“救生窗”,但很遗憾,在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
protectedvoidonQueryComplete(inttoken,Objectcookie,Cursorcursor){
//在这里你可以获得一个cursor和你传入的附加的token和cookie。
//该方法在当前线程下(如果传入的是默认的Looper话),可以自由设定UI信息
}
};
//调用时只需要调用startQuery(inttoken,Objectcookie,ContentURIuri,String[]projection,Stringselection,String[]selectionArgs,StringsortOrder)函数即可:
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:sf.org/Android/lumen/21075.html
显示控件使用
Android的界面显示同样也是基于控件的。通常是用View(包括ViewGroup)控件配上XML的样式来做的。具体细节不想说了,可以参考Samples里的ApiDemos/View,和View的Doc,以及ImplementingaUI这篇Doc。其他还有很多,感觉算是SDK讲述的最多的内容。
从控件的使用上,和网页的设计类似,尽量用parent_width之类的抽象长度,用Theme来做风格,抽取所有的字串等信息做本地化设计。相关内容参看ImplementingaUI就好。
一类比较重要的是数据绑定控件。如果做过ASP.Net会从中看到很多类似的地方。一个支持数据绑定的控件,比如ListView。可以通过一个ListAdapter绑定到一个数据源上。ListAdapter是一个抽象类,主要的实现类包括SimpleAdapter和SimpleCursorAdapter。前者是绑定一个静态的Array,后者是绑定一个动态的Cursor。Cursor前面说过,是一个指向数据源的随机迭代器,将View绑定到Cursor通常要设置这样几个参数。一个是每一行的样式,称作RowLayout,其实就是一个普通的Layout的XML文件。还有就是一个列和现实控件的对应关系。那个控件显示哪个列的值,这是需要配置的。为了定制一个良好的数据显示控件,最简单你可以定制很PP的RowLayout,复杂一点就是可以重载绑定控件View,或者是适配器ListAdapter。如果是一个数据显示密集的应用,且你对UI有些追求,这个工作估计是必不可少的。
一个主要用于显示数据内容的Activity,可以选择派生自ListActivity。它提供了一个具有ListView的Layout,还有simple_list_item_1,simple_list_item_2,two_line_list_item等默认的RowLayout,还有一些比较不错的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:sf.org/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,可能都会响应Viewaction。但很多与我们需要显示的数据类型不一样,可以加一个type信息,明确的指出我们需要显示的数据类型,甚至还可以加上一个catalog信息,指明只有你只有按的是“中键”并发出这样的消息才响应。
从上面可以看出,Android的Intent可以添加上class,action,data/type,catalog等消息,注册中心会根据这些信息帮你找到符合的接收者。其中class是点对点的指示,一旦指明,其他信息都被忽略。Intent中还可以添加key/value的数据,发送方和接收方需要保持统一的key信息和value类型信息,这种数据的marshal在java里做,是不费什么力气的。
Android的Intent发送,可以分成单播和广播两种。广播的接收者是所有注册了的符合条件的IntentReceiver。在单播的情况下,即使有很多符合条件的接收者,也只要有一个出来处理这个消息就好(恩,个人看法,没找到确切条款或抉择的算法,本来想实验一下,没来得及...),这样的情况很容易理解,当你需要修改某个数据的时候,你肯定不会希望有十个编辑器轮流让你来处理。当广播不是这样,一个receiver没有办法阻止其他receiver进行对广播事件的处理。这种情况也很容易理解,比如时钟改变了,闹钟、备忘录等很多程序都需要分别进行处理。在自己的程序的使用中,应该分清楚区别,合理的使用。
URL:sf.org/Android/lumen/21072.html
ContentProvider数据模型概述
An
droid的数据(包括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进行插入数据类型和数据值的设置和获取。在数据库层面上来看,这应该是columnname与value的对应。但为了屏蔽ContentProvider用户涉及到具体数据库的细节,在Android的示例中,用了一个小小的模式。它为每一个表建一个基于BaseColumn类的派生类(其实完全可以不派生自BaseColumn,特别当你的表不基于默认的自动id做主键的时候),这个类通常包括一个描述该表的ContentURI对象和形如publicstaticfinalTITLE="title"这样的column到类数据的对应。从改变上角度来看,你可以修改column的名字而不需要更改用户上层代码,增加了灵活性。insert方法如果成功会返回一个uri,该uri会在原有的uri基础上增加有一个rowid。对于为什么使用rowid而不是keyid我想破了脑袋。到最后,我发现我傻了,因为ContentProvider不一定需要使用数据库,使用数据库对应的表也可以没有主键,只有rowid,才能在任何底层介质下做索引标识。
但,基于rowid在删除和修改操作是会造成一定的混乱。删除和修改操作类似。删除操作需要传入一个uri,一个where字串,一组where的参数(做条件判定...),而修改操作会多一个ContentValues做更新值。着两个操作的uri都支持在末尾添加一个rowid。于是混乱就出现了。当在where参数中指明了keyid,而在uri中提供了rowid,并且rowid和keyid所指函数不一致的时候,你听谁的?示例代码中的做法是完全无视rowid(无语...),如此野蛮的方式我估计也只能在示例中出现,在实际中该如何用,恩,我也不知道。幸运的是,我看了下上层对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,相当于emptycurs
or;如果是情况3,不抛出任何异常,返回null的Cursor。这样的话明明白白写出来是很好理解的,但由于没有官方的文档说明,在自定义的时候经常会误用。比如在某些情况下,用null表征查询失败,用抛出异常来描述错误的输入。
返回emptycursor,如果是通过databasecursor自然会有db帮你维护,但是如果返回ArrayListCursor,MergeCursor或其他自定义的Cursor,就需要自己维系了。ArrayListCursor可以通过newArrayListCursor(Columns,newArrayList(){})来提供。其中Columns一定不为null。MergeCursor不能以newMergeCursor(newCursor[]{})来创建,而需要通过newMergeCursor(newCursor[]{aEmptyCursor,...}来维系(其实很好理解,我呆了...)。自定义的Cursor也一定要提供生成emptycursor的方式。
如果将ContentProvider作为一个单独的module来理解,不通过异常而是通过null来返回MS是有好处的。在module的出口吃掉所有异常,虽然不能提供足够的信息(异常信息全部写入日志),但可能会使上层使用更简单。但在Android中,我并没有感觉到这一点。作为ContentProvider的上层函数,ListActivity.managedQuery、ListView.setListAdapter等,根本不能处理一个null的Cursor,在ListView中这会触发一个异常。更无语的是,当你把一个nullCursor设置为manage的后。它不会立即抛异常,而是在OnFreeze等生命周期函数的时候,因无法处理nullCursor而抛出一个异常。这使得你根本无法在当地catch该异常,换句话,ListActivity的manageCursor根本是个无法使用的函数。你必须用getContext().query()获得Cursor,然后判定该Cursor是否null,在进行startManagingCursor进行绑定。这远不如直接用异常进行错误路径的处理来的统一和方便。
当然,有些东西我们是不能改变的,只能去适应。对于自定义的cursor,ContentProvider,最重要的,是在无人造错误输入的情况下返回emptycursor,而不是null。至于使用null响应还是异常响应上,我个人觉得还是和系统同步为好,虽然别扭,但至少统一不容易有歧义。
此外,ContentProvider还有很多细致的语义。比如返回的Cursor需要绑定一个URI,以便自动响应更新。自定义的更新需要支持deleteRow等操作语义等等。
PS:而上层的ListView,更是陷阱重重。首先绑定到ListView的Cursor必须有_id项,否则会有异常抛出。如果做过.net的开发,这一点是可以想到的,但是,这种问题应该在文档中写明。另外,在ListView中,如果你不绑定一个数据源,你一定不能在layout中添加涉及内容的属性。比如android:height="wrap_content",这会在onMeasure的时候抛出异常。