当前位置: 网学 > 编程文档 > Android > 正文

android的五大类各种常用类的资料整理

来源:Http://myeducs.cn 联系QQ:点击这里给我发消息 作者: myeducs.cn 发布时间: 13/03/17

【网学网提醒】:网学会员为需要朋友们搜集整理了android的五大类各种常用类的资料整理相关资料,希望对各位网友有所帮助!


    androidIntent使用整理
    在一个Android应用中,主要是由一些组件组成,(Activity,Service,ContentProvider,etc.)在这些组件之间的通讯中,由Intent协助完成。正如网上一些人解析所说,Intent负责对应用中一次操作的动作、动作涉及数据、附加数据进行描述,Android则根据此Intent的描述,负责找到对应的组件,将Intent传递给调用的组件,并完成组件的调用。Intent在这里起着实现调用者与被调用者之间的解耦作用。Intent传递过程中,要找到目标消费者(另一个Activity,IntentReceiver或Service),也就是Intent的响应者,有两种方法来匹配:1,显示匹配(Explicit):,显示匹配(:publicTestBextentsActivity{.........};publicclassTestextendsActivity{......publicvoidswitchActivity(){Intenti=newIntent(Test.this,TestB.class);this.startActivity(i);}}代码简洁明了,执行了switchActivity()函数,就会马上跳转到名为TestB的Activity中。2,隐式匹配,隐式匹配(Implicit):隐式匹配,首先要匹配Intent的几项值:Action,Category,Data/Type,Component如果填写了Componet就是上例中的Test.class)这就形成了显示匹配。所以此部分只讲前几种匹配。匹配规则为最大匹配规则,1,如果你填写了Action,如果有一个程序的Manifest.xml中的某一个Activity的IntentFilter段中定义了包含了相同的Action那么这个Intent就与这个目标Action匹配,如果这个Filter段中没有定义Type,Category,那么这个Activity就匹配了。但是如果手机中有两个以上的程序匹配,那么就会弹出一个对话可框来提示说明。Action的值在Android中有很多预定义,如果你想直接转到你自己定义的Intent接收者,你可以在接收者的IntentFilter中加入一个自定义的Action值(同时要设定Category值为"android.intent.category.DEFAULT")在你的Intent中设定该值为Intent的Action,就直接能跳,转到你自己的Intent接收者中。因为这个Action在系统中是唯一的。2,data/type,你可以用Uri来做为data,比如Uriuri=Uri.parse(google);Intenti=newIntent(Intent.ACTION_VIEW,uri);手机的Intent分发过程中,会根据google的scheme判断出数据类型type手机的Brower则能匹配它,在Brower的Manifest.xml中的IntenFilter中首先有
    1
     ACTION_VIEWAction,也能处理http:的type,3,至于分类Category,一般不要去在Intent中设置它,如果你写Intent的接收者,就在Manifest.xml的Activity的IntentFilter中包含android.category.DEFAULT,这样所有不设置Category(Intent.addCategory(Stringc);)的Intent都会与这个Category匹配。4,extras(附加信息),是其它所有附加信息的集合。使用extras
    可以为组件提供扩展信息,比如,如果要执行“发送电子邮件”这个动作,可以将电子邮件的标题、正文等保存在extras里,传给电子邮件发送组件。
    三,例子代码:例子代码:publicclassHelloActivityextendsActivity{@OverridepublicbooleanonCreateOptionsMenu(Menumenu){//TODOAuto-generatedmethodstubsuper.onCreateOptionsMenu(menu);menu.add(0,Menu.FIRST+1,1,R.string.menu_open);menu.add(0,Menu.FIRST+2,2,R.string.menu_edit);menu.add(0,Menu.FIRST+3,3,R.string.menu_update);menu.add(0,Menu.FIRST+4,4,R.string.menu_close);returntrue;}@OverridepublicbooleanonOptionsItemSelected(MenuItemitem){//TODOAuto-generatedmethodstubsuper.onOptionsItemSelected(item);switch(item.getItemId()){caseMenu.FIRST+1:{this.setTitle("OpenText!");Intenti=newIntent();i.setAction("test_action");if(Tools.isIntentAvailable(this,i))this.startActivity(i);elsethis.setTitle("theIntentisunavailable!!!");break;}caseMenu.FIRST+2:{this.setTitle("EditText!");break;
    2
     }caseMenu.FIRST+3:{this.setTitle("UpdateText!");break;}caseMenu.FIRST+4:{this.setTitle("CloseText!");break;}}returntrue;}@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);this.setContentView(R.layout.main);}}
    publicclassTestIntentextendsActivity{@OverrideprotectedvoidonCreate(BundlesavedInstanceState){//TODOAuto-generatedmethodstubsuper.onCreate(savedInstanceState);TextViewtv=newTextView(this);tv.setText("TestingIntenthere!");this.setContentView(tv);}}来看看TestIntent所在项目的Manifest.xml.........四。用Intent调用系统中经常被用到的组件此常用组件部分来源于(此常用组件部分来源于(kuikui.javaeye/blog/318627))
    3
     1,web浏览器Uriuri=Uri.parse("kuikui.javaeye");returnIt=newIntent(Intent.ACTION_VIEW,uri);2,地图UrimapUri=Uri.parse("geo:38.899533,-77.036476");returnIt=newIntent(Intent.ACTION_VIEW,mapUri);3,调拨打电话界面UritelUri=Uri.parse("tel:100861");returnIt=newIntent(Intent.ACTION_DIAL,telUri);4,直接拨打电话UricallUri=Uri.parse("tel:100861");returnIt=newIntent(Intent.ACTION_CALL,callUri);(Intent.ACTION_DELETE,uninstallUri);6,安装UriinstallUri=Uri.fromParts("package","xxx",null);returnIt=newIntent(Intent.ACTION_PACKAGE_ADDED,installUri);7,播放UriplayUri=Uri.parse("file:///sdcard/download/everything.mp3");returnIt=newIntent(Intent.ACTION_VIEW,playUri);8,掉用发邮件UriemailUri=Uri.parse("mailto:shenrenkui@gmail");returnIt=newIntent(Intent.ACTION_SENDTO,emailUri);9,发邮
    件returnIt=newIntent(Intent.ACTION_SEND);String[]tos={"shenrenkui@gmail"};String[]ccs={"shenrenkui@gmail"};returnIt.putExtra(Intent.EXTRA_EMAIL,tos);returnIt.putExtra(Intent.EXTRA_CC,ccs);returnIt.putExtra(Intent.EXTRA_TEXT,"body");returnIt.putExtra(Intent.EXTRA_SUBJECT,"subject");
    Android学习笔记(来自Android实验室)学习笔记(
    Android,学习笔记Google的AndroidSDK发布也有一段时间了,一直想研究一下却苦于找不到时间。利用这个周未,开始强迫自己再次进入学习状态,原因很简单:我看好开放的gPhone。SDK的下载与安装并不复杂,网上也有不少同学已经进入状态了,我就不再重复了吧。今天主要讨论的,还是永远不变的话题:HelloWorld.1.最简单的HelloWorld安装了SDK后,直接生成一个AndroidProject,一句代码不用写,就能跑出一个最简单的HelloWorld例程。我们看一下它的代码:引用:
    4
     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表示所有组件将纵向排布。而经典的HelloWorld是用一个TextView来展示的。由此,我们知道,Android的程序从一个Activity派生出来,并且从它的onCreate开始启动;Android里要显示的组件用XML文件描述而不用在代码中硬编码(这是一个好的习惯,我们应该从一开始就坚持下去);2.让Button来说HelloWorld上面的例子是ADT自动生成的代码,似乎与我们一点关系也没有。那我们来改一下代码,因为在windows平台上的Helloworld经常是由一个按钮触发的,所以,我们想第二个Helloworld应该是这样的:加一个按钮和文本输入框,单击按钮后在原来的TextView后面加上输入框中输入的文字。第一步是,增加一个Button和一个EditText,与TextView一样,它们也在main.xml里描述一下:引用:    out_width="fill_parent"android:layout_height="wrap_content"android:text=""/>
    5
     这里有两个地方要注意: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例子加上一个菜单,
    6
     并且让它可以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一样,很方便的。
    (2)初识Activity初识
    根据文档的解释,Activity是Android开发中非常重要的一个基础类。我把它想像成J2ME中的Display类,或者是Win32平台上的Form类,也许不准确,但是它的重要性我觉得应该是一样的(当然,如果我们写的是一个没有界面的应用,例如后台运行的服务之类的,可以不用Display的)。1.在一个Activity中使用多个View如果把Activity看作MVC中的Control?它负责管理UI和接受事件(包括用户的输入)虽,
    7
     然说一个Activity通常对应一个屏幕,但事实上,我们是可以只用一个Activity管理多个不同的View来实现简单的逻辑。首先,我们增加一个新的资源描述layout/second.xml。引用:

    除了一个“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()...{
    8
     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文件了,如下:引用:很简单,就是加一个标签而已,新标签的class是.HelloThreeB,显示的应用标题与前一个Activity一样而已,然后第二步就是修改一个HelloThree类的实现,在onCreate方法中绑定按钮的事件处理器:引用:
    9
     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)参数类型是Cont
    entURI,它的详细内容下回再分析,现在就把它当成一个String类型来用
    10
     吧。参数带到新的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)...{
    11
     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即可。
    编译运行即可。
    (3)Activity的生命周期
    注意到在Activity的API中有大量的onXXXX形
    式的函数定义,除了我们前面用到的onCreate以外,还有onStart,onStop以及onPause等等。从字面上看,它们是一些事件回调,那么次序又是如何的呢?其实这种事情,自己做个实验最明白不过了。在做这个实验之前,我们先得找到在Android中的Log是如何输出的。
    12
     显然,我们要用的是android.util.log类,这个类相当的简单易用,因为它提供的全是一些静态方法:引用:Log.v(Stringtag,Stringmsg);//VERBOSELog.d(Stringtag,Stringmsg);//DEBUGLog.i(Stringtag,Stringmsg);//INFOLog.w(Stringtag,Stringmsg);//WARNLog.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,"onReStart");}publicvoidonPause()...{super.onPause();Log.v(TAG,"onPause");}publicvoidonDestroy()...{super.onDestroy();Log.v(TAG,"onDestroy");}
    13
     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(如果原来被st
    op掉了,这种情况会在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,表达的了是否处于激活状态的过程。
    14
     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的生命,切记的是:如果组件的选择不当,很有可能系统会杀掉一个正在进行重要工作的进程。下一步要了解的应该是Intent和它的IntentReceiver了,改天继续。
    (4)-学习Intent的使用-
    刚看到Intent的时候,我的确有点困惑:从字面上来说,它表示一种意图和目的;从使用上看,它似乎总是用于Activity之间的切换;而从它所在包android.content来看,它似乎与内容有关。所以,我想或许可以这样理解它:Intent类绑定一次操作,它负责携带这次操作所需要的数据以及操作的类型等。
    15
     如果是这样的话,是否可以将它与事件处理联想起来?即一个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;}
    16
     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://是
    17
     个协议头,固定这样写就行了。然后就是那个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,其实还有话说,它主要是的那些看似数据库操作的方法我们都没真正去实现呢。不过今天就到这里了,等下回再去研究吧。
    (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("土曜日");
    18
     returnitems;}@OverrideprotectedvoidonListItemClick(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:layout_width="fill_parent"android:layout_height="fill_parent">这里需要注意的是那个ListView的ID,是系统自定义的android:list,不是我们随便取的,否则系统会说找不到它想要的listview了。然后,在这个listview之外,我们又增加了一个TextView,用来显示选中的条目。再来说说这里用到的ArrayAdapter,它的构造函数中第二个参数是一个资源ID,ArrayAdapter的API文档中说是要求用一个包含TextView的layout文件,平台用它来显示每个选择条目的样式,这里的取值是R.layout.list_row,所以,我们还有一个list_row.xml文件来描述这个
    19
     布局,相当简单。引用:    android:layout_width="fill_parent"android:layout_height="fill_parent">从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");
    20
     items.add(i3);HashMapi4=newHashMap();i4.put("name","木曜日");i4.put("key","THU");items.add(i4);Has
    hMapi5=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的数组。我的代码是这样的:引用://SimpleAdapterdemoList>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入手吧,同样修改代码:引用:
    21
     //CursorAdapterdemoCursormCursor=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文件,让我们的应用可以访问到通讯簿:引用:......OK,先到这里。
    (6)-关于Dialog的简单体验
    续android.app中的几个类的学习,今天的内容是那几个Dialog的体验。注意到android.app包下除了Dialog(可用于制作复杂的对话框)以外,还包括了几个系统定义好的对话框类,如DatePickerDialog、TimePickerDialog及AlertDialog。其中AlertDialog我上回用过一次,基本上就那样子了,今天看看另外两个对话框的使用吧。首先是DatePickerDialog类,修改代码如下:引用:publicclassHelloTwoCextendsActivityimplementsOnClickListener,OnDateSetListener...{publicHelloTwoC()...{super();}publicvoidonCreate(Bundleicicle)...{
    22
     super.onCreate(icicle);setTheme(android.R.style.Theme_Dark);setContentView(R.layout.mainc);Buttonbtn=(Button)findViewById(R.id.date);btn.setOnClickListener(this);}@OverridepublicvoidonClick(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();}@OverridepublicvoiddateSet(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);今天先到这里,下回再看看Service和Notification的使用。
    (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()。
    23
     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);}@OverridepublicvoidonClick(Viewarg0)...{//用一个显式的Intent来启动服务Intenti=newIntent();i.setClass(this,HelloTwoDService.class);//带上我的名字Bundleb=newBundle();b.putString("name","sharetop");this.startService(i,b);}
    24
     }当然要启动这个HelloTwoD,也需要在我最初的那个HelloTwo中加一点代码(我就不罗嗦了)。再看看那个HelloTwoDService是如何实现的:引用:publicclassHelloTwoDServiceextendsService...{publicTimertimer;publicfinalStringTAG="HelloTwoDService_TAG";publicvoidonCreate()...{super.onCreate();Log.d(TAG,"onCreate");timer=newTimer(true);}@OverridepublicIBindergetBinder()...{//TODOAuto-generatedmethodstubreturnnull;}publicvoidonStart(intstartId,Bundlearg)...{//看看startId是什么内容if(arg!=null)Log.d(TAG,"onStart"+Integer.valueOf(startId).toString()+"from"+arg.getString("name"));elseLog.d(TAG,"onStartwithnullBundle");timer.schedule(newTimerTask()...{publicvoidrun()...{//表示一下我的存在Log.d(TAG,"sayfromatimer.");//停掉自己这个服务HelloTwoDService.this.stopSelf();}},5000);}publicvoidonDestroy()...{Log.d(TAG,"onDestroy");}}
    25
     这里我用一个定时器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用来表示点击后可以发生一个跳转行为。OK,今天到此为止。下次该体验各种View了。
    (8)GridView与ImageView
    26
     很久没有进行我的Android学习之旅了,今天抽空继续。简单一点吧,就瞧瞧那个Grid的效果,Android提供了一个GridView,不过从APIDemo中看来,它似乎与PC上的GRID差别还是挺大的,更像那个IconView的感觉。不知道Android中如何实现表格界面?虽然在移动终端上,表格一般不会有谁使用,大家似乎更倾向于使用ListView,而Android对于ListView则有更简单的实现ListActivity。
    废话不说,还是自己写几句代码来实验一下。引用:从描述文件中的这些属性来看,与表格非常类似,除了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
    27
     };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。除了上面用到的几个方法以外,还有以下几个方法值得注意:
    与图像来源有关的方法,我们只用了资源文件的方式。引用://不同的图像来源
    28
     publicvoidsetImageBitmap(Bitmapbm)publicvoidsetImageDrawable(Drawabledrawable)publicvoidsetImageResource(intresid)publicvoidsetImageURI(ContentURIuri)
    图像效果的操作。引用://颜色过滤publicvoidsetColorFilter(intcolor,Modemode)//矩阵变换publicvoidsetImageMatrix(Matrixmatrix)//透明度publicvoidsetAlpha(intalpha)具体的使用可以参考API,动手试一下就差不多了
    (9)-开始做一个数独游戏[(9)-开始做一个数独游戏[上]
    不想再写Hello123了,今天开始做一个数独小游戏,因为这个游戏比较简单应该容易上手,就作为我学习Android之后的第一个程序比较合适。
    初步的设计是只有一个界面(如下图),然后用绿色字体表示题目中有的固定的数字,黄色字体显示玩家输入的数字,而红色字体则是程序判断输入错误后的显示。另外模式分为三种:普通写入、标记(玩家用蓝色小方块标记当前单元格可以输入的数字)、排除模式(玩家指定数字,游戏自动判断一下这个数字肯定不能输入的单元格,将它反相显示出来)。
    准备工作就是做一张背景图(棋盘)和三套数字小图片(红、绿、黄)即可。
    29
    
    首先建立工程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.value]);canvas.drawBitmap(b,xx,yy,null);
    二是显示文本的方法,刚才显示图像的drawBitmap中最后一个参数直接给了null,因为我
    30
     们实在没有什么效果需要给图像的,但是文本则不同,我们用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));
    (10)-开始做一个数独游戏[中]-开始做一个数独游戏中
    继续,今天讨论的是记录文件的读写。因为原来在Brew平台上
    实现的数独将题库是一个二进制文件,所以在Android就直接拿那个文件来用了。
    计划实现两个函数,先是LoadTiList(),加载题库,先装题库文件放在资源里,然后从资源里加载它作为一个DataInputStream即可。代码也没几行,如下:引用:publicstaticbooleanLoadTiList(MainActivityme)
    31
     ...{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    //代码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;
    32
     }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,虽可用来读写文件,但是总是
    33
     不太方便。
    另外,用SQLite其实也不方便,因为手机中弄这些东西,事实上用处不大。
    (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++;
    34
     Log.d("MyTask",Integer.toString(me.gridView.time));
    timerHandler.sendEmptyMessage(0);}}
    这里做两件事,一是将gridView中的time加一,二是发送一个消息通知timerHandler。原来我在这里直接让MainView去刷新屏幕,发现不行,所以就改成这样处理了。然后就是如何实现TimerHandler类的,也不复杂,就是让它去刷新一下屏幕即可。引用:publicclassTimerHandlerextendsHandler...{
    privateMainViewme;publicTimerHandler(MainViewm)...{me=m;}
    @OverridepublicvoidhandleMessage(Messagemsg)...{Log.d("Ti",msg.toString());me.invalidate();}
    }
    如此一来,就顺了。在MainView中的onDraw,根据当前的time值显示成00:00:00的格式即可。
    另外,发现Android的模拟器运算速度不如BREW的模拟器,相当的慢
    (12)-开始做一个数独游戏补充-开始做一个数独游戏[补充一个数独游戏补充]
    再补充一点吧,如果需要给游戏加上背景音乐,其实也是非常容易的事情。因为Android提供了一个MediaPlayer类可以方便的播放音乐文件。
    android.media.MediaPlayer类没有构造函数,一般是用它的静态方法create生成实例,简单地告诉它音乐文件的资源ID即可(支持mp3/wav/midi等)。
    35
     首先,我们得建立一个Service,就叫MediaService吧,它的代码如下:引用:publicclassMediaServiceextendsServiceimplementsMediaPlayer.OnErrorListener,MediaPlayer.OnCompletionListener,MediaPlayer.OnPreparedListener...{
    ......
    privateMediaPlayerplayer;
    @OverrideprotectedvoidonDestroy()...{//TODOAuto-generatedmethodstubsuper.onDestroy();if(player!=null)...{player.stop();player.release();}}
    @OverrideprotectedvoidonStart(intstartId,Bundlearguments)...{//TODOAuto-generatedmethodstubLog.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();}}
    36
     @OverridepublicvoidonCompletion(MediaPlayerarg0)...{//TODOAuto-generatedmethodstubLog.d("Media","finished.");}
    @OverridepublicvoidonPrepared(MediaPlayerarg0)...{//TODOAuto-generatedmethodstubLog.d("Media","prepared.");player.start();}
    @OverridepublicvoidonError(MediaPlayerarg0,intwhat,intextra)...{
    Log.d("Media","onError");pl
    ayer.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系统)。
    37
     Android实例剖析笔记(一)
    开卷语俗话说,“熟读唐诗三百首,不会作诗也会吟”。最近收集了很多Android的示例代码,从这些代码的阅读和实验中学习到很多知识,从而产生写这个系列的打算,目标就是一步步跟着实例进行动手实作,真正从“做”中体会和学习Android开发。本文是这个系列的第一篇,目标是Android自带的一个范例程序:记事本,将分为四篇文章进行详细介绍。
    预备知识搭建开发环境,尝试编写”HelloWorld”,了解Android的基本概念,熟悉Android的API(官方文档中都有,不赘述)。
    程序截图
    38
     先来简单了解下程序运行的效果
    39
     40
     41
     程序入口点类似于win32程序里的WinMain函数,Android自然也有它的程序入口点。它通过在AndroidManifest.xml文件中配置来指明,可以看到名为NotesList的activity节点下有这样一个intent-filter,其action为android.intent.action.MAIN,Category指定为android.intent.category.LAUNCHER,这就指明了这个activity是作为入口activity,系统查找到它后,就会创建这个activity实例来运行,若未发现就不启动(你可以把MAIN改名字试试)。NotesList详解
    42
     就从入口点所在的activity(见图1)开始,可以看到这个activity最重要的功能就是显示日志列表。这个程序的日志都存放在Sqlite数据库中,因此需要读取出所有的日志记录并显示。先来看两个重要的私有数据,第一个PROJECTION字段指明了“日志列表“所关注的数据库中的字段(即只需要ID和Title就可以了)。privatestaticfinalString[]PROJECTION=newString[]{Notes._ID,//0Notes.TITLE,//1};第二个字段COLUMN_INDEX_TITLE指明title字段在数据表中的索引。privatestaticfinalintCOLUMN_INDEX_TITLE=1;然后就进入第一个调用的函数onCreate。Intentintent=ge
    tIntent();if(intent.getData()==null){intent.setData(Notes.CONTENT_URI);}因为NotesList这个activity是系统调用的,此时的intent是不带数据和操作类型的,系统只是在其中指明了目标组件是Notelist,所以这里把”content://com.google.provider.NotePad/notes”保存到intent里面,这个URI地址指明了数据库中的数据表名(参见以后的NotePadProvider类),也就是保存日志的数据表notes。Cursorcursor=managedQuery(getIntent().getData(),PROJECTION,null,null,Notes.DEFAULT_SORT_ORDER);然后调用managedQuery函数查询出所有的日志信息,这里第一个参数就是上面设置的”content://com.google.provider.NotePad/notes”这个URI,即notes数据表。PROJECTION字段指明了结果中所需要的字段,Notes.DEFAULT_SORT_ORDER指明了结果的排序规则。实际上managedQuery并没有直接去查询数据库,而是通过ContentProvider来完成实际的数据库操作,这样就实现了逻辑层和数据库层的分离。
    43
     SimpleCursorAdapteradapter=newSimpleCursorAdapter(this,R.layout.noteslist_item,cursor,newString[]{Notes.TITLE},newint[]{android.R.id.text1});setListAdapter(adapter);查询出日志列表后,构造一个CursorAdapter,并将其作为ListView的数据源,从而在界面上显示出日志列表。可以看到,第二个参数是R.layout.noteslist_item,打开对应的noteslist_item.xml文件,就是用来显示一条日志记录的TextView,最后两个字段指明了实际的字段映射关系,通过这个TextView来显示一条日志记录的title字段。
    处理“选择日志事件处理选择日志”事件选择日志
    既然有了“日志列表”,就自然要考虑如何处理某一条日志的单击事件,这通过重载onListItemClick方法来完成,@OverrideprotectedvoidonListItemClick(ListViewl,Viewv,intposition,longid){Uriuri=ContentUris.withAppendedId(getIntent().getData(),id);
    44
     Stringaction=getIntent().getAction();if(Intent.ACTION_PICK.equals(action)||Intent.ACTION_GET_CONTENT.equals(action)){//Thecalleriswaitingforustoreturnanoteselectedby//theuser.Thehaveclickedonone,soreturnitnow.
    setResult(RESULT_OK,newIntent().setData(uri));}else{//Launchactivitytoview/editthecurrentlyselecteditemstartActivity(newIntent(Intent.ACTION_EDIT,uri));}}首先通过”content://com.google.provider.NotePad/notes”和日志的id号拼接得到选中日志的真正URI,然后创建一个新的Intent,其操作类型
    为Intent.ACTION_EDIT,数据域指出待编辑的日志URI(这里只分析else块)。Intent深度剖析那么,上面这句startActivity(newIntent(Intent.ACTION_EDIT,uri))执行后会发生什么事情呢?这时候Android系统就跳出来接管了,它会根据intent中的信息找到对应的activity,在这里找到的是NoteEditor这个activity,然后创建这个activity的实例并运行。那么,Android又是如何找到NoteEditor这个对应的activity的呢?这就是intent发挥作用的时刻了。newIntent(Intent.ACTION_EDIT,uri)这里的Intent.ACTION_EDIT=”android.int, ent.action.EDIT”,另外通过设置断点,我们看下这里的uri值:
    45
     可以看到选中的日志条目的URI是:content://com.google.provider.NotePad/notes/1然后我们再来看下Androidmanfest.xml,其中有这个provider发现没有?它也有com.google.provider.NotePad,这个是content://com.google.provider.NotePad/notes/1的一部分,同时上面第一个intent-filter中有一个action名为android.intent.action.EDIT,而前面我们创建的Intent也正好是Intent.ACTION_EDIT=”android.intent.action.EDIT”,想必大家已经明白是怎么回事了吧。下面就进入activity选择机制了:系统从intent中获取道uri,得到了content://com.google.provider.NotePad/notes/1,去掉开始的content:标识,得到com.google.provider.NotePad/notes/1,然后获取前面的com.google.provider.NotePad,然后就到Androidmanfest.xml中找到authorities为com.google.provider.NotePad的provider,这个就是后面要讲的contentprovider,然后就加载这个contentprovider。
    47
     在这里是NotePadProvider,然后调用NotePadProvider的gettype函数,并把上述URI传给这个函数,
    函数返回URI所对应的类型(这里返回Notes.CONTENT_ITEM_TYPE,代表一条日志记录,CONTENT_ITEM_TYPE="vnd.android.cursor.item/vnd.google.note而")。@OverridepublicStringgetType(Uriuri){switch(sUriMatcher.match(uri)){caseNOTES:returnNotes.CONTENT_TYPE;caseNOTE_ID:returnNotes.CONTENT_ITEM_TYPE;default:thrownewIllegalArgumentException("UnknownURI"+uri);}}上面的sUriMatcher.match是用来检测uri是否能够被处理,而sUriMatcher.match(uri)返回值其实是由sUriMatcher=newUriMatcher(UriMatcher.NO_MATCH);sUriMatcher.addURI(NotePad.AUTHORITY,"notes",NOTES);sUriMatcher.addURI(NotePad.AUTHORITY,"notes/#",NOTE_ID);决定的。然后系统使用获得的"vnd.android.cursor.item/vnd.google.note"和”android.intent.action.EDIT”到androidmanfest.xml中去找匹配的activity.正好NoteEditor这个activity的intent-filter满足上述条件,这样就找到了NoteEditor。于是系统加载这个类并实例化,运行,然后就到了NoteEditor的OnCreate函数中(见后续文章)。
    小技巧1,在命令行中使用”adbshell”命令进入系统中,然后”cdapp”进入应用程序所在目录,”rmXXX”就可以删除你指定的apk,从而去掉其在系统顶层界面占据的图标,若两次”cddata”则可以进入应用程序使用的数据目录,你的数据可以保存在这里,例如Notepad就是把其数据库放在它的databases目录下,名为note_pad.db.2,第一次启动模拟器会比较慢,但以后就别关闭模拟器了,修改代码,调试都不需要再次启动的,直接修改后run或debug就是。
    Android实例剖析笔记(二)
    上篇文章分析了NotesList这个Activity,并着重剖析了其中的intent机制,本文将继续上篇未完的工作,以NotesList为实例介绍Android的菜单机制(尤其是动态菜单机制)。
    简介android提供了三种菜单类型,分别为optionsmenu,contextmenu,submenu。optionsmenu就是通过按home键来显示,contextmenu需要在view上按上2s后显示。这两种menu都有可以加入子菜单,子菜单不能种不能嵌套子菜单。optionsmenu最多只能在屏幕最下面显示6个菜单选项,称为iconmenu,iconmenu不能有checkable选项。多于6的菜单项会以moreiconmenu来调出,称为expandedmenu。optionsmenu通过activity的onCreateOptionsMenu来生成,这个函数只会在menu第一次生成时调用。任何想改变optionsmenu的想法只
    49
    
    能在onPrepareOptionsMenu来实现,这个函数会在menu显示前调用。onOptionsItemSelected用来处理选中的菜单项。contextmenu是跟某个具体的view绑定在一起,在activity种用registerForContextMenu来为某个view注册contextmenu。contextmenu在显示前都会调用onCreateContextMenu来生成menu。onContextItemSelected用来处理选中的菜单项。android还提供了对菜单项进行分组的功能,可以把相似功能的菜单项分成同一个组,这样就可以通过调用setGroupCheckable,setGroupEnabled,setGroupVisible来设置菜单属性,而无须单独设置。OptionsMenuNotepad中使用了optionsmenu和contextmenu两种菜单。首先来看生成optionsmenu的onCreateOptionsMenu函数。
    menu.add(0,MENU_ITEM_INSERT,0,R.string.menu_insert).setShortcut('3','a').setIcon(android.R.drawable.ic_menu_add);
    这是一个标准的插入一个菜单项的方法,菜单项的id为MENU_ITEM_INSERT。有意思的是下面这几句代码:
    Intentintent=newIntent(null,getIntent().getData());intent.addCategory(Intent.CATEGORY_ALTERNATIVE);menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE,0,0,newComponentName(this,NotesList.class),null,intent,0,null);
    这到底有何用处呢?其实这是一种动态菜单技术(也有点像插件机制),若某一个activity,其类型是”android.intent.category.ALTERNATIVE”,数据是”vnd.android.cursor.dir/vnd.google.note”的话,系统就会为这个activity增加一个菜单项。在androidmanfest.xml中查看后发现,没有一个activity符合条件,所以这段代码并没有动态添加出任何一个菜单项。为了验证上述分析,我们可以来做一个实验,在androidmanfest.xml中进行修改,看是否会动态生成出菜单项。
    50
     实验一首先我们来创建一个新的activity作为目标activity,名为HelloAndroid,没有什么功能,就是显示一个界面。
    publicclassHelloAndroidextendsActivity{@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);this.setContentView(R.layout.main);}}
    它所对应的布局界面XML文件如下:
    
    然后修改androidmanfest.xml,加入下面这段配置,让HelloAndroid满足上述两个条件:
    
    51
     好了,运行下试试,哎,还是没有动态菜单项加入呀!怎么回事呢?查看代码后发现,原来是onPrepareOptionsMenu搞的鬼!这个函数在onCreateOptionsMenu之后运行,下面这段代码中,由于Menu.CATEGORY_ALTERNATIVE是指向同一个组,所以把onCreateOptionsMenu中设置的菜单项给覆盖掉了,而由于onPrepareOptionsMenu没有给Menu.CATEGORY_ALTERNATIVE附新值,故Menu.CATEGORY_ALTERNATIVE还是为空。
    Intentintent=newIntent(null,uri);intent.addCategory(Intent.CATEGORY_ALTERNATIVE);menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE,0,0,null,specifics,intent,0,items);
    好的,那我们暂时把上面这几句给注释掉,当然,也可以不注释这几句,在onCreateOptionsMenu中改groupid号,即将Menu.CATEGORY_ALTERNATIVE改为Menu.first,其他的也行,但注意不要改为menu.none,这样会覆盖掉
    menu.add(0,MENU_ITEM_INSERT,0,R.string.menu_insert).setShortcut('3','a').setIcon(android.R.drawable.ic_menu_add);
    添加的菜单。因为menu.none也为0运行后就可以看到动态菜单出来了!
    52
     上面这个optionsmenu是在NotesList界面上没有日志列表选中的情况下生成的,若先选中一个日志,然后再点”menu”,则生成的optionsmenu是下面这样的:
    53
     哎,又动态增加了两个菜单项”Editnote”和”Edittitle”,这又是如何动态加入的呢?这就是onPrepareOptionsMenu的功劳了。
    Uriuri=ContentUris.withAppendedId(getIntent().getData(),getSelectedItemId());
    首先获取选中的日志(若没有选择,则uri为空)Intent[]specifics=newIntent[1];specifics[0]=newIntent(Intent.ACTION_EDIT,uri);MenuItem[]items=newMenuItem[1];然后为选中的日志创建一个intent,操作类型为Intent.ACTION_EDIT,数据为选中日志的URI.于是会为选中的日志创建一个”Editnote”菜单项。
    Intentintent=newIntent(null,uri);intent.addCategory(Intent.CATEGORY_ALTERNATIVE);menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE,0,0,null,specifics,intent,0,items);
    这几句和上面onCreateOptionsMenu函数中类似,用于动态增加菜单项,若某一个activity,其类型是”android.intent.category.ALTERNATIVE”,数据
    54
     是”vnd.android.cursor.item/vnd.google.note”的话,系统就会为这个activity增加一个菜单项。在androidmanfest.xml中查看后发现,TitleEditor这个activity符合条件,于是系统就为TitleEditor这个activity动态添加一个菜单项”Edittitle”。else{menu.removeGroup(Menu.CATEGORY_ALTERNATIVE);}若日志列表为空,则从菜单中删除组号为Menu.CATEGORY_ALTERNATIVE的菜单项,只剩下”Addnote”菜单项。处
    理“选中菜单项事件处理选中菜单项”事件选中菜单项菜单项选中事件的处理非常简单,通过onOptionsItemSelected来完成,这里只是简单地调用startActivity(newIntent(Intent.ACTION_INSERT,getIntent().getData()));这个intent的操作类型为Intent.ACTION_INSERT,数据为日志列表的URI,即”content://com.google.provider.NotePad/notes”
    @OverridepublicbooleanonOptionsItemSelected(MenuItemitem){switch(item.getItemId()){caseMENU_ITEM_INSERT://LaunchactivitytoinsertanewitemstartActivity(newIntent(Intent.ACTION_INSERT,getIntent().getData()));returntrue;}returnsuper.onOptionsItemSelected(item);}
    ContextMenu下面介绍另一种菜单---上下文菜单,这通过重载onCreateContextMenu函数实现。
    首先确认已经选中了日志列表中的一个日志,若没选择,则直接返回。Cursor指向选中的日志项。
    Cursorcursor=(Cursor)getListAdapter().getItem(info.position);if(cursor==null){//Forsomereasontherequesteditemisn'tavailable,donothingreturn;
    55
     }
    然后,设置上下文菜单的标题为日志标题//Setupthemenuheadermenu.setHeaderTitle(cursor.getString(COLUMN_INDEX_TITLE));最后为上下文菜单增加一个菜单项
    //Addamenuitemtodeletethenotemenu.add(0,MENU_ITEM_DELETE,0,R.string.menu_delete);
    对于上下文菜单项选中的事件处理,是通过重载onContextItemSelected实现的。
    switch(item.getItemId()){caseMENU_ITEM_DELETE:{//DeletethenotethatthecontextmenuisforUrinoteUri=ContentUris.withAppendedId(getIntent().getData(),info.id);getContentResolver().delete(noteUri,null,null);returntrue;}}returnfalse;}
    对于日志的删除,首先调用ContentUris.withAppendedId(getIntent().getData(),info.id);来拼接出待删除日志的URI.然后getContentResolver().delete(noteUri,null,null);调用下层的ContentProvider去删除此日志。实验二
    来做个简单实验,在上述代码基础上增加一个上下文菜单项。首先在onCreateContextMenu函数中增加一个上下文菜单项:
    menu.add(0,MENU_ITEM_INSERT,0,R.string.menu_insert);
    然后为其在onContextItemSelected函数中增加一个处理过程:caseMENU_ITEM_INSERT:{newAlertDialog.Builder(this).setIcon(R.drawable.app_notes).setTitle(R.string.app_name).setMessage(R.string.error_message).setPositiveButton(R.string.button_ok,newOnClickListener(){
    publicvoidonClick(DialogInterfacedialog,intwhich){
    56
     //TODOAuto-generatedmethodstub}}).show();returntrue;}
    实验结果如下:
    57
     附记感谢EvanJIANG对前一篇文章的错误之处进行指正,“只是指明会在Launcher中显示图标,同一个apk可以在桌面上加很多的图标,分别启动内部不同的多个界面。“,实验
    后发现确实如此,学习了。
    作者:phinecos(洞庭散人)出处:phinecosblogs/本文版权归作者和博客园共有,欢迎转载,但请保留此段声明,并在文章页面明显位置给出原文连接
    Android实例剖析笔记(三)
    58
     上一篇文章介绍了Android的菜单机制,并动手做了一个实验来探究动态菜单的实验机制。这一篇将重点介绍Activity的生命周期,通过一个简单的实验来摸索状态转换的机制,最后介绍NotePad中使用的自定义控件技术。Activity的生命周期Activity类中有许多onXXX形式的函数可以重载,比如onCreate,onStart,onStop,onPause,那么它们的调用顺序到底是如何的呢?下面就通过一个实验来进行分析。在做这个实验之前,我们先得知道如何在Android中进行Log输出的。我们要使用的是android.util.log类,这个类相当的简单易用,因为它提供的全是一些静态方法:
    Log.v(StringLog.d(StringLog.i(StringLog.w(StringLog.e(Stringtag,tag,tag,tag,tag,StringStringStringStringStringmsg);msg);msg);msg);msg);//VERBOSE//DEBUG//INFO//WARN//ERROR
    前面的tag是由我们定义的一个标识,一般可以用“类名_方法名“来定义。要在Eclipse中查看输出的log信息,需要打开Logcat(WindowShowViewotherAndroidLogCat即可打开)实验一我们要做的实验非常简单,就是有两个Activity(我这里分别叫做frmLogin和hello2),t它们各自有一个button,可以从第一个跳到第二个,也可以从第二个跳回到第一个。配置文件AndroidManifest.xml非常简单,第二个activity并没有多余的信息需要指定。
    
    59
    

    第一个activity的代码如下:publicclassfrmLoginextendsActivity{privatefinalstaticStringTAG="FrmLogin";
    /**Calledwhentheactivityisfirstcreated.*/@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);Log.v(TAG,"onCreate");setContentView(R.layout.main);this.setViewOneCommand();}publicvoidsetViewOneCommand(){Buttonbtn=(Button)findViewById(R.id.btnGo);btn.setOnClickListener(newView.OnClickListener(){publicvoidonClick(Viewv){Intentintent=newIntent();intent.setClass(frmLogin.this,hello2.class);startActivity(intent);finish();}});ButtonbtnExit=(Button)findViewById(R.id.btnExit);btnExit.setOnClickListener(newView.OnClickListener(){publicvoidonClick(Viewv){frmLogin.this.finish();}});
    60
     }@Overrideprotec
    tedvoidonDestroy(){super.onDestroy();Log.v(TAG,"onDestroy");}@OverrideprotectedvoidonPause(){super.onPause();Log.v(TAG,"onPause");}@OverrideprotectedvoidonRestart(){super.onRestart();Log.v(TAG,"onRestart");}@OverrideprotectedvoidonResume(){super.onResume();Log.v(TAG,"onResume");}@OverrideprotectedvoidonStart(){super.onStart();Log.v(TAG,"onStart");}@OverrideprotectedvoidonStop(){super.onStop();Log.v(TAG,"onStop");}
    61
     }
    我在每个onXXX方法中都加入了log方法,值得注意的一点是按钮单击事件处理函数中,在最后我调用了finish();待会我会将此行注释掉进行对比实验。第二个activity的代码和第一个完全一样,只是将setClass的两个参数反一下,这样就可以简单地实现在两个Activity界面中来回切换的功能了。下面开始实验,第一个实验室从第一个activity跳到第二个activity(此时第一个关闭),然后从第二个跳回第一个(此时第二个关闭)运行后观察LogCat,得到如下画面:
    然后来进行第二个实验,对代码进行调整,我们把第一个activity中的finish()注释掉,从第一个activity跳到第二个(此时第一个没有关闭)然后第二个直接关闭,(则第一个会重新来到前端),结果如图所示,可以看出调用了FrmLogin的onRestart而不是onStart,因为第一个activity只是stop,而并没有被destory掉。
    62
     前面两个实验都很好理解,可第三个实验就让我不明白了,过程如下:从第一个activity跳到第二个activity(此时第一个不关闭),然后第二个跳回第一个(此时第二个也不关闭),然后第一个再跳回第二个(此时第一个不关闭),照上面来推断,应该还是会调用onRestart才对,可实际上它调用的却是onStart,why???
    这里先不讨论例子了,来看看官方文档对Activity生命周期的介绍。
    63
     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是可以被清掉的,以及其中哪一个应该是最先被清掉。不过,文档中提到在这
    64
     个已被清掉的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的生命,切记的是:如果组件的选择不当,很有可能系统会杀掉一个正在进行重要工作的进程。
    自定义控件
    这里主要介绍下“编辑日志”中使用的一个自定义EditTe
    xt控件,它的效果如下图:
    主要功能就是在文本语句之间绘制分割线。publicstaticclassLinedEditTextextendsEditText{privateRectmRect;privatePaintmPaint;//weneedthisconstructorforLayoutInflaterpublicLinedEditText(Contextcontext,AttributeSetattr
    65
     s){super(context,attrs);mRect=newRect();mPaint=newPaint();mPaint.setStyle(Paint.Style.STROKE);mPaint.setColor(0x800000FF);}@OverrideprotectedvoidonDraw(Canvascanvas){intcount=getLineCount();Rectr=mRect;Paintpaint=mPaint;for(inti=0;i    主要工作就是重载onDraw方法,利用从TextView继承下来的getLineCount函数获取文本所占的行数,以及getLineBounds来获取特定行的基准高度值,而且这个函数第二个参数会返回此行的“外包装”值。再利用这些值绘制这一行的线条。为了让界面的View使用自定义的EditText类,必须在配置文件中进行设置
    
    66
     这里class="com.example.android.notepad.NoteEditor$LinedEditText"就指明了应当使用自定义的LinedEditText类。
    作者:phinecos(洞庭散人)出处:phinecosblogs/本文版权归作者和博客园共有,欢迎转载,但请保留此段声明,并在文章页面明显位置给出原文连接
    Android实例剖析笔记(四)
    上篇文章介绍了Activity的生命周期,并通过一个实验来探索状态转换的机制,然后介绍了应用中使用的一个自定义控件。本文将继续分析NoteEditor这个类和以及ContentProvider机制。NoteEditor深入分析首先来弄清楚“日志编辑“的状态转换,通过上篇文章的方法来做下面这样一个实验,首先进入“日志编辑“时会触发onCreate和onResume,然后用户通过OptionMenu选择”Edittitle”后,会触发onSaveInstanceState和onPause,最后,用户回到编辑界面,则再次触发onResume.最终通过LogCat可以得到下图:
    那么下面就按照上述顺序对此类进行剖析。首先是onCreate方法,一开始先获取导致进入“日志编辑”界面的intent,分析其操作类型可得知是“编辑日志”还是“新增日志”。
    67
     finalIntentintent=getIntent();//Dosomesetupbasedontheactionbeingperformed.finalStringaction=intent.getAction();
    若是“编辑日志”,则设置当前状态为
    “编辑”,并保存待编辑日志的URI.mState=STATE_EDIT;mUri=intent.getData();若是“新增日志”,则设置当前状态为“新增”,并通过contentprovider向数据库中新增一个“空白日志”,后者返回“空白日志”的URI.
    mState=STATE_INSERT;mUri=getContentResolver().insert(intent.getData(),null);
    然后不管是“编辑”或“新增”,都需要从数据库中读取日志信息(当然,若是“新增”,读出来的肯定是空数据)。mCursor=managedQuery(mUri,PROJECTION,null,null,null);最后,类似于web应用中使用的Session,这里也将日志文本保存在InstanceState中,因此,若此activity的实例此前是处于stop状态,则我们可以从它那取出它原本的文本数据.
    if(savedInstanceState!=null){mOriginalContent=savedInstanceState.getString(ORIGINAL_CONTENT);}
    第二个来分析onResume函数,首先把游标置于第一行(也只有一行)mCursor.moveToFirst();然后取出“正文”字段,这时有一个比较有趣的技巧,“设置文本”并不是调用setText,而是调用的setTextKeepState,后者相对于前者有一个优点,就是当界面此前stop掉,现在重新resume回来,那么此前光标所在位置仍然得以保存。而若使用setText,则光标会重置到行首。
    Stringnote=mCursor.getString(COLUMN_INDEX_NOTE);mText.setTextKeepState(note);
    最后,将当前编辑的正文保存到一个字符串变量中,用于当activity被暂停时使用。if(mOriginalContent==null){mOriginalContent=note;}通过前面的图可以得知,activity被暂停时,首先调用的是onSaveInstanceState函数。
    68
     outState.putString(ORIGINAL_CONTENT,mOriginalContent);
    这里就仅仅将当前正编辑的正文保存到InstanceState中(类似于Session).最后来看onPause函数,这里首先要考虑的是若activity正要关闭,并且编辑区没有正文,则将此日志删除。
    if(isFinishing()&;&;(length==0)&;&;!mNoteOnly){setResult(RESULT_CANCELED);deleteNote();}
    否则的话,就更新日志信息ContentValuesvalues=newContentValues();if(!mNoteOnly){values.put(Notes.MODIFIED_DATE,System.currentTimeMillis());if(mState==STATE_INSERT){Stringtitle=text.substring(0,Math.min(30,length));if(length>30){intlastSpace=title.lastIndexOf('');if(lastSpace>0){title=title.substring(0,lastSpace);}}values.put(Notes.TITLE,title);}}values.put(Notes.NOTE,text);getContentResolver().update(mUri,values,null,null);}}在生成OptionMenu的函数onCreateOptionsMenu中,我们再一次看到下面这段熟悉的代码了:
    Intentintent=newIntent(null,getIntent().getData());
    69
     intent.addCategory(Intent.CATEGORY_ALTERNATIVE);menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE,0,0,newComponentName(this,NoteEditor.class),null,intent,0,null);
    这种生成动态
    菜单的机制在Android实例剖析笔记(二)这篇文章中已经介绍过了,就不赘述了。最后,来看下放弃日志和删除日志的实现,由于还没有接触到底层的contentprovider,这里都是通过getContentResolver()提供的update,delete,insert来向底层的contentprovider发出请求,由后者完成实际的数据库操作。
    privatefinalvoid{if(mCursor!={if(mState{//PutasemCursor.close();mCursor=null;ContentValuesvalues=newContentValues();values.put(Notes.NOTE,mOriginalContent);getContentResolver().update(mUri,values,null,null);}elseif(mState==STATE_INSERT){//Weinsertedanemptynote,makesuretodeleteitdeleteNote();}}setResult(RESULT_CANCELED);finish();}privatefinalvoiddeleteNote(){if(mCursor!=null){mCursor.close();mCursor=null;getContentResolver().delete(mUri,null,null);
    70
    cancelNote()null)==STATE_EDIT)theoriginalnotetextbackintothedatab
     mText.setText("");}}
    剖析NotePadProviderNotePadProvider就是所谓的contentprovider,它继承自android.content.ContentProvider,也是负责数据库层的核心类,主要提供五个功能:1)查询数据2)修改数据3)添加数据4)删除数据5)返回数据类型这五个功能分别对应下述五个可以重载的方法:
    publicintdelete(Uriuri,Stringselection,String[]selectionArgs){return0;}publicStringgetType(Uriuri){returnnull;}publicUriinsert(Uriuri,ContentValuesvalues){returnnull;}publicbooleanonCreate(){returnfalse;}publicCursorquery(Uriuri,String[]projection,Stringselection,String[]selectionArgs,StringsortOrder){returnnull;}publicintupdate(Uriuri,ContentValuesvalues,Stringselection,String[]selectionArgs)
    71
     {return0;}
    这些都要你自己实现,不同的实现就是对应不同的content-provider。但是activity使用content-provider不是直接创建一个对象,然后调用这些具体方法。而是调用managedQuery,getContentResolver().delete,update等来实现,这些函数其实是先找到符合条件的content-provider,然后再调用具体content-provider的函数来实现,那又是怎么找到content-provider,就是通过uri中的authority来找到content-provider,这些都是通过系统完成,应用程序不用操心,这样就达到了有效地隔离应用和内容提供者的具体实现的目的。有了以上初步知识后,我们来看NotePadProvider是如何为上层提供数据库层支持的。下面这三个字段指明了数据库名称,数据库版本,数据表名称。
    privatestaticfinalStringDATABASE_NAME="note_pad.db";privatestaticfinalintDATABASE_VERSION=2;privatestaticfinalStringNOTES_TABLE_NAME="notes";
    实际的数据库操作其实都是通过一个私有静态类DatabaseHelper实现的,其构造函数负责创建指定名称和版本的数据库,onCreate函
    数则创建指定名称和各个数据域的数据表(就是简单的建表SQL语句)。onUpgrade负责删除数据表,再重新建表。privatestaticclassDatabaseHelperextendsSQLiteOpenHelper{DatabaseHelper(Contextcontext){super(context,DATABASE_NAME,null,DATABASE_VERSION);}@OverridepublicvoidonCreate(SQLiteDatabasedb){db.execSQL("CREATETABLE"+NOTES_TABLE_NAME+"("+Notes._ID+"INTEGERPRIMARYKEY,"+Notes.TITLE+"TEXT,"+Notes.NOTE+"TEXT,"+Notes.CREATED_DATE+"INTEGER,"+Notes.MODIFIED_DATE+"INTEGER"+");");}@OverridepublicvoidonUpgrade(SQLiteDatabasedb,intoldVersio
    72
     n,intnewVersion){Log.w(TAG,"Upgradingdatabasefromversion"+oldVersion+"to"+newVersion+",whichwilldestroyallolddata");db.execSQL("DROPTABLEIFEXISTSnotes");onCreate(db);}}
    在Android实例剖析笔记(一)这篇文章中我们已经见识到了getType函数的用处了,也正是通过它的解析,才能区分开到底是对全部日志还是对某一条日志进行操作。
    publicStringgetType(Uriuri){switch(sUriMatcher.match(uri)){caseNOTES:returnNotes.CONTENT_TYPE;caseNOTE_ID:returnNotes.CONTENT_ITEM_TYPE;default:thrownewIllegalArgumentException("UnknownURI"+uri);}}
    上面的sUriMatcher.match是用来检测uri是否能够被处理,而sUriMatcher.match(uri)返回值其实是由下述语句决定的。
    sUriMatcher=newUriMatcher(UriMatcher.NO_MATCH);sUriMatcher.addURI(NotePad.AUTHORITY,"notes",NOTES);sUriMatcher.addURI(NotePad.AUTHORITY,"notes/#",NOTE_ID);
    sNotesProjectionMap这个私有字段是用来在上层应用使用的字段和底层数据库字段之间建立映射关系的,当然,这个程序里两处对应的字段都是一样(但并不需要一样)。
    privatestaticHashMapsNotesProjectionMap;static{sNotesProjectionMap=newHashMap();sNotesProjectionMap.put(Notes._ID,Notes._ID);sNotesProjectionMap.put(Notes.TITLE,Notes.TITLE);sNotesProjectionMap.put(Notes.NOTE,Notes.NOTE);sNotesProjectionMap.put(Notes.CREATED_DATE,Notes.CREATED
    73
     _DATE);sNotesProjectionMap.put(Notes.MODIFIED_DATE,Notes.MODIFIED_DATE);}
    数据库的增,删,改,查操作基本都一样,具体可以参考官方文档,这里就仅仅以删除为例进行说明。一般可以分为三步来完成,首先打开数据库
    SQLiteDatabasedb=mOpenHelper.getWritableDatabase();
    然后根据URI指向的是日志列表还是某一篇日志,到数据库中执行删除动作
    switch(sUriMatcher.match(uri)){caseNOTES:count=db.delete(NOTES_TABLE_NAME,where,whereArgs);break;caseNOTE_ID:StringnoteId=uri.getPathSegments().get(1);count=db.delete(NOTES_TABLE_NAME,Notes._ID+"="+noteId+(!TextUtils.isEmpty(where)?"AND("+where+')':""),whereArgs);break;}
    最后,一定记得通知上层:其传递下来的URI在底层数据库中
    已经发生了变化。
    getContext().getContentResolver().notifyChange(uri,null);
    对NotePad的改进首先我想指出NotePad的一个bug,其实这个小bug在2月份就有人向官方报告了,参见code.google/p/android/issues/detail?id=1909。NoteEditor类中的变量mNoteOnly根本就是没有用处的,因为它始终都是false,没有任何变化,所以可以删除掉。第二点是在NoteEditor类中,有下面这样的语句:
    setResult(RESULT_OK,(newIntent()).setAction(mUri.toString()));setResult(RESULT_CANCELED);
    可到底想展示什么技术呢?实际上并没有完整展现出来,这里我对其进行修改后来指明
    参见code.google/p/android/issues/detail?id=1671)。
    74
     首先在NotesList类中增加一个变量
    privatestaticfinalintREQUEST_INSERT=100;//请求插入标识符
    然后修改onOptionsItemSelected函数如下:
    @OverridepublicbooleanonOptionsItemSelected(MenuItemitem){switch(item.getItemId()){caseMENU_ITEM_INSERT:this.startActivityForResult(newIntent(Intent.ACTION_INSERT,getIntent().getData()),REQUEST_INSERT);returntrue;}returnsuper.onOptionsItemSelected(item);}
    最后重载onActivityResult函数来处理接收到的activityresult。
    protectedvoidonActivityResult(intrequestCode,intresultCode,Intentdata){if(requestCode==REQUEST_INSERT){if(resultCode==RESULT_OK){Log.d(TAG,"OK!!!");}elseif(resultCode==RESULT_CANCELED){Log.d(TAG,"CANCELED!!!");}}}
    试试,当你在NoteEditor中保存或放弃日志时,观察LogCat,你可以看到下面这样的画面:
    75
     作者:phinecos(洞庭散人)出处:phinecosblogs/本文版权归作者和博客园共有,欢迎转载,但请保留此段声明,并在文章页面明显位置给出原文连接Android实例剖析笔记(五)
    这个系列的前四篇文章介绍了Androidsdk中自带的NotePad程序,算是开了个头,这篇开始介绍Androidsdk中自带的第二个示例程序—Snake(贪食蛇)。本文将主要介绍我对这个示例程序进行的一些修改。
    游戏暂停/继续机制游戏暂停继续机制
    由于原来的代码中在游戏运行时没有提供控制选项(比如暂停/继续),因此除非你死了,否则只能玩到底。我这里对代码进行一些修改,加入一个OptionMenu来提供暂停/继续机制。
    首先加入一个变量记录游戏当前状态
    privateintmState=SnakeView.READY;
    然后重载onCreateOptionsMenu函数,创建一个控制菜单项,并对其进行处理,提供暂停/继续机制。
    /**@seeandroid.app.Activity#onOptionsItemSelected(android.view.MenuItem)*@Author:phinecos*@Date:2009-08-28*/@OverridepublicbooleanonOptionsItemSelected(MenuItemitem){switch(item.getItemId()){caseMENU_CONTROL:{
    76
     if(mState==SnakeView.PAUSE){//此前状态是"停止",则转为"运行"mState=SnakeV
    iew.RUNNING;mSnakeView.setMode(SnakeView.RUNNING);item.setIcon(android.R.drawable.ic_media_pause).setTitle(R.string.cmd_pause);}elseif(mState==SnakeView.RUNNING){//此前状态是"运行",则转为“暂停"mState=SnakeView.PAUSE;mSnakeView.setMode(SnakeView.PAUSE);item.setIcon(android.R.drawable.ic_media_play).setTitle(R.string.cmd_run);}elseif(mState==SnakeView.READY){//此前是"初始状态",则转为"运行"mState=SnakeView.RUNNING;}returntrue;}}returnsuper.onOptionsItemSelected(item);}/**@seeandroid.app.Activity#onOptionsItemSelected(android.view.MenuItem)*@Author:phinecos*@Date:2009-08-28*/@OverridepublicbooleanonCreateOptionsMenu(Menumenu){super.onCreateOptionsMenu(menu);menu.add(0,MENU_CONTROL,0,R.string.cmd_pause).setIcon(android.R.drawable.ic_media_pause);returntrue;}
    修改后运行截图如下:
    77
     78
     当然,这段代码还是有问题的,游戏刚开始时,必须先点击菜单确认,再按上方向键才能开始。(以后再来修改。。。)
    穿墙贪食蛇第二个修改是把这个普通的贪食蛇改成可以穿墙(呵呵,这样就可以不死了。。。)。想必要修改的就是撞墙检测那段代码?没错,就是下面这段!
    //Collisiondetection//Fornowwehavea1-squarewallaroundtheentirearenaif((newHead.x<1)||(newHead.y<1)||(newHead.x>mXTileCount-2)||(newHead.y>mYTileCount-2)){//撞墙setMode(LOSE);return;}
    原来的版本是发现撞墙时就直接判定为失败,我这里做个小小的修改,让它可以穿墙而去:privatevoidupdateSnake()
    79
     {booleangrowSnake=false;//grabthesnakebytheheadCoordinatehead=mSnakeTrail.get(0);CoordinatenewHead=newCoordinate(1,1);mDirection=mNextDirection;switch(mDirection){caseEAST:{newHead=newCoordinate(head.x+1,head.y);break;}caseWEST:{newHead=newCoordinate(head.x-1,head.y);break;}caseNORTH:{newHead=newCoordinate(head.x,head.y-1);break;}caseSOUTH:{newHead=newCoordinate(head.x,head.y+1);break;}}//穿墙的处理if(newHead.x==0){//穿左边的墙newHead.x=mXTileCount-2;}elseif(newHead.y==0){//穿上面的墙newHead.y=mYTileCount-2;}elseif(newHead.x==mXTileCount-1){//穿右边的墙
    80
     newHead.x=1;}elseif(newHead.y==mYTileCount-1){//穿下面的墙newHead.y=1;}//判断是否撞到自己intsnakelength=mSnakeTrail.size();for(intsnakeindex=0;snakeindex    loffthetailmSnakeTrail.add(0,newHead);//exceptifwewantthesnaketogrowif(!growSnake){mSnakeTrail.remove(mSnakeTrail.size()-1);}intindex=0;for(Coordinatec:mSnakeTrail){
    81
     if(index==0){setTile(YELLOW_STAR,c.x,c.y);}else{setTile(RED_STAR,c.x,c.y);}index++;}}
    其实修改后的代码非常简单,就是把新节点的值做些处理,让它移动到对应的行/列的头部或尾部即可。
    下面就是修改后的“穿墙”贪食蛇的运行截图:
    全屏机制
    82
     游戏一般都是全屏的,原始代码也考虑到标题栏太过难看了,于是使用下面这句代码就去掉了标题栏:
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    可还是没有达到全屏的效果,在Android1.5中实现全屏效果非常简单,只需要一句代码即可实现:getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);运行效果如下图所示:
    接下来的修改目标是加入得分排行榜机制,再加入一个启动画面和选择菜单。当然,这一篇文章水了点,仅仅记录了自己做的一些小实验,并没有涉及到Snake的代码分析,不过请继
    续期待下一篇文章。。。
    作者:phinecos(洞庭散人)出处:phinecosblogs/
    83
     本文版权归作者和博客园共有,欢迎转载,但请保留此段声明,并在文章页面明显位置给出原文连接
    Android实例剖析笔记(六)
    上一篇文章介绍了我对Snake这个示例程序进行的一些简单修改,从这一篇开始真正开始详细分析Snake的具体实现。本文首先分析Snake的界面Layout实现,并通过一个实验来说明项目中使用的FrameLayout和RelativeLayout这两种布局的效果,其次还介绍了一个用于UI优化的工具---hierarchyviewer。AndroidLayout机制1,FrameLayout先来看官方文档的定义:FrameLayout是最简单的一个布局对象。它被定制为你屏幕上的一个空白备用区域,之后你可以在其中填充一个单一对象—比如,一张你要发布的图片。所有的子元素将会固定在屏幕的左上角;你不能为FrameLayout中的一个子元素指定一个位置。后一个子元素将会直接在前一个子元素之上进行覆盖填充,把它们部份或全部挡住(除非后一个子元素是透明的)。有点绕口而且难理解,下面还是通过一个实例来理解吧。我们仿照Snake项目中使用的界面一样,建立一个简单的FrameLayout,其中包含两个Views元素:ImageView和TextView,而后面的TextView还包含在一个RelativeLayout中。
        84
     android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerInParent="true"android:gravity="center_horizontal"android:textColor="#ffffffff"android:textSize="24sp"/>效果如下图所示:
    2,UI优化,Android的tools目录下提供了许多实用工具,这里介绍其中一个用于查看当前UI结构视图的工具hierarchyviewer。打开tools/hierarchyviewer.bat后,查看上面这个示例的UI结构图可得:
    85
     我们可以很明显的看到由红色线框所包含的结构出现了两个framelayout节点,很明显这两个完全意义相同的节点造成了资源浪费(这里可以提醒大家在开发工程中可以习惯性的通过hierarchyViewer查看当前UI资源的分配情况),那么如何才能解决这种问题呢(就当前例子是如何去掉多余的frameLayout节点)?这时候就要用到标签来处理类似的问题了。我们将上边xml代码中的framLayout替换成merge:    86
     android:layout_height="fill_parent">

    运行程序后在Emulator中显示的效果是一样的,可是通过hierarchyviewer查看的UI结构是有变化的,当初多余的FrameLayout节点被合并在一起了,或者可以理解为将merge标签中的子集直接加到Activity的FrameLayout跟节点下(这里需要提醒大家注意:所有的Activity视图的根节点都是frameLayout)。如果你所创建的Layout并不是用framLayout作为根节点(而是应用LinerLayout等定义root标签),就不能应用上边的例子通过merge来优化UI结构。
    87
     3,RelativeLayout,RelativeLayout允许子元素指定他们相对于其它元素或父元素的位置(通过ID指定)。因此,你可以以右对齐,或上下,或置于屏幕中央的形式来排列两个元素。元素按顺序排列,因此如果第一个元素在屏幕的中央,那么相对于这个元素的其它元素将以屏幕中央的相对位置来排列。如果使用XML来指定这个layout,在你定义它之前,被关联的元素必须定义。
    解释起来也比较麻烦,不过我做个对比实验可以明白它的用处了,
    试着把上面例子里的RelativeLayout节点去掉看看,效果如下图所示,可以看到由于FrameLayout的原因,都在左上角靠拢了,而使用了RelativeLayout,则可以让TextView相对于屏幕居中。
    88
     4,Snake的界面分析,有了上述Layout的基础知识,我们再来看Snake的布局文件就很好理解了,就是一个SnakeView和一个TextView,启动后,后者会覆盖在前者上面。
    
    也就是这样的效果:
    89
     那么相应的代码是如何实现这个效果的呢?SnakeView有一个私有变量存放覆盖其上的TextView:
    privateTextViewmStatusText;
    在Snake这个activity的onCreate方法中,首先将Layout文件中的SnakeView和TextView关联起来:setContentView(R.layout.snake_layout);mSnakeView=(SnakeView)findViewById(R.id.snake);mSnakeView.setTextView((TextView)findViewById(R.id.text));然后设置SnakeView的状态为ReadymSnakeView.setMode(SnakeView.READY);这一句代码会调用下述函数:publicvoidsetMode(intnewMode){intoldMode=mMode;mMode=newMode;if(newMode==RUNNING&;oldMode!=RUNNING)
    {//游戏进入“运行”状态,则隐藏文字信息
    90
     mStatusText.setVisibility(View.INVISIBLE);update();return;}//根据新状态,设置待显示的文字信息Resourcesres=getContext().getResources();CharSequencestr="";if(newMode==PAUSE){//新状态为“暂停”str=res.getText(R.string.mode_pause);}if(newMode==READY){//新状态为“准备开始”str=res.getText(R.string.mode_ready);}if(newMode==LOSE){//新状态为“游戏失败”str=res.getString(R.string.mode_lose_prefix)+mScore+res.getString(R.string.mode_lose_suffix);}//设置文字信息并显示mStatusText.setText(str);mStatusText.setVisibility(View.VISIBLE);}
    在mStatusText.setVisibility(View.VISIBLE);这一句后就显示出上面这个游戏起始画面了。
    Android实例剖析笔记(七)
    上一篇文章分析了Snake的界面Layout实现,本文将关注游戏主界面这个View是如何实现的,并提出了我的一些困惑之处,希望有朋友能帮忙解惑。Snake这个项目把主界面剖成界面UI和游戏逻辑两层,最基础的界面
    UI部分用父类TileView来表示,子类SnakeView是在TileView的UI基础上,加入相应的游戏控制逻辑,从而实现了两者的分离,这对于游戏的修改非常有用。UI实现部分首先来看界面UI部分,基本思想大家都非常清楚:把整个屏幕看做一个二维数组,每一个元素可以视为一个方块,因此每个方格在游戏进行过程中可以处于不同的状态,比如空闲,墙,苹果,贪食蛇(蛇身或蛇头)。我们在操作游戏的过程,其实就是不断
    91
     修改相应方格的状态,然后再让整个View去重绘制自身(当然,还需要加入一些游戏当前所处状态(失败或成功)的判定机制)。TileView的数据成员如下:
    //方格的大小protectedstaticintmTileSize;//方格的行数和列数protectedstaticintmXTileCount;protectedstaticintmYTileCount;//xy坐标系的偏移量privatestaticintmXOffset;privatestaticintmYOffset;//存储三种方格的图标文件privateBitmap[]mTileArray;//二维方格地图privateint[][]mTileGrid;那么在游戏还未正式开始前,首先要做一些初始化工作,在View第一次加载时会首先调用onSizeChanged,这里就是做这些事的最好时机。@OverrideprotectedvoidonSizeChanged(intw,inth,intoldw,intoldh){//计算屏幕中可放置的方格的行数和列数mXTileCount=(int)Math.floor(w/mTileSize);mYTileCount=(int)Math.floor(h/mTileSize);mXOffset=((w-(mTileSize*mXTileCount))/2);mYOffset=((h-(mTileSize*mYTileCount))/2);mTileGrid=newint[mXTileCount][mYTileCount];clearTiles();}
    注意模拟器屏幕默认的像素是320×400,而代码中默认的方格大小为12,因此屏幕上放置的方格数为26×40,把屏幕剖分成这么大后,再设置一个相应的二维int型数组来记录每一个方格的状态,根据方格的状态,可以从mTileArray保存的图标文件中读取对应的状态图标。第一次调用完onSizeChanged后,会紧跟着第一次来调用onDraw来绘制View自身,当然,此时由于所有方格的状态都是0,所以它在屏幕上等于什么也不会去绘制。
    publicvoidonDraw(Canvascanvas){super.onDraw(canvas);for(intx=0;x    92
     {for(inty=0;y0){canvas.drawBitmap(mTileArray[mTileGrid[x][y]],mXOffset+x*mTileSize,mYOffset+y*mTileSize,mPaint);}}}}
    onDraw要做的工作非常简单,就是扫描每一个方格,根据方格当前状态,从图标文件中选择对应的图标绘制到这个方格上。当然这个onDraw在游戏进行过程中,会不断地被调用,从而界面不断被更新。
    游戏逻辑部分再来看子类SnakeView是如何在父类TileView的基础上,加入特定的游戏逻辑,从而完成Snake这个程序的。
    privateArrayListmSnakeTr
    ail=newArrayList();//组成贪食蛇的方格列表privateArrayListmAppleList=newArrayList();//苹果方格列表由于SnakeView从TileView继承而来,则可以说它已经拥有这个二维方格地图了(只是此时地图里的所有方格状态都是0)。那么它有了这么一个二维方格地图,如何去初始化这个地图呢?这在initNewGame函数中实现。privatevoidinitNewGame(){//清空蛇和苹果占据的方格mSnakeTrail.clear();mAppleList.clear();//目前组成蛇的方格式固定的,而且方向也固定朝北mSnakeTrail.add(newCoordinate(7,7));mSnakeTrail.add(newCoordinate(6,7));mSnakeTrail.add(newCoordinate(5,7));mSnakeTrail.add(newCoordinate(4,7));mSnakeTrail.add(newCoordinate(3,7));mSnakeTrail.add(newCoordinate(2,7));
    93
     mNextDirection=NORTH;//随即加入苹果for(inti=0;i    想象下对整个游戏屏幕拍张照,然后对其下一个状态再拍张照,那么两张照片之间的区别是怎么产生的呢?对于系统来说,它只知道不断调用onDraw,后者负责对整个屏幕进行绘制,那要产生两个屏幕之间的差异,肯定要通过一些手段对某些数据结构(比如这里的二维方格地图)进行调整(比如用户的控制指令,定时器等),然后等到下一次onDraw时就会把这些更改在界面上反映出来。这里要着重说明下privatelongmMoveDelay=600;这个成员变量,虽然很不起眼,但仔细考虑它的作用就会发现很有趣,那么改变它的大小到底是如何让我们感觉到游戏变快或变慢呢?可以打个简单的比方,在时刻0游戏启动,首先把蛇和苹果的位置都在方格地图上作好了标记,然后我们在update函数中修改蛇身让蛇向北前进一步,而这个改变此时还只是停留在内部的核心数据结构上(即二维方格地图),还没有在界面上显示出来。当然,我们马上想到要想让这更改显示出来,让系统调用onDraw去绘制不就完了吗?可是问题是我们不知道系统是隔多长时间去调用onDraw函数,于是mMoveDelay此时就发挥作用了,通过它就可以设置休眠的时间,等时间一到,马上就会通知SnakeView去重绘制。你可以试试把mMoveDelay数值调大,就会看出我上面提到的“拍照“的效果。Handler的使用写过JavaScript或者ActionScript的开发者,对于setInterval的用法会非常了解。那么在Android中如何实现setInterval的方法呢?其中有两种方法可以实现类似的功能,其中一个是在线程中调用Handler方法,另外一个是应用Timer。Snake中使用了前者
    94
     classRefreshHandlerextendsHandler{@OverridepublicvoidhandleMessage(Messagemsg){//“
    苏醒”后的处理SnakeView.this.update();SnakeView.this.invalidate();}publicvoidsleep(longdelayMillis){//休眠delayMillis毫秒this.removeMessages(0);sendMessageDelayed(obtainMessage(0),delayMillis);}};
    而实际调用的处理函数update就可以说是整个游戏的引擎,正是由于它的工作(修改蛇和苹果的状态到一个新的状态,然后休眠自己,然后等到苏醒后在Handler中就会让系统区绘制上次修改过的二维方块地图,然后再次调用update,如此循环反复,生生不息),才使得游戏不断被推进,因此,比做“引擎“不为过。publicvoidupdate(){if(mMode==RUNNING){longnow=System.currentTimeMillis();if(now-mLastMove>mMoveDelay){clearTiles();updateWalls();updateSnake();updateApples();mLastMove=now;}mRedrawHandler.sleep(mMoveDelay);}}既然update是游戏的动力,要让游戏停止下来只要不再调用update就可以了(因为此时其实是画面静止了),因此游戏进入暂停(这个状态还可以转为“运行“,其实就是继续可以修改,再绘制),若进入失败(其实此时二维方块地图还停留在最后一个画面处,这也是为什么在开始时要首先清理掉整个地图)【这一点,可以在游戏失败后,再次开始新游戏,此时通过设置的断点即可观察到上次游戏运行时的底层数据】。
    95
     一点困惑可是个人认为Snake下面这段代码读起来有点怪,有点像一个“先有鸡,还是先有蛋?“的问题,导致我的思维逻辑上出现一个“怪圈“。
    publicvoidhandleMessage(Messagemsg){SnakeView.this.update();SnakeView.this.invalidate();}
    按照这段代码的意思来看,当休眠的时间已经到了,首先去调用update,即为下一次绘制做准备工作,再让自己休眠起来,最后通知系统重绘制自己。哎,这让我难以理解,还是回到时刻0的例子来说,在时刻0时让蛇身向北前进了一步(指的是底层的二维方格地图的修改,不是界面),然后让自己休眠0.6毫秒,当时间到了,首先去调用update方法,那么就又会让蛇身做出修改,也就是把上一次还没绘制的覆盖掉了(那么上一次的修改岂不是白费,还没画上去呢)更何况在update,中又会让自己去休眠(还没调用invalidate,怎么又去休眠了?),又怎么还能去通知系统调用我的onDraw方法呢?也就是说invalidate根本没有执行???按我的理解,应该把顺序颠倒一下,先通知系统去调用onDraw方法重绘,使得上一次对底层二维方格地图的修改显示出来,然后再去为下一次修改做准备工作,最后让自己进入休眠,等待苏醒过来,如此循环反复。实验证明,颠倒过来也是正确的,不过关于这一个迷惑我的地方,希望有朋友能指点我一下
    !记得在javascript里使用setInterval时,也是先写处理逻辑,然后在末尾处写上一句setInterval(这也是我习惯的思维方式了),难道google上面这种写法有何深意?
    此外,感觉每次绘制时都重新绘制墙壁,有点浪费时间,因为墙壁根本没有任何变化的。还有就是mLastMove这个变量设置的初衷是保证当前时间点距上一次变化已经过去了mMoveDelay毫秒,可是既然已经用了sleep机制,再使用这个时间差看上去并无必要。
    Android实例剖析笔记(八)
    上一篇文章分析了小游戏Snake的基本框架,本文将分析Android自带的另一个小游戏LunarLander,它与前者的“定时器+系统调用onDraw”架构相比,由于采用
    96
     了“多线程+强制自行绘制”的架构思路,因而更为实用。
    和Snake的比较就界面Layout来说,这个程序其实和Snake没有什么不同,同样是采用了FrameLayout,而且游戏的主界面由一个自定义的View来实现,这里是LunarView。读过上一篇文章的朋友也许会发现,Snake的架构是“定时器+系统调用onDraw”来实现的,这里有一个最大的缺陷就是onDraw是由Android系统来调用的,我们只能依赖它,却无法自行控制。这就好比一个黑盒,当然,总是能把我们要的东西给做出来,可却无法控制其做事的细节,这对于游戏这样高效率的东西可是不利的,因此最好的解决之道当然是把绘制这部分工作自己”承包“过来,告别吃大锅饭的,进入”联产承包制”时代。
    此外,由于游戏的本质就是连续两帧图片之间发生些许差异,那么要不断催生这种差异的发生,只要有某种连续不断发生的事件在进行就可以,例如Snake中使用的
    97
     定时器,就是在不断地产生这种“差异源”,与此类似,一个线程也是不断在运行中,通过它也是可以不断产生这种“差异源”的。SurfaceView初探如果说Snake中使用的Layout加自定义View是一把小型武器的话,那在SurfaceView对于android中游戏的开发来说就算是重型武器了。我们使用前者时总是容易把游戏中某个对象(比如上文的每一个方格)当做一个小组件来处理,而后者则根本没有这种划分的概念,在它眼中,所有东西都是在Canvas(画布)中自行绘制出来的(背景,人物等)。SurfaceView提供直接访问一个可画图的界面,可以控制在界面顶部的子视图层。SurfaceView是提供给需要直接画像素而不是使用窗体部件的应用使用的。Android图形系统中一个重要的概念和线索是surface。View及其子类(如TextView,Button)要画在surface上。每个surface创建一个Canvas对象(但属性时常改变),用来管理view在surface上的绘图操作,如画点画
    线。还要注意的是,使用它的时候,一般都是出现在最顶层的:TheviewhierarchywilltakecareofcorrectlycompositingwiththeSurfaceanysiblingsoftheSurfaceViewthatwouldnormallyappearontopofit.使用的SurfaceView的时候,一般情况下还要对其进行创建,销毁,改变时的情况进行监视,这就要用到SurfaceHolder.Callback.
    classLunarViewextendsSurfaceViewimplementsSurfaceHolder.Callback{publicvoidsurfaceChanged(SurfaceHolderholder,intformat,intwidth,intheight){}//在surface的大小发生改变时激发publicvoidsurfaceCreated(SurfaceHolderholder){}//在创建时激发,一般在这里调用画图的线程。publicvoidsurfaceDestroyed(SurfaceHolderholder){}//销毁时激发,一般在这里将画图的线程停止、释放。}
    98
     surfaceCreated会首先被调用,然后是surfaceChanged,当程序结束时会调用surfaceDestroyed。下面来看看LunarView最重要的成员变量,也就是负责这个View所有处理的线程privateLunarThreadthread;//实际工作线程thread=newLunarThread(holder,context,newHandler(){@OverridepublicvoidhandleMessage(Messagem){mStatusText.setVisibility(m.getData().getInt("viz"));mStatusText.setText(m.getData().getString("text"));}});这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。由于有了LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。
    publicvoidsurfaceChanged(SurfaceHolderholder,intformat,intwidth,intheight){thread.setSurfaceSize(width,height);}publicvoidsurfaceCreated(SurfaceHolderholder){//启动工作线程结束thread.setRunning(true);thread.start();}publicvoidsurfaceDestroyed(SurfaceHolderholder){booleanretry=true;thread.setRunning(false);while(retry){try{//等待工作线程结束,主线程才结束
    99
     thread.join();retry=false;}catch(InterruptedExceptione){}}}
    工作线程LunarThread由于SurfaceHolder是一个共享资源,因此在对其操作时都应该实行“互斥操作“,即需要使用synchronized进行”封锁“机制。再来讨论下为什么要使用消息机制来更新界面的文字信息呢?其实原因是这样的,渲染文字的工作实际上是主线程(也就是LunarView类)的父类View的工作,而并不属于工作线程LunarThread,因此在工作线程中式无法控制的。所以我们改为向主线程发送一个Message来代替,让主线程通过Handler对接收到的消息进行处理,从而更新界面文字信息。再回顾上一篇SnakeView里的文字信息更新,由于
    是SnakeView自己(就这一个线程)对其包含的TextView做控制,当然没有这样的问题了。
    publicvoidsetState(intmode,CharSequencemessage){synchronized(mSurfaceHolder){mMode=mode;if(mMode==STATE_RUNNING){//运行中,隐藏界面文字信息Messagemsg=mHandler.obtainMessage();Bundleb=newBundle();b.putString("text","");b.putInt("viz",View.INVISIBLE);msg.setData(b);mHandler.sendMessage(msg);}else{//根据当前状态设置文字信息mRotating=0;mEngineFiring=false;Resourcesres=mContext.getResources();CharSequencestr="";
    100
     if(mMode==STATE_READY)str=res.getText(R.string.mode_ready);elseif(mMode==STATE_PAUSE)str=res.getText(R.string.mode_pause);elseif(mMode==STATE_LOSE)str=res.getText(R.string.mode_lose);elseif(mMode==STATE_WIN)str=res.getString(R.string.mode_win_prefix)+mWinsInARow+""+res.getString(R.string.mode_win_suffix);if(message!=null){str=message+"\n"+str;}if(mMode==STATE_LOSE)mWinsInARow=0;Messagemsg=mHandler.obtainMessage();Bundleb=newBundle();b.putString("text",str.toString());b.putInt("viz",View.VISIBLE);msg.setData(b);mHandler.sendMessage(msg);}}}下面就是LunaThread这个工作线程的执行函数了,它一直不断在重复做一件事情:锁定待绘制区域(这里是整个屏幕),若游戏还在进行状态,则更新底层的数据,然后直接强制界面重新绘制。publicvoidrun(){while(mRun){Canvasc=null;try{//锁定待绘制区域c=mSurfaceHolder.lockCanvas(null);synchronized(mSurfaceHolder){if(mMode==STATE_RUNNING)updatePhysics();//更新底层数据,判断游戏状态doDraw(c);//强制重绘制
    101
     }}finally{if(c!=null){mSurfaceHolder.unlockCanvasAndPost(c);}}}}
    这里要注意的是最后要调用unlockCanvasAndPost来结束锁定画图,并提交改变强行自绘制doDraw这段代码就是在自己的Canvas上进行绘制,具体的绘制就不解释了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代码是下面这个:
    canvas.save();canvas.rotate((float)mHeading,(float), mX,mCanvasHeight-(float)mY);if(mMode==STATE_LOSE){mCrashedImage.setBounds(xLeft,yTop,xLeft+mLanderWidth,yTop+mLanderHeight);mCrashedImage.draw(canvas);}elseif(mEngineFiring){mFiringImage.setBounds(xLeft,yTop,xLeft+mLanderWidth,yTop+mLanderHeight);mFiringImage.draw(canvas);}else{mLanderImage.setBounds(xLeft,yTop,xLeft+mLanderWidth,yTop+mLanderHeight);mLanderImage.draw(canvas);}canvas.restore();
    在绘制火箭的前后,调用了save()和restore(),它是先保存当前矩阵,将其复制到一个私有堆栈上。然后接下来对rotate的调用还是在原有的矩阵上进行操作,但当restore调用后,以前保存的设置又重新恢复。不过,在这里还是看不出有什么用处。。。暂停/继续机制
    暂停继续机制
    102
     LunarLancher的暂停其实并没有不再强制重绘制,而是没有对底层的数据做任何修改,依然绘制同一帧画面,而继续则是把mLastTime设置为当前时间+100毫秒的时间点,因为以前暂停时mLastTime就不再更新了,这样做事为了与当前时间同步起来。
    publicvoidpause(){//暂停synchronized(mSurfaceHolder){if(mMode==STATE_RUNNING)setState(STATE_PAUSE);}}publicvoidunpause(){//继续//Movetherealtimeclockuptonowsynchronized(mSurfaceHolder){mLastTime=System.currentTimeMillis()+100;}setState(STATE_RUNNING);}
    这样做的目的是为了制造“延迟“的效果,都是因为updatePhysics函数里这两句if(mLastTime>now)return;doubleelapsed=(now-mLastTime)/1000.0;至于游戏的控制逻辑和判定部分就不介绍了,没有多大意思。
    103
    
    
  • 上一篇资讯: Android的基础资料
  • 下一篇资讯: Android的Audio 系统
  • 网学推荐

    免费论文

    原创论文

    浏览:
    设为首页 | 加入收藏 | 论文首页 | 论文专题 | 设计下载 | 网学软件 | 论文模板 | 论文资源 | 程序设计 | 关于网学 | 站内搜索 | 网学留言 | 友情链接 | 资料中心
    版权所有 QQ:3710167 邮箱:3710167@qq.com 网学网 [Myeducs.cn] 您电脑的分辨率是 像素
    Copyright 2008-2015 myeducs.Cn www.myeducs.Cn All Rights Reserved
    湘ICP备09003080号