1:Provider 的创建使用
俗话说,“熟读唐诗三百首,不会作诗也会吟”。最近收集了很多Android的示例代码,从这些代码的阅读和实验中学习到很多知识,从而产生写这个系列的打算,目标就是一步步跟着实例进行动手实作,真正从“做”中体会和学习AndroidAndroid自带的一个范例程序:记事本,将分为四篇文章进行详细介绍。
预备知识
搭建开发环境,尝试编写”Hello World”,了解Android的基本概念,熟悉Android的API(官方文档中都有,不赘述)。
程序截图
先来简单了解下程序运行的效果
程序入口点
类似于win32程序里的WinMain函数,Android自然也有它的程序入口点。它通过在AndroidManifest.xml文件中配置来指明,可以看到名为NotesList的activity节点下有这样一个intent-filter,其action为android.intent.action.MAIN,
Category指定为 android.intent.category.LAUNCHER,这就指明了这个activity是作为入口activity,系统查找到它后,就会创建这个activity实例来运行,若未发现就不启动(你可以把MAIN改名字试试)。
<intent-filter>
<action android:name="android.intent.action.MAIN"
/>
<category android:name="android.intent.category.LAUNCHER"
/>
</intent-filter>
NotesList详解
就从入口点所在的activity(见图1)开始,可以看到这个activity最重要的功能就是显示日志列表。这个程序的日志都存放在Sqlite数据库中,因此需要读取出所有的日志记录并显示。
先来看两个重要的私有数据,第一个PROJECTION字段指明了“日志列表“所关注的数据库中的字段(即只需要ID和Title就可以了)。
private
static
final String[] PROJECTION =
new// 0
Notes.TITLE, // 1
};
第二个字段COLUMN_INDEX_TITLE指明title字段在数据表中的索引。
private
static
final
int COLUMN_INDEX_TITLE =
1;
然后就进入第一个调用的函数onCreate。
Intent intent =if (intent.getData() ==
null)
{
intent.setData(Notes.CONTENT_URI);
}
因为NotesList这个activity是系统调用的,此时的intent是不带数据和操作类型的,系统只是在其中指明了目标组件是Notelist,所以这里把”content:// com.google.provider.NotePad/notes”保存到intent里面,这个URI地址指明了数据库中的数据表名(参见以后的NotePadProvider类),也就是保存日志的数据表notes。
Cursor cursor = 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并没有直接去查询数据库,而是通过Content Provider来完成实际的数据库操作,这样就实现了逻辑层和数据库层的分离。
SimpleCursorAdapter adapter =
new SimpleCursorAdapter(this, R.layout.noteslist_item, cursor,
new String[] { Notes.TITLE }, new
int[] { android.R.id.text1 });
setListAdapter(adapter);
查询出日志列表后,构造一个CursorAdapter,并将其作为List View的数据源,从而在界面上显示出日志列表。可以看到,第二个参数是R.layout.noteslist_item,打开对应的noteslist_item.xml文件,
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center_vertical"
android:paddingLeft="5dip"
android:singleLine="true"
/>
就是用来显示一条日志记录的TextView,最后两个字段指明了实际的字段映射关系,通过这个TextView来显示一条日志记录的title字段。
既然有了“日志列表”,就自然要考虑如何处理某一条日志的单击事件,这通过重载onListItemClick方法来完成,
protected
void onListItemClick(ListView l, View v, int position, long= ContentUris.withAppendedId(getIntent().getData(), id);
String action =if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action)) {
// The caller is waiting for us to return a note selected by
// the user. The have clicked on one, so return it now.
setResult(RESULT_OK, newelse// Launch activity to view/edit the currently selected item
startActivity(new Intent(Intent.ACTION_EDIT, uri));
}
}
首先通过”content:// com.google.provider.NotePad/notes”和日志的id 号拼接得到选中日志的真正URI,然后创建一个新的Intent,其操作类型为Intent.ACTION_EDIT,数据域指出待编辑的日志URI(这里只分析else块)。
那么,上面这句startActivity(new Intent(Intent.ACTION_EDIT, uri))执行后会发生什么事情呢?这时候Android系统就跳出来接管了,它会根据intent中的信息找到对应的activity,在这里找到的是NoteEditor这个activity,然后创建这个activity的实例并运行。
那么,Android又是如何找到NoteEditor这个对应的activity的呢?这就是intent发挥作用的时刻了。
new Intent(Intent.ACTION_EDIT, uri)
这里的Intent.ACTION_EDIT=” android.intent.action.EDIT”,另外通过设置断点,我们看下这里的uri值:
可以看到选中的日志条目的URI是:content://com.google.provider.NotePad/notes/1
然后我们再来看下Androidmanfest.xml,其中有这个provider
<provider android:name="NotePadProvider"
android:authorities="com.google.provider.NotePad"
/>
发现没有?它也有com.google.provider.NotePad,这个是content://com.google.provider.NotePad/notes/1的一部分,同时
<activity android:name="NoteEditor"
android:theme="@android:style/Theme.Light"
android:label="@string/title_note"
android:screenOrientation="sensor"
android:configChanges="keyboardHidden|orientation"
>
<!-- This filter says that we can view or edit the data of
a single note -->
<intent-filter android:label="@string/resolve_edit">
<action android:name="android.intent.action.VIEW"
/>
<action android:name="android.intent.action.EDIT"
/>
<action android:name="com.android.notepad.action.EDIT_NOTE"
/>
<category android:name="android.intent.category.DEFAULT"
/>
<data android:mimeType="vnd.android.cursor.item/vnd.google.note"
/>
</intent-filter>
<!-- This filter says that we can create a new note inside
of a directory of notes. -->
<intent-filter>
<action android:name="android.intent.action.INSERT"
/>
<category android:name="android.intent.category.DEFAULT"
/>
<data android:mimeType="vnd.android.cursor.dir/vnd.google.note"
/>
</intent-filter>
</activity>
上面第一个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,然后就加载这个content provider。
<provider android:name="NotePadProvider"
android:authorities="com.google.provider.NotePad"
/>
在这里是NotePadProvider,然后调用NotePadProvider的gettype函数,并把上述URI传给这个函数,函数返回URI所对应的类型(这里返回Notes.CONTENT_ITEM_TYPE,代表一条日志记录,而CONTENT_ITEM_TYPE = " vnd.android.cursor.item/vnd.google.note ")。
publicswitchcasereturncasereturndefaultthrow
new IllegalArgumentException("Unknown URI "
+
上面的sUriMatcher.match是用来检测uri是否能够被处理,而sUriMatcher.match(uri)返回值其实是由
sUriMatcher =
new UriMatcher(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.
<intent-filter android:label="@string/resolve_edit">
<action android:name="android.intent.action.VIEW"
/>
<action android:name="android.intent.action.EDIT"
/>
<action android:name="com.android.notepad.action.EDIT_NOTE"
/>
<category android:name="android.intent.category.DEFAULT"
/>
<data android:mimeType="vnd.android.cursor.item/vnd.google.note"
/>
</intent-filter>
正好NoteEditor这个activity的intent-filter满足上述条件,这样就找到了NoteEditor。于是系统加载这个类并实例化,运行,然后就到了NoteEditor的OnCreate函数中(见后续文章)。
小技巧
1,在命令行中使用”adb shell”命令进入系统中,然后”cd app”进入应用程序所在目录,”rm XXX”就可以删除你指定的apk,从而去掉其在系统顶层界面占据的图标,若两次”cd data”则可以进入应用程序使用的数据目录,你的数据可以保存在这里,例如Notepad就是把其数据库放在它的databases目录下,名为note_pad.db.
2,第一次启动模拟器会比较慢,但以后就别关闭模拟器了,修改代码,调试都不需要再次启动的,直接修改后run或debug就是。
上篇文章分析了NotesList这个Activity,并着重剖析了其中的intent机制,本文将继续上篇未完的工作,以NotesList为实例介绍Android的菜单机制(尤其是动态菜单机制)。
简介
android提供了三种菜单类型,分别为options menu,context menu,sub menu。
options menu就是通过按home键来显示,context menu需要在view上按上2s后显示。这两种menu都有可以加入子菜单,子菜单不能种不能嵌套子菜单。options menu最多只能在屏幕最下面显示6个菜单选项,称为icon menu,icon menu不能有checkable选项。多于6的菜单项会以more icon menu来调出,称为expanded menu。options menu通过activity的onCreateOptionsMenu来生成,这个函数只会在menu第一次生成时调用。任何想改变options menu的想法只能在onPrepareOptionsMenu来实现,这个函数会在menu显示前调用。onOptionsItemSelected 用来处理选中的菜单项。
context menu是跟某个具体的view绑定在一起,在activity种用registerForContextMenu来为某个view注册context menu。context menu在显示前都会调用onCreateContextMenu来生成menu。onContextItemSelected用来处理选中的菜单项。
android还提供了对菜单项进行分组的功能,可以把相似功能的菜单项分成同一个组,这样就可以通过调用setGroupCheckable,setGroupEnabled,setGroupVisible来设置菜单属性,而无须单独设置。
Options Menu
Notepad中使用了options menu和context menu两种菜单。首先来看生成options menu的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。
有意思的是下面这几句代码:
Intent intent =
new Intent(null, getIntent().getData());
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0new ComponentName(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中进行修改,看是否会动态生成出菜单项。
实验一
首先我们来创建一个新的activity作为目标activity,名为HelloAndroid,没有什么功能,就是显示一个界面。
public
class HelloAndroid extendsprotected
void onCreate(Bundle savedInstanceState) {
superthis
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<android:layout_width="fill_parent"="wrap_content" android:id="@+id/TextView01"/>
<Button android:id="@+id/Button01" android:layout_height="wrap_content" android:layout_width="fill_parent" android:text="@string/txtInfo"></Button>
</LinearLayout>
然后修改androidmanfest.xml,加入下面这段配置,让HelloAndroid满足上述两个条件:
<activity android:name="HelloAndroid" android:label="@string/txtInfo">
<intent-filter>
<action android:name="com.android.notepad.action.HELLO_TEST"
/>
<category android:name="android.intent.category.ALTERNATIVE"/>
<data android:mimeType="vnd.android.cursor.dir/vnd.google.note"
/>
</intent-filter>
</activity>
好了,运行下试试,哎,还是没有动态菜单项加入呀!
怎么回事呢?查看代码后发现,原来是onPrepareOptionsMenu搞的鬼!这个函数在onCreateOptionsMenu之后运行,下面这段代码中,由于Menu.CATEGORY_ALTERNATIVE是指向同一个组,所以把onCreateOptionsMenu中设置的菜单项给覆盖掉了,而由于onPrepareOptionsMenu没有给Menu.CATEGORY_ALTERNATIVE附新值,故Menu.CATEGORY_ALTERNATIVE还是为空。
Intent intent =
new Intent(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);
运行后就可以看到动态菜单出来了!
上面这个options menu是在NotesList界面上没有日志列表选中的情况下生成的,若先选中一个日志,然后再点”menu”,则生成的options menu是下面这样的:
、
哎,又动态增加了两个菜单项”Edit note”和”Edit title”,这又是如何动态加入的呢?这就是onPrepareOptionsMenu的功劳了。
Uri uri = ContentUris.withAppendedId(getIntent().getData(), getSelectedItemId());
Intent[] specifics =
new Intent[10] =
new Intent(Intent.ACTION_EDIT, uri);
MenuItem[] items =
new MenuItem[1];
然后为选中的日志创建一个intent,操作类型为Intent.ACTION_EDIT,数据为选中日志的URI.于是会为选中的日志创建一个”Edit note”菜单项。
Intent intent =
new Intent(null, uri);
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, null, specifics, intent, 0
这几句和上面onCreateOptionsMenu函数中类似,用于动态增加菜单项,若某一个activity,其类型是”android.intent.category.ALTERNATIVE”,数据是”vnd.android.cursor.item/vnd.google.note”的话,系统就会为这个activity增加一个菜单项。在androidmanfest.xml中查看后发现,TitleEditor这个activity符合条件,于是系统就为TitleEditor这个activity动态添加一个菜单项”Edit title”。
else {
menu.removeGroup(Menu.CATEGORY_ALTERNATIVE);
}
若日志列表为空,则从菜单中删除组号为Menu.CATEGORY_ALTERNATIVE的菜单项,只剩下”Add note”菜单项。
菜单项选中事件的处理非常简单,通过onOptionsItemSelected来完成,这里只是简单地调用 startActivity(new Intent(Intent.ACTION_INSERT, getIntent().getData()));这个intent的操作类型为Intent.ACTION_INSERT,数据为日志列表的URI,即”content:// com.google.provider.NotePad/notes”
public
boolean onOptionsItemSelected(MenuItem item) {
switchcase// Launch activity to insert a new item
startActivity(new Intent(Intent.ACTION_INSERT, getIntent().getData()));
return
truereturn
super
Context Menu
下面介绍另一种菜单---上下文菜单,这通过重载onCreateContextMenu函数实现。
首先确认已经选中了日志列表中的一个日志,若没选择,则直接返回。Cursor指向选中的日志项。
Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
if (cursor ==
null// For some reason the requested item isn't available, do nothing
return
然后,设置上下文菜单的标题为日志标题
// Setup the menu header
menu.setHeaderTitle(cursor.getString(COLUMN_INDEX_TITLE));
最后为上下文菜单增加一个菜单项
// Add a menu item to delete the note
menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_delete);
对于上下文菜单项选中的事件处理,是通过重载onContextItemSelected实现的。
switchcase// Delete the note that the context menu is for
Uri noteUri = ContentUris.withAppendedId(getIntent().getData(), info.id);
getContentResolver().delete(noteUri, null, nullreturn
truereturn
false
对于日志的删除,首先调用ContentUris.withAppendedId(getIntent().getData(), info.id);来拼接出待删除日志的URI.然后getContentResolver().delete(noteUri, null, null);调用下层的Content Provider去删除此日志。
实验二
来做个简单实验,在上述代码基础上增加一个上下文菜单项。首先在onCreateContextMenu函数中增加一个上下文菜单项:
menu.add(0,MENU_ITEM_INSERT,0,R.string.menu_insert);
然后为其在onContextItemSelected函数中增加一个处理过程:
case MENU_ITEM_INSERT:
{
new AlertDialog.Builder(this).setIcon(R.drawable.app_notes)
.setTitle(R.string.app_name).setMessage(R.string.error_message).setPositiveButton(R.string.button_ok, newpublic
void onClick(DialogInterface dialog, int// TODO Auto-generated method stub
}
}).show();
return
true
实验结果如下:
附记
感谢Evan JIANG对前一篇文章的错误之处进行指正,
“<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
只是指明会在Launcher中显示图标,同一个apk可以在桌面上加很多的图标,分别启动内部不同的多个界面。“,实验后发现确实如此,学习了。
上一篇文章介绍了Android的菜单机制,并动手做了一个实验来探究动态菜单的实验机制。这一篇将重点介绍Activity的生命周期,通过一个简单的实验来摸索状态转换的机制,最后介绍NotePad中使用的自定义控件技术。
Activity的生命周期
Activity类中有许多onXXX形式的函数可以重载,比如onCreate,onStart,onStop,onPause,那么它们的调用顺序到底是如何的呢?下面就通过一个实验来进行分析。在做这个实验之前,我们先得知道如何在Android中进行Log输出的。
我们要使用的是android.util.log类,这个类相当的简单易用,因为它提供的全是一些静态方法:
Log.v(String tag, String msg); //VERBOSE
Log.d(String tag, String msg); //DEBUG
Log.i(String tag, String msg); //INFO
Log.w(String tag, String msg); //WARN
Log.e(String tag, String msg); //ERROR
前面的tag是由我们定义的一个标识,一般可以用“类名_方法名“来定义。要在Eclipse中查看输出的log信息,需要打开Logcat(WindowàShow ViewàotheràAndroidàLogCat即可打开)
实验一
我们要做的实验非常简单,就是有两个Activity(我这里分别叫做frmLogin和hello2),t它们各自有一个button,可以从第一个跳到第二个,也可以从第二个跳回到第一个。
配置文件AndroidManifest.xml非常简单,第二个activity并没有多余的信息需要指定。
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".frmLogin"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"
/>
<category android:name="android.intent.category.LAUNCHER"
/>
</intent-filter>
</activity>
<activity android:name="hello2" android:label="@string/app_name">
</activity>
</application>
第一个activity的代码如下:
public
class frmLogin extendsprivate
final
static String TAG =
"FrmLogin"/** Called when the activity is first created. */public
void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Log.v(TAG,"onCreate");
setContentView(R.layout.main);
thispublic
void setViewOneCommand()
{
Button btn = (Button)findViewById(R.id.btnGo);
btn.setOnClickListener(new View.OnClickListener()
{
public
void onClick(View v)
{
Intent intent =
new Intent();
intent.setClass(frmLogin.this, hello2.class);
startActivity(intent);
finish();
}
});
Button btnExit=(Button)findViewById(R.id.btnExit);
btnExit.setOnClickListener(new View.OnClickListener()
{
public
void onClick(View v)
{
frmLogin.this.finish();
}
});
}
@Override
protected
voidsuper"onDestroy"protected
voidsuper"onPause"protected
voidsuper"onRestart"protected
voidsuper"onResume"protected
voidsuper"onStart"protected
voidsuper"onStop"
我在每个onXXX方法中都加入了log方法,值得注意的一点是按钮单击事件处理函数中,在最后我调用了
finish();待会我会将此行注释掉进行对比实验。
第二个activitysetClass的两个参数反一下,这样就可以简单地实现在两个Activity界面中来回切换的功能了。
下面开始实验,第一个实验室从第一个activity跳到第二个activity(此时第一个关闭),然后从第二个跳回第一个(此时第二个关闭)
运行后观察LogCat,得到如下画面:
然后来进行第二个实验,对代码进行调整,我们把第一个activity中的finish()注释掉,从第一个activity跳到第二个(此时第一个没有关闭),然后第二个直接关闭(则第一个会重新来到前端),结果如图所示,可以看出调用了FrmLogin的onRestart而不是onStart,因为第一个activity只是stop,而并没有被destory掉。
前面两个实验都很好理解,可第三个实验就让我不明白了,过程如下:从第一个activity跳到第二个activity(此时第一个不关闭),然后第二个跳回第一个(此时第二个也不关闭),然后第一个再跳回第二个(此时第一个不关闭),照上面来推断,应该还是会调用onRestart才对,可实际上它调用的却是onStart,why???
这里先不讨论例子了,来看看官方文档对Activity生命周期的介绍。
1.Android用Activity Stack来管理多个Activity,所以呢,同一时刻只会有最顶上的那个Activity是处于active或者running状态。其它的Activity都被压在下面了。
2.如果非活动的Activity仍是可见的(即如果上面压着的是一个非全屏的Activity或透明的Activity),它是处于paused状态的。在系统内存不足的情况下,paused状态的Activity是有可被系统杀掉的。只是不明白,如果它被干掉了,界面上的显示又会变成什么模样?看来下回有必要研究一下这种情况了。
3.几个事件的配对可以比较清楚地理解它们的关系。Create与Destroy配成一对,叫entrie lifetime,在创建时分配资源,则在销毁时释放资源;往上一点还有Start与Stop一对,叫visible lifetime,表达的是可见与非可见这么一个过程;最顶上的就是Resume和Pause这一对了,叫foreground lifetime,表达的了是否处于激活状态的过程。
4.因此,我们实现的Activity派生类,要重载两个重要的方法:onCreate()进行初始化操作,onPause()保存当前操作的结果。
除了Activity Lifecycle以外,Android还有一个Process Lifecycle的说明:
在内存不足的时候,Android是会主动清理门户的,那它又是如何判断哪个process是可以清掉的呢?文档中也提到了它的重要性排序:
1.最容易被清掉的是empty process,空进程是指那些没有Activity与之绑定,也没有任何应用程序组件(如Services或者IntentReceiver)与之绑定的进程,也就是说在这个process中没有任何activity或者service之类的东西,它们仅仅是作为一个cache,在启动新的Activity时可以提高速度。它们是会被优先清掉的。因此建议,我们的后台操作,最好是作成Service的形式,也就是说应该在Activity中启动一个Service去执行这些操作。
2.接下来就是background activity了,也就是被stop掉了那些activity所处的process,那些不可见的Activity被清掉的确是安全的,系统维持着一个LRU列表,多个处于background的activity都在这里面,系统可以根据LRU列表判断哪些activity是可以被清掉的,以及其中哪一个应该是最先被清掉。不过,文档中提到在这个已被清掉的Activity又被重新创建的时候,它的onCreate会被调用,参数就是onFreeze时的那个Bundle。不过这里有一点不明白的是,难道这个Activity被killed时,Android会帮它保留着这个Bundle吗?
3.然后就轮到service process了,这是一个与Service绑定的进程,由startService方法启动。虽然它们不为用户所见,但一般是在处理一些长时间的操作(例如MP3的播放),系统会保护它,除非真的没有内存可用了。
4.接着又轮到那些visible activity了,或者说visible process。前面也谈到这个情况,被Paused的Activity也是有可能会被系统清掉,不过相对来说,它已经是处于一个比较安全的位置了。
5.最安全应该就是那个foreground activity了,不到迫不得已它是不会被清掉的。这种process不仅包括resume之后的activity,也包括那些onReceiveIntent之后的IntentReceiver实例。
在Android Application的生命周期的讨论中,文档也提到了一些需要注意的事项:因为Android应用程序的生存期并不是由应用本身直接控制的,而是由Android系统平台进行管理的,所以,对于我们开发者而言,需要了解不同的组件Activity、Service和IntentReceiver的生命,切记的是:如果组件的选择不当,很有可能系统会杀掉一个正在进行重要工作的进程。
自定义控件
这里主要介绍下“编辑日志”中使用的一个自定义EditText控件,它的效果如下图:
主要功能就是在文本语句之间绘制分割线。
public
static
class LinedEditText extendsprivateprivate// we need this constructor for LayoutInflater
public LinedEditText(Context context, AttributeSet attrs)
{
super(context, attrs);
mRect =
new=
new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0x800000FFprotected
voidint count ===for (int i =
0; i < count; i++int baseline = getLineBounds(i, r);
canvas.drawLine(r.left, baseline +
1, r.right, baseline +
1super
主要工作就是重载onDraw方法,利用从TextView继承下来的getLineCount函数获取文本所占的行数,以及getLineBounds来获取特定行的基准高度值,而且这个函数第二个参数会返回此行的“外包装”值。再利用这些值绘制这一行的线条。
为了让界面的View使用自定义的EditText类,必须在配置文件中进行设置
<view xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/note"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@android:color/transparent"
android:padding="5dip"
android:scrollbars="vertical"
android:fadingEdge="vertical"
android:gravity="top"
android:textSize="22sp"
android:capitalize="sentences"
/>
这里class="com.example.android.notepad.NoteEditor$LinedEditText"就指明了应当使用自定义的LinedEditText类。
上篇文章介绍了Activity的生命周期,并通过一个实验来探索状态转换的机制,然后介绍了应用中使用的一个自定义控件。本文将继续分析NoteEditor这个类和以及Content Provider机制。
NoteEditor深入分析
首先来弄清楚“日志编辑“的状态转换,通过上篇文章的方法来做下面这样一个实验,
首先进入“日志编辑“时会触发onCreate和onResume,然后用户通过Option Menu选择”Edit title”后,会触发onSaveInstanceState和onPause,最后,用户回到编辑界面,则再次触发onResume.
最终通过LogCat可以得到下图:
那么下面就按照上述顺序对此类进行剖析。
首先是onCreate方法,一开始先获取导致进入“日志编辑”界面的intent,分析其操作类型可得知是“编辑日志”还是“新增日志”。
final Intent intent =// Do some setup based on the action being performed.
final String action = intent.getAction();
若是“编辑日志”,则设置当前状态为“编辑”,并保存待编辑日志的URI.
mState == intent.getData();
若是“新增日志”,则设置当前状态为“新增”,并通过content provider向数据库中新增一个“空白日志”,后者返回“空白日志”的URI.
mState == getContentResolver().insert(intent.getData(), null);
然后不管是“编辑”或“新增”,都需要从数据库中读取日志信息(当然,若是“新增”,读出来的肯定是空数据)。
mCursor = managedQuery(mUri, PROJECTION, null, null, null);
最后,类似于web应用中使用的Session,这里也将日志文本保存在InstanceState中,因此,若此activity的实例此前是处于stop状态,则我们可以从它那取出它原本的文本数据.
if (savedInstanceState !=
null= savedInstanceState.getString(ORIGINAL_CONTENT);
}
第二个来分析onResume函数,首先把游标置于第一行(也只有一行)
mCursor.moveToFirst();
然后取出“正文”字段,这时有一个比较有趣的技巧,“设置文本”并不是调用setText,而是调用的setTextKeepState,后者相对于前者有一个优点,就是当界面此前stop掉,现在重新resume回来,那么此前光标所在位置仍然得以保存。而若使用setText,则光标会重置到行首。
String note = mCursor.getString(COLUMN_INDEX_NOTE);
mText.setTextKeepState(note);
最后,将当前编辑的正文保存到一个字符串变量中,用于当activity被暂停时使用。
if (mOriginalContent ==
null)
{
mOriginalContent =
通过前面的图可以得知,activity被暂停时,首先调用的是onSaveInstanceState函数。
outState.putString(ORIGINAL_CONTENT, mOriginalContent);
这里就仅仅将当前正编辑的正文保存到InstanceState中(类似于Session).
最后来看onPause函数,这里首先要考虑的是若activity正要关闭,并且编辑区没有正文,则将此日志删除。
if (isFinishing() && (length ==
0) &&
!mNoteOnly)
{
setResult(RESULT_CANCELED);
deleteNote();
}
ContentValues values =
newif (!mNoteOnly)
{
values.put(Notes.MODIFIED_DATE, System.currentTimeMillis());
if (mState == STATE_INSERT)
{
String title = text.substring(0, Math.min(30if (length >
30)
{
int lastSpace = 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
在生成Option Menu的函数onCreateOptionsMenu中,我们再一次看到下面这段熟悉的代码了:
Intent intent =
new Intent(null, getIntent().getData());
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
new ComponentName(this, NoteEditor.class), null, intent, 0, null);
这种生成动态菜单的机制在这篇文章中已经介绍过了,就不赘述了。
最后,来看下放弃日志和删除日志的实现,由于还没有接触到底层的content provider,这里都是通过getContentResolver()提供的update,delete,insert来向底层的content provider发出请求,由后者完成实际的数据库操作。
private
final
voidif (mCursor !=
nullif (mState ==// Put the original note text back into the database
mCursor.close();
mCursor =
null=
new ContentValues();
values.put(Notes.NOTE, mOriginalContent);
getContentResolver().update(mUri, values, null, nullelse
if (mState ==// We inserted an empty note, make sure to delete it
deleteNote();
}
}
setResult(RESULT_CANCELED);
finish();
}
private
final
voidif (mCursor !=
null)
{
mCursor.close();
mCursor =
null;
getContentResolver().delete(mUri, null, null""
剖析NotePadProvider
NotePadProvider就是所谓的content provider,它继承自android.content.ContentProvider,也是负责数据库层的核心类,主要提供五个功能:
这五个功能分别对应下述五个可以重载的方法:
public
int delete(Uri uri, String selection, String[] selectionArgs)
{
return
0publicreturn
nullpublic Uri insert(Uri uri, ContentValues values)
{
return
nullpublic
booleanreturn
falsepublic Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
{
return
nullpublic
int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs)
{
return
0
这些都要你自己实现,不同的实现就是对应不同的content-provider。但是activity使用content-provider不是直接创建一个对象,然后调用这些具体方法。
而是调用managedQuery,getContentResolver().delete,update等来实现,这些函数其实是先找到符合条件的content-provider,然后再调用具体content-provider的函数来实现,那又是怎么找到content-provider,就是通过uri中的authority来找到content-provider,这些都是通过系统完成,应用程序不用操心,这样就达到了有效地隔离应用和内容提供者的具体实现的目的。
下面这三个字段指明了数据库名称,数据库版本,数据表名称。
private
static
final String DATABASE_NAME =
"note_pad.db";
private
static
final
int DATABASE_VERSION =
2;
private
static
final String NOTES_TABLE_NAME =
"notes";
实际的数据库操作其实都是通过一个私有静态类DatabaseHelper实现的,其构造函数负责创建指定名称和版本的数据库,onCreate函数则创建指定名称和各个数据域的数据表(就是简单的建表SQL语句)。onUpgrade负责删除数据表,再重新建表。
private
static
class DatabaseHelper extends SQLiteOpenHelper
{
DatabaseHelper(Context context)
{
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public
void onCreate(SQLiteDatabase db)
{
db.execSQL("CREATE TABLE "
+ NOTES_TABLE_NAME +
" ("
+ Notes._ID +
" INTEGER PRIMARY KEY,"
+ Notes.TITLE +
" TEXT,"
+ Notes.NOTE +
" TEXT,"
+ Notes.CREATED_DATE +
" INTEGER,"
+ Notes.MODIFIED_DATE +
" INTEGER"
+
");"public
void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
Log.w(TAG, "Upgrading database from version "
+ oldVersion +
" to "
+ newVersion +
", which will destroy all old data""DROP TABLE IF EXISTS notes"
在这篇文章中我们已经见识到了getType函数的用处了,也正是通过它的解析,才能区分开到底是对全部日志还是对某一条日志进行操作。
publicswitchcasereturncasereturndefaultthrow
new IllegalArgumentException("Unknown URI "
+
上面的sUriMatcher.match是用来检测uri是否能够被处理,而sUriMatcher.match(uri)返回值其实是由下述语句决定的。
sUriMatcher =
new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
sNotesProjectionMap这个私有字段是用来在上层应用使用的字段和底层数据库字段之间建立映射关系的,当然,这个程序里两处对应的字段都是一样(但并不需要一样)。
private
static HashMap<String, String>static=
new HashMap<String, String>();
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_DATE);
sNotesProjectionMap.put(Notes.MODIFIED_DATE, Notes.MODIFIED_DATE);
}
数据库的增,删,改,查操作基本都一样,具体可以参考官方文档,这里就仅仅以删除为例进行说明。
一般可以分为三步来完成,首先打开数据库
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
switchcase= db.delete(NOTES_TABLE_NAME, where, whereArgs);
breakcase= uri.getPathSegments().get(1= db.delete(NOTES_TABLE_NAME, Notes._ID +
"="
++ (!TextUtils.isEmpty(where) ?
" AND ("
+ where +
')' : ""break
getContext().getContentResolver().notifyChange(uri, null);
首先我想指出NotePad的一个bug,其实这个小bug在2月份就有人向官方报告了,参见根本就是没有用处的,因为它始终都是false,没有任何变化,所以可以删除掉。
setResult(RESULT_OK, (new Intent()).setAction(mUri.toString()));
setResult(RESULT_CANCELED);
可到底想展示什么技术呢?实际上并没有完整展现出来,这里我对其进行修改后来指明 参见)。
private
static
final
int REQUEST_INSERT =
100;//请求插入标识符
然后修改onOptionsItemSelected函数如下:
public
boolean onOptionsItemSelected(MenuItem item)
{
switchcasethis.startActivityForResult(new Intent(Intent.ACTION_INSERT, getIntent().getData()), REQUEST_INSERT);
return
truereturn
super
protected
void onActivityResult(int requestCode, intif(requestCode ==if(resultCode==RESULT_OK)
{
Log.d(TAG, "OK!!!"else
if(resultCode==RESULT_CANCELED)
{
Log.d(TAG, "CANCELED!!!"
这个系列的前四篇文章介绍了Android sdk中自带的NotePad—Snake(贪食蛇)。本文将主要介绍我对这个示例程序进行的一些修改。
游戏暂停/继续机制
由于原来的代码中在游戏运行时没有提供控制选项(比如暂停/继续),因此除非你死了,否则只能玩到底。我这里对代码进行一些修改,加入一个Option Menu来提供暂停/继续机制。
首先加入一个变量记录游戏当前状态
private
int mState = SnakeView.READY;
然后重载onCreateOptionsMenu函数,创建一个控制菜单项,并对其进行处理,提供暂停/继续机制。
/*
* @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
* @Author:phinecos
* @Date:2009-08-28
*/public
boolean onOptionsItemSelected(MenuItem item)
{
switchcaseif (mState ==//此前状态是"停止",则转为"运行"
mState = SnakeView.RUNNING;
mSnakeView.setMode(SnakeView.RUNNING);
item.setIcon(android.R.drawable.ic_media_pause).setTitle(R.string.cmd_pause);
}
else
if(mState ==//此前状态是"运行",则转为“暂停"
mState = SnakeView.PAUSE;
mSnakeView.setMode(SnakeView.PAUSE);
item.setIcon(android.R.drawable.ic_media_play).setTitle(R.string.cmd_run);
}
else
if(mState ==//此前是"初始状态",则转为"运行"
mState =return
truereturn
super/*
* @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
* @Author:phinecos
* @Date:2009-08-28
*/public
boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
menu.add(0, MENU_CONTROL, 0, R.string.cmd_pause).setIcon(android.R.drawable.ic_media_pause);
return
true
修改后运行截图如下:
当然,这段代码还是有问题的,游戏刚开始时,必须先点击菜单确认,再按上方向键才能开始。(以后再来修改。。。)
穿墙贪食蛇
第二个修改是把这个普通的贪食蛇改成可以穿墙(呵呵,这样就可以不死了。。。)。想必要修改的就是撞墙检测那段代码?没错,就是下面这段!
// Collision detection
// For now we have a 1-square wall around the entire arena
if ((newHead.x <
1) || (newHead.y <
1) || (newHead.x > mXTileCount -
2)|| (newHead.y > mYTileCount -
2//撞墙
return
原来的版本是发现撞墙时就直接判定为失败,我这里做个小小的修改,让它可以穿墙而去:
private
voidboolean growSnake =
false// grab the snake by the head
Coordinate head = mSnakeTrail.get(0=
new Coordinate(1, 1=switchcase=
new Coordinate(head.x +
1breakcase=
new Coordinate(head.x -
1breakcase=
new Coordinate(head.x, head.y -
1breakcase=
new Coordinate(head.x, head.y +
1break//穿墙的处理
if (newHead.x ==
0//穿左边的墙
newHead.x = mXTileCount -
2else
if (newHead.y ==
0//穿上面的墙
newHead.y = mYTileCount -
2else
if (newHead.x == mXTileCount -
1//穿右边的墙
newHead.x =
1else
if (newHead.y == mYTileCount -
1//穿下面的墙
newHead.y =
1// 判断是否撞到自己
int snakelength =for (int snakeindex =
0; snakeindex < snakelength; snakeindex++=if (c.equals(newHead))
{
setMode(LOSE);
return// 判断是否吃掉“苹果”
int applecount =for (int appleindex =
0; appleindex < applecount; appleindex++=if (c.equals(newHead))
{
mAppleList.remove(c);
addRandomApple();
mScore++*=
0.9=
true// push a new head onto the ArrayList and pull off the tail
mSnakeTrail.add(0// except if we want the snake to grow
if (!growSnake)
{
mSnakeTrail.remove(mSnakeTrail.size() -
1int index =
0for (Coordinate c : mSnakeTrail)
{
if (index ==
0)
{
setTile(YELLOW_STAR, c.x, c.y);
}
else
{
setTile(RED_STAR, c.x, c.y);
}
index++
其实修改后的代码非常简单,就是把新节点的值做些处理,让它移动到对应的行/列的头部或尾部即可。
下面就是修改后的“穿墙”贪食蛇的运行截图:
全屏机制
游戏一般都是全屏的,原始代码也考虑到标题栏太过难看了,于是使用下面这句代码就去掉了标题栏:
requestWindowFeature(Window.FEATURE_NO_TITLE);
可还是没有达到全屏的效果,在Android1.5中实现全屏效果非常简单,只需要一句代码即可实现:
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
运行效果如下图所示:
接下来的修改目标是加入得分排行榜机制,再加入一个启动画面和选择菜单。当然,这一篇文章水了点,仅仅记录了自己做的一些小实验,并没有涉及到Snake的代码分析,不过请继续期待下一篇文章。。。
上一篇文章介绍了我对Snake这个示例程序进行的一些简单修改,从这一篇开始真正开始详细分析Snake的具体实现。
本文首先分析Snake的界面Layout实现,并通过一个实验来说明项目中使用的FrameLayout和RelativeLayout这两种布局的效果,其次还介绍了一个用于UI优化的工具---hierarchyviewer。
1, FrameLayout
先来看官方文档的定义:FrameLayout是最简单的一个布局对象。它被定制为你屏幕上的一个空白备用区域,之后你可以在其中填充一个单一对象 — 比如,一张你要发布的图片。所有的子元素将会固定在屏幕的左上角;你不能为FrameLayout中的一个子元素指定一个位置。后一个子元素将会直接在前一个子元素之上进行覆盖填充,把它们部份或全部挡住(除非后一个子元素是透明的)。
有点绕口而且难理解,下面还是通过一个实例来理解吧。我们仿照Snake项目中使用的界面一样,建立一个简单的FrameLayout,其中包含两个Views元素:ImageView和TextView,而后面的TextView还包含在一个RelativeLayout中。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<android:layout_width="fill_parent"
android:layout_height="fill_parent"="center" android:src="@drawable/img0"/>
<android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<android:text="Hello Android"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:textColor="#ffffffff"
android:textSize="24sp"/>
</RelativeLayout>
</FrameLayout>
效果如下图所示:
2,UI优化
Android的tools目录下提供了许多实用工具,这里介绍其中一个用于查看当前UI结构视图的工具hierarchyviewer。
打开tools/hierarchyviewer.bat后,查看上面这个示例的UI结构图可得:
我们可以很明显的看到由红色线框所包含的结构出现了两个framelayout节点,很明显这两个完全意义相同的节点造成了资源浪费(这里可以提醒大家在开发工程中可以习惯性的通过hierarchyViewer查看当前UI资源的分配情况),那么如何才能解决这种问题呢(就当前例子是如何去掉多余的frameLayout节点)?这时候就要用到<merge />标签来处理类似的问题了。我们将上边xml代码中的framLayout替换成merge:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<android:layout_width="fill_parent"
android:layout_height="fill_parent"="center" android:src="@drawable/img0"/>
<android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<android:text="Hello Android"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:textColor="#ffffffff"
android:textSize="24sp"/>
</RelativeLayout>
</merge >
运行程序后在Emulator中显示的效果是一样的,可是通过hierarchyviewer查看的UI结构是有变化的,当初多余的FrameLayout节点被合并在一起了,或者可以理解为将merge标签中的子集直接加到Activity的FrameLayout跟节点下(这里需要提醒大家注意:所有的Activity视图的根节点都是frameLayout)。如果你所创建的Layout并不是用framLayout作为根节点(而是应用LinerLayout等定义root标签),就不能应用上边的例子通过merge来优化UI结构。
3,RelativeLayout
RelativeLayout允许子元素指定他们相对于其它元素或父元素的位置(通过ID指定)。因此,你可以以右对齐,或上下,或置于屏幕中央的形式来排列两个元素。元素按顺序排列,因此如果第一个元素在屏幕的中央,那么相对于这个元素的其它元素将以屏幕中央的相对位置来排列。如果使用XML来指定这个layout,在你定义它之前,被关联的元素必须定义。
解释起来也比较麻烦,不过我做个对比实验可以明白它的用处了,试着把上面例子里的RelativeLayout节点去掉看看,效果如下图所示,可以看到由于FrameLayout的原因,都在左上角靠拢了,而使用了RelativeLayout,则可以让TextView相对于屏幕居中。
有了上述Layout的基础知识,我们再来看Snake的布局文件就很好理解了,就是一个SnakeView和一个TextView,启动后,后者会覆盖在前者上面。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<android:id="@+id/snake"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tileSize="24"
/>
<android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<android:id="@+id/text"
android:text="@string/snake_layout_text_text"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:textColor="#ff8888ff"
android:textSize="24sp"/>
</RelativeLayout>
</FrameLayout>
也就是这样的效果:
那么相应的代码是如何实现这个效果的呢?
SnakeView有一个私有变量存放覆盖其上的TextView:
private TextView mStatusText;
在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的状态为Ready
mSnakeView.setMode(SnakeView.READY);
这一句代码会调用下述函数:
public
void setMode(intint oldMode ==if (newMode == RUNNING & oldMode !=//游戏进入“运行”状态,则隐藏文字信息
mStatusText.setVisibility(View.INVISIBLE);
update();
return//根据新状态,设置待显示的文字信息
Resources res = getContext().getResources();
CharSequence str =
""if (newMode ==//新状态为“暂停”
str = res.getText(R.string.mode_pause);
}
if (newMode ==//新状态为“准备开始”
str = res.getText(R.string.mode_ready);
}
if (newMode ==//新状态为“游戏失败”
str = res.getString(R.string.mode_lose_prefix) ++ res.getString(R.string.mode_lose_suffix);
}
//设置文字信息并显示
mStatusText.setText(str);
mStatusText.setVisibility(View.VISIBLE);
}
在mStatusText.setVisibility(View.VISIBLE);这一句后就显示出上面这个游戏起始画面了。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<android:id="@+id/snake"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tileSize="24"
/>
<android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<android:id="@+id/text"
android:text="@string/snake_layout_text_text"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:textColor="#ff8888ff"
android:textSize="24sp"/>
</RelativeLayout>
</FrameLayout>
也就是这样的效果:
那么相应的代码是如何实现这个效果的呢?
SnakeView有一个私有变量存放覆盖其上的TextView:
private TextView mStatusText;
在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的状态为Ready
mSnakeView.setMode(SnakeView.READY);
这一句代码会调用下述函数:
public
void setMode(intint oldMode ==if (newMode == RUNNING & oldMode !=//游戏进入“运行”状态,则隐藏文字信息
mStatusText.setVisibility(View.INVISIBLE);
update();
return//根据新状态,设置待显示的文字信息
Resources res = getContext().getResources();
CharSequence str =
""if (newMode ==//新状态为“暂停”
str = res.getText(R.string.mode_pause);
}
if (newMode ==//新状态为“准备开始”
str = res.getText(R.string.mode_ready);
}
if (newMode ==//新状态为“游戏失败”
str = res.getString(R.string.mode_lose_prefix) ++ res.getString(R.string.mode_lose_suffix);
}
//设置文字信息并显示
mStatusText.setText(str);
mStatusText.setVisibility(View.VISIBLE);
}
在mStatusText.setVisibility(View.VISIBLE);这一句后就显示出上面这个游戏起始画面了。
1, FrameLayout
先来看官方文档的定义:FrameLayout是最简单的一个布局对象。它被定制为你屏幕上的一个空白备用区域,之后你可以在其中填充一个单一对象 — 比如,一张你要发布的图片。所有的子元素将会固定在屏幕的左上角;你不能为FrameLayout中的一个子元素指定一个位置。后一个子元素将会直接在前一个子元素之上进行覆盖填充,把它们部份或全部挡住(除非后一个子元素是透明的)。
有点绕口而且难理解,下面还是通过一个实例来理解吧。我们仿照Snake项目中使用的界面一样,建立一个简单的FrameLayout,其中包含两个Views元素:ImageView和TextView,而后面的TextView还包含在一个RelativeLayout中。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<android:layout_width="fill_parent"
android:layout_height="fill_parent"="center" android:src="@drawable/img0"/>
<android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<android:text="Hello Android"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:textColor="#ffffffff"
android:textSize="24sp"/>
</RelativeLayout>
</FrameLayout>
效果如下图所示:
2,UI优化
Android的tools目录下提供了许多实用工具,这里介绍其中一个用于查看当前UI结构视图的工具hierarchyviewer。
打开tools/hierarchyviewer.bat后,查看上面这个示例的UI结构图可得:
我们可以很明显的看到由红色线框所包含的结构出现了两个framelayout节点,很明显这两个完全意义相同的节点造成了资源浪费(这里可以提醒大家在开发工程中可以习惯性的通过hierarchyViewer查看当前UI资源的分配情况),那么如何才能解决这种问题呢(就当前例子是如何去掉多余的frameLayout节点)?这时候就要用到<merge />标签来处理类似的问题了。我们将上边xml代码中的framLayout替换成merge:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<android:layout_width="fill_parent"
android:layout_height="fill_parent"="center" android:src="@drawable/img0"/>
<android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<android:text="Hello Android"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:textColor="#ffffffff"
android:textSize="24sp"/>
</RelativeLayout>
</merge >
运行程序后在Emulator中显示的效果是一样的,可是通过hierarchyviewer查看的UI结构是有变化的,当初多余的FrameLayout节点被合并在一起了,或者可以理解为将merge标签中的子集直接加到Activity的FrameLayout跟节点下(这里需要提醒大家注意:所有的Activity视图的根节点都是frameLayout)。如果你所创建的Layout并不是用framLayout作为根节点(而是应用LinerLayout等定义root标签),就不能应用上边的例子通过merge来优化UI结构。
3,RelativeLayout
RelativeLayout允许子元素指定他们相对于其它元素或父元素的位置(通过ID指定)。因此,你可以以右对齐,或上下,或置于屏幕中央的形式来排列两个元素。元素按顺序排列,因此如果第一个元素在屏幕的中央,那么相对于这个元素的其它元素将以屏幕中央的相对位置来排列。如果使用XML来指定这个layout,在你定义它之前,被关联的元素必须定义。
解释起来也比较麻烦,不过我做个对比实验可以明白它的用处了,试着把上面例子里的RelativeLayout节点去掉看看,效果如下图所示,可以看到由于FrameLayout的原因,都在左上角靠拢了,而使用了RelativeLayout,则可以让TextView相对于屏幕居中。
有了上述Layout的基础知识,我们再来看Snake的布局文件就很好理解了,就是一个SnakeView和一个TextView,启动后,后者会覆盖在前者上面。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<android:id="@+id/snake"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tileSize="24"
/>
<android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<android:id="@+id/text"
android:text="@string/snake_layout_text_text"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:textColor="#ff8888ff"
android:textSize="24sp"/>
</RelativeLayout>
</FrameLayout>
也就是这样的效果:
那么相应的代码是如何实现这个效果的呢?
SnakeView有一个私有变量存放覆盖其上的TextView:
private TextView mStatusText;
在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的状态为Ready
mSnakeView.setMode(SnakeView.READY);
这一句代码会调用下述函数:
public
void setMode(intint oldMode ==if (newMode == RUNNING & oldMode !=//游戏进入“运行”状态,则隐藏文字信息
mStatusText.setVisibility(View.INVISIBLE);
update();
return//根据新状态,设置待显示的文字信息
Resources res = getContext().getResources();
CharSequence str =
""if (newMode ==//新状态为“暂停”
str = res.getText(R.string.mode_pause);
}
if (newMode ==//新状态为“准备开始”
str = res.getText(R.string.mode_ready);
}
if (newMode ==//新状态为“游戏失败”
str = res.getString(R.string.mode_lose_prefix) ++ res.getString(R.string.mode_lose_suffix);
}
//设置文字信息并显示
mStatusText.setText(str);
mStatusText.setVisibility(View.VISIBLE);
}
在mStatusText.setVisibility(View.VISIBLE);这一句后就显示出上面这个游戏起始画面了。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<android:id="@+id/snake"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tileSize="24"
/>
<android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<android:id="@+id/text"
android:text="@string/snake_layout_text_text"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:textColor="#ff8888ff"
android:textSize="24sp"/>
</RelativeLayout>
</FrameLayout>
也就是这样的效果:
那么相应的代码是如何实现这个效果的呢?
SnakeView有一个私有变量存放覆盖其上的TextView:
private TextView mStatusText;
在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的状态为Ready
mSnakeView.setMode(SnakeView.READY);
这一句代码会调用下述函数:
public
void setMode(intint oldMode ==if (newMode == RUNNING & oldMode !=//游戏进入“运行”状态,则隐藏文字信息
mStatusText.setVisibility(View.INVISIBLE);
update();
return//根据新状态,设置待显示的文字信息
Resources res = getContext().getResources();
CharSequence str =
""if (newMode ==//新状态为“暂停”
str = res.getText(R.string.mode_pause);
}
if (newMode ==//新状态为“准备开始”
str = res.getText(R.string.mode_ready);
}
if (newMode ==//新状态为“游戏失败”
str = res.getString(R.string.mode_lose_prefix) ++ res.getString(R.string.mode_lose_suffix);
}
//设置文字信息并显示
mStatusText.setText(str);
mStatusText.setVisibility(View.VISIBLE);
}
在mStatusText.setVisibility(View.VISIBLE);这一句后就显示出上面这个游戏起始画面了。
上一篇文章分析了Snake的界面Layout实现,本文将关注游戏主界面这个View是如何实现的,并提出了我的一些困惑之处,希望有朋友能帮忙解惑。
Snake这个项目把主界面剖成界面UI和游戏逻辑两层,最基础的界面UI部分用父类TileView来表示,子类SnakeView是在TileView的UI基础上,加入相应的游戏控制逻辑,从而实现了两者的分离,这对于游戏的修改非常有用。
首先来看界面UI部分,基本思想大家都非常清楚:把整个屏幕看做一个二维数组,每一个元素可以视为一个方块,因此每个方格在游戏进行过程中可以处于不同的状态,比如空闲,墙,苹果,贪食蛇(蛇身或蛇头)。我们在操作游戏的过程,其实就是不断修改相应方格的状态,然后再让整个View去重绘制自身(当然,还需要加入一些游戏当前所处状态(失败或成功)的判定机制)。
TileView的数据成员如下:
//方格的大小
protected
static
int mTileSize;
//方格的行数和列数
protected
static
int mXTileCount;
protected
static
int mYTileCount;
//xy坐标系的偏移量
private
static
int mXOffset;
private
static
int mYOffset;
//存储三种方格的图标文件
private Bitmap[] mTileArray;
//二维方格地图
private
int[][] mTileGrid;
那么在游戏还未正式开始前,首先要做一些初始化工作,在View第一次加载时会首先调用onSizeChanged,这里就是做这些事的最好时机。
//方格的大小
protected
static
int mTileSize;
//方格的行数和列数
protected
static
int mXTileCount;
protected
static
int mYTileCount;
//xy坐标系的偏移量
private
static
int mXOffset;
private
static
int mYOffset;
//存储三种方格的图标文件
private Bitmap[] mTileArray;
//二维方格地图
private
int[][] mTileGrid;
那么在游戏还未正式开始前,首先要做一些初始化工作,在View第一次加载时会首先调用onSizeChanged,这里就是做这些事的最好时机。
@Override
protected
void onSizeChanged(int w, int h, int oldw, int//计算屏幕中可放置的方格的行数和列数
mXTileCount = (int) Math.floor(w /= (int) Math.floor(h /= ((w - (mTileSize * mXTileCount)) /
2= ((h - (mTileSize * mYTileCount)) /
2=
new
int[mXTileCount][mYTileCount];
clearTiles();
}
注意模拟器屏幕默认的像素是320×400,而代码中默认的方格大小为12,因此屏幕上放置的方格数为26×40,把屏幕剖分成这么大后,再设置一个相应的二维int型数组来记录每一个方格的状态,根据方格的状态,可以从mTileArray保存的图标文件中读取对应的状态图标。
第一次调用完onSizeChanged后,会紧跟着第一次来调用onDraw来绘制View自身,当然,此时由于所有方格的状态都是0,所以它在屏幕上等于什么也不会去绘制。
public
voidsuperfor (int x =
0; x < mXTileCount; x +=
1for (int y =
0; y < mYTileCount; y +=
1if (mTileGrid[x][y] >
0)
{
canvas.drawBitmap(mTileArray[mTileGrid[x][y]],
mXOffset + x * mTileSize,
mYOffset + y * mTileSize,
mPaint);
}
}
}
}
onDraw要做的工作非常简单,就是扫描每一个方格,根据方格当前状态,从图标文件中选择对应的图标绘制到这个方格上。当然这个onDraw在游戏进行过程中,会不断地被调用,从而界面不断被更新。
游戏逻辑部分
再来看子类SnakeView是如何在父类TileView的基础上,加入特定的游戏逻辑,从而完成Snake这个程序的。
//方格的大小
protected
static
int mTileSize;
//方格的行数和列数
protected
static
int mXTileCount;
protected
static
int mYTileCount;
//xy坐标系的偏移量
private
static
int mXOffset;
private
static
int mYOffset;
//存储三种方格的图标文件
private Bitmap[] mTileArray;
//二维方格地图
private
int[][] mTileGrid;
那么在游戏还未正式开始前,首先要做一些初始化工作,在View第一次加载时会首先调用onSizeChanged,这里就是做这些事的最好时机。
//方格的大小
protected
static
int mTileSize;
//方格的行数和列数
protected
static
int mXTileCount;
protected
static
int mYTileCount;
//xy坐标系的偏移量
private
static
int mXOffset;
private
static
int mYOffset;
//存储三种方格的图标文件
private Bitmap[] mTileArray;
//二维方格地图
private
int[][] mTileGrid;
那么在游戏还未正式开始前,首先要做一些初始化工作,在View第一次加载时会首先调用onSizeChanged,这里就是做这些事的最好时机。
@Override
protected
void onSizeChanged(int w, int h, int oldw, int//计算屏幕中可放置的方格的行数和列数
mXTileCount = (int) Math.floor(w /= (int) Math.floor(h /= ((w - (mTileSize * mXTileCount)) /
2= ((h - (mTileSize * mYTileCount)) /
2=
new
int[mXTileCount][mYTileCount];
clearTiles();
}
注意模拟器屏幕默认的像素是320×400,而代码中默认的方格大小为12,因此屏幕上放置的方格数为26×40,把屏幕剖分成这么大后,再设置一个相应的二维int型数组来记录每一个方格的状态,根据方格的状态,可以从mTileArray保存的图标文件中读取对应的状态图标。
第一次调用完onSizeChanged后,会紧跟着第一次来调用onDraw来绘制View自身,当然,此时由于所有方格的状态都是0,所以它在屏幕上等于什么也不会去绘制。
public
voidsuperfor (int x =
0; x < mXTileCount; x +=
1for (int y =
0; y < mYTileCount; y +=
1if (mTileGrid[x][y] >
0)
{
canvas.drawBitmap(mTileArray[mTileGrid[x][y]],
mXOffset + x * mTileSize,
mYOffset + y * mTileSize,
mPaint);
}
}
}
}
onDraw要做的工作非常简单,就是扫描每一个方格,根据方格当前状态,从图标文件中选择对应的图标绘制到这个方格上。当然这个onDraw在游戏进行过程中,会不断地被调用,从而界面不断被更新。
游戏逻辑部分
再来看子类SnakeView是如何在父类TileView的基础上,加入特定的游戏逻辑,从而完成Snake这个程序的。
private ArrayList<Coordinate> mSnakeTrail =
new ArrayList<Coordinate>();//组成贪食蛇的方格列表
private ArrayList<Coordinate> mAppleList =
new ArrayList<Coordinate>();//苹果方格列表
由于SnakeView从TileView继承而来,则可以说它已经拥有这个二维方格地图了(只是此时地图里的所有方格状态都是0)。那么它有了这么一个二维方格地图,如何去初始化这个地图呢?这在initNewGame函数中实现。
private
void//清空蛇和苹果占据的方格
mSnakeTrail.clear();
mAppleList.clear();
//目前组成蛇的方格式固定的,而且方向也固定朝北
mSnakeTrail.add(new Coordinate(7, 7new Coordinate(6, 7new Coordinate(5, 7new Coordinate(4, 7new Coordinate(3, 7new Coordinate(2, 7=//随即加入苹果
for (int i =
0; i < nApples; ++i)
{
addRandomApple();
}
//初始化运动速率和玩家成绩
mMoveDelay =
600=
0
想象下对整个游戏屏幕拍张照,然后对其下一个状态再拍张照,那么两张照片之间的区别是怎么产生的呢?对于系统来说,它只知道不断调用onDraw,后者负责对整个屏幕进行绘制,那要产生两个屏幕之间的差异,肯定要通过一些手段对某些数据结构(比如这里的二维方格地图)进行调整(比如用户的控制指令,定时器等),然后等到下一次onDraw时就会把这些更改在界面上反映出来。
这里要着重说明下private long mMoveDelay = 600;这个成员变量,虽然很不起眼,但仔细考虑它的作用就会发现很有趣,那么改变它的大小到底是如何让我们感觉到游戏变快或变慢呢?
可以打个简单的比方,在时刻0游戏启动,首先把蛇和苹果的位置都在方格地图上作好了标记,然后我们在update函数中修改蛇身让蛇向北前进一步,而这个改变此时还只是停留在内部的核心数据结构上(即二维方格地图),还没有在界面上显示出来。当然,我们马上想到要想让这更改显示出来,让系统调用onDraw去绘制不就完了吗?可是问题是我们不知道系统是隔多长时间去调用onDraw函数,于是mMoveDelay此时就发挥作用了,通过它就可以设置休眠的时间,等时间一到,马上就会通知SnakeView去重绘制。你可以试试把mMoveDelay数值调大,就会看出我上面提到的“拍照“的效果。
写过JavaScript或者ActionScript的开发者,对于setInterval的用法会非常了解。那么在Android中如何实现setInterval的方法呢?其中有两种方法可以实现类似的功能,其中一个是在线程中调用Handler方法,另外一个是应用Timer。Snake中使用了前者
class RefreshHandler extendspublic
void//“苏醒”后的处理
SnakeView.thisthispublic
void sleep(long//休眠delayMillis毫秒
this.removeMessages(0);
sendMessageDelayed(obtainMessage(0
而实际调用的处理函数update就可以说是整个游戏的引擎,正是由于它的工作(修改蛇和苹果的状态到一个新的状态,然后休眠自己,然后等到苏醒后在Handler中就会让系统区绘制上次修改过的二维方块地图,然后再次调用update,如此循环反复,生生不息),才使得游戏不断被推进,因此,比做“引擎“不为过。
public
voidif (mMode ==long now =if (now - mLastMove > mMoveDelay)
{
clearTiles();
updateWalls();
updateSnake();
updateApples();
mLastMove = now;
}
mRedrawHandler.sleep(mMoveDelay);
}
}
既然update是游戏的动力,要让游戏停止下来只要不再调用update就可以了(因为此时其实是画面静止了),因此游戏进入暂停(这个状态还可以转为“运行“,其实就是继续可以修改,再绘制),若进入失败(其实此时二维方块地图还停留在最后一个画面处,这也是为什么在开始时要首先清理掉整个地图)【这一点,可以在游戏失败后,再次开始新游戏,此时通过设置的断点即可观察到上次游戏运行时的底层数据】。
一点困惑
可是个人认为Snake下面这段代码读起来有点怪,有点像一个“先有鸡,还是先有蛋?“的问题,导致我的思维逻辑上出现一个“怪圈“。
public
void handleMessage(Message msg)
{
SnakeView.thisthis
按照这段代码的意思来看,当休眠的时间已经到了,首先去调用update,即为下一次绘制做准备工作,再让自己休眠起来,最后通知系统重绘制自己。
哎,这让我难以理解,还是回到时刻0的例子来说,在时刻0时让蛇身向北前进了一步(指的是底层的二维方格地图的修改,不是界面),然后让自己休眠0.6毫秒,当时间到了,首先去调用update方法,那么就又会让蛇身做出修改,也就是把上一次还没绘制的覆盖掉了(那么上一次的修改岂不是白费,还没画上去呢),更何况在update中又会让自己去休眠(还没调用invalidate,怎么又去休眠了?),又怎么还能去通知系统调用我的onDraw方法呢?也就是说invalidate根本没有执行???
按我的理解,应该把顺序颠倒一下,先通知系统去调用onDraw方法重绘,使得上一次对底层二维方格地图的修改显示出来,然后再去为下一次修改做准备工作,最后让自己进入休眠,等待苏醒过来,如此循环反复。实验证明,颠倒过来也是正确的,不过关于这一个迷惑我的地方,希望有朋友能指点我一下!
记得在javascript里使用setInterval时,也是先写处理逻辑,然后在末尾处写上一句setInterval(这也是我习惯的思维方式了),难道google上面这种写法有何深意?
此外,感觉每次绘制时都重新绘制墙壁,有点浪费时间,因为墙壁根本没有任何变化的。还有就是mLastMove这个变量设置的初衷是保证当前时间点距上一次变化已经过去了mMoveDelay毫秒,可是既然已经用了sleep机制,再使用这个时间差看上去并无必要。
上一篇文章分析了小游戏Snake的基本框架,本文将分析Android自带的另一个小游戏LunarLander,它与前者的“定时器+系统调用onDraw”架构相比,由于采用了“多线程+强制自行绘制”的架构思路,因而更为实用。
就界面Layout来说,这个程序其实和Snake没有什么不同,同样是采用了FrameLayout,而且游戏的主界面由一个自定义的View来实现,这里是LunarView。读过上一篇文章的朋友也许会发现,Snake的架构是“定时器+系统调用onDraw”来实现的,这里有一个最大的缺陷就是onDraw是由Android系统来调用的,我们只能依赖它,却无法自行控制。这就好比一个黑盒,当然,总是能把我们要的东西给做出来,可却无法控制其做事的细节,这对于游戏这样高效率的东西可是不利的,因此最好的解决之道当然是把绘制这部分工作自己”承包“过来,告别吃大锅饭的,进入”联产承包制”时代。
此外,由于游戏的本质就是连续两帧图片之间发生些许差异,那么要不断催生这种差异的发生,只要有某种连续不断发生的事件在进行就可以,例如Snake中使用的定时器,就是在不断地产生这种“差异源”,与此类似,一个线程也是不断在运行中,通过它也是可以不断产生这种“差异源”的。
SurfaceView初探
如果说Snake中使用的Layout加自定义View是一把小型武器的话,那在SurfaceView对于android中游戏的开发来说就算是重型武器了。我们使用前者时总是容易把游戏中某个对象(比如上文的每一个方格)当做一个小组件来处理,而后者则根本没有这种划分的概念,在它眼中,所有东西都是在Canvas(画布)中自行绘制出来的(背景,人物等)。
SurfaceView提供直接访问一个可画图的界面,可以控制在界面顶部的子视图层。SurfaceView是提供给需要直接画像素而不是使用窗体部件的应用使用的。Android图形系统中一个重要的概念和线索是surface。View及其子类(如TextView, Button)
要画在surface上。每个surface创建一个Canvas对象(但属性时常改变),用来管理view在surface上的绘图操作,如画点画线。还要注意的是,使用它的时候,一般都是出现在最顶层的:The view hierarchy will take care of correctly compositing
with the Surface any siblings of the SurfaceView that would normally appear on top of it.
使用的SurfaceView的时候,一般情况下还要对其进行创建,销毁,改变时的情况进行监视,这就要用到SurfaceHolder.Callback.
class LunarView extends SurfaceView implementspublic
void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
//在surface的大小发生改变时激发
public
void surfaceCreated(SurfaceHolder holder){}
//在创建时激发,一般在这里调用画图的线程。
public
void surfaceDestroyed(SurfaceHolder holder) {}
//销毁时激发,一般在这里将画图的线程停止、释放。
}
surfaceCreated会首先被调用,然后是surfaceChanged,当程序结束时会调用surfaceDestroyed。
下面来看看LunarView最重要的成员变量,也就是负责这个View所有处理的线程
private LunarThread thread; // 实际工作线程
thread =
new LunarThread(holder, context, new Handler() {
@Override
public
void handleMessage(Message m)
{
mStatusText.setVisibility(m.getData().getInt("viz"));
mStatusText.setText(m.getData().getString("text"
这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。
由于有了LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。
public
void surfaceChanged(SurfaceHolder holder, int format, int width,int height)
{
thread.setSurfaceSize(width, height);
}
public
void//启动工作线程结束
thread.setRunning(truepublic
void surfaceDestroyed(SurfaceHolder holder)
{
boolean retry =
truefalsewhiletry
{//等待工作线程结束,主线程才结束
thread.join();
retry =
falsecatch (InterruptedException e)
{
}
}
}
工作线程LunarThread
由于SurfaceHolder是一个共享资源,因此在对其操作时都应该实行“互斥操作“,即需要使用synchronized进行”封锁“机制。
再来讨论下为什么要使用消息机制来更新界面的文字信息呢?其实原因是这样的,渲染文字的工作实际上是主线程(也就是LunarView类)的父类View的工作,而并不属于工作线程LunarThread,因此在工作线程中式无法控制的。所以我们改为向主线程发送一个Message来代替,让主线程通过Handler对接收到的消息进行处理,从而更新界面文字信息。再回顾上一篇SnakeView里的文字信息更新,由于是SnakeView自己(就这一个线程)对其包含的TextView做控制,当然没有这样的问题了。
public
void setState(int mode, CharSequence message)
{
synchronized (mSurfaceHolder)
{
mMode =if (mMode ==//运行中,隐藏界面文字信息
Message msg = mHandler.obtainMessage();
Bundle b =
new"text", """viz", View.INVISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
else//根据当前状态设置文字信息
mRotating =
0=
false= mContext.getResources();
CharSequence str =
""if (mMode === res.getText(R.string.mode_ready);
else
if (mMode === res.getText(R.string.mode_pause);
else
if (mMode === res.getText(R.string.mode_lose);
else
if (mMode === res.getString(R.string.mode_win_prefix)
+ mWinsInARow +
"
"
+ res.getString(R.string.mode_win_suffix);
if (message !=
null= message +
"/n"
+ str;
}
if (mMode == STATE_LOSE)
mWinsInARow =
0= mHandler.obtainMessage();
Bundle b =
new"text", str.toString());
b.putInt("viz", View.VISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
}
}
下面就是LunaThread这个工作线程的执行函数了,它一直不断在重复做一件事情:锁定待绘制区域(这里是整个屏幕),若游戏还在进行状态,则更新底层的数据,然后直接强制界面重新绘制。
public
voidwhile (mRun)
{
Canvas c =
nulltry//锁定待绘制区域
c = mSurfaceHolder.lockCanvas(nullsynchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
updatePhysics();//更新底层数据,判断游戏状态
doDraw(c);//强制重绘制
}
}
finallyif (c !=
null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
强行自绘制
doDraw这段代码就是在自己的Canvas上进行绘制,具体的绘制就不解释了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代码是下面这个:
canvas.save();
canvas.rotate((float) mHeading, (float- (floatif (mMode == STATE_LOSE) {
mCrashedImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mCrashedImage.draw(canvas);
} else
if (mEngineFiring) {
mFiringImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mFiringImage.draw(canvas);
} else {
mLanderImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mLanderImage.draw(canvas);
}
canvas.restore();
在绘制火箭的前后,调用了save()和restore(),它是先保存当前矩阵,将其复制到一个私有堆栈上。然后接下来对rotate的调用还是在原有的矩阵上进行操作,但当restore调用后,以前保存的设置又重新恢复。不过,在这里还是看不出有什么用处。。。
LunarLancher的暂停其实并没有不再强制重绘制,而是没有对底层的数据做任何修改,依然绘制同一帧画面,而继续则是把mLastTime设置为当前时间+100毫秒的时间点,因为以前暂停时mLastTime就不再更新了,这样做事为了与当前时间同步起来。
public
void//暂停
synchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
setState(STATE_PAUSE);
}
}
public
void//// Move the real time clock up to now
synchronized (mSurfaceHolder)
{
mLastTime = System.currentTimeMillis() +
100;
}
setState(STATE_RUNNING);
}
这样做的目的是为了制造“延迟“的效果,都是因为updatePhysics函数里这两句
if (mLastTime > now) returndouble elapsed = (now - mLastTime) /
1000.0;
至于游戏的控制逻辑和判定部分就不介绍了,没有多大意思。
private LunarThread thread; // 实际工作线程
thread =
new LunarThread(holder, context, new Handler() {
@Override
public
void handleMessage(Message m)
{
mStatusText.setVisibility(m.getData().getInt("viz"));
mStatusText.setText(m.getData().getString("text"
这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。
由于有了LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。
public
void surfaceChanged(SurfaceHolder holder, int format, int width,int height)
{
thread.setSurfaceSize(width, height);
}
public
void//启动工作线程结束
thread.setRunning(truepublic
void surfaceDestroyed(SurfaceHolder holder)
{
boolean retry =
truefalsewhiletry
{//等待工作线程结束,主线程才结束
thread.join();
retry =
falsecatch (InterruptedException e)
{
}
}
}
工作线程LunarThread
由于SurfaceHolder是一个共享资源,因此在对其操作时都应该实行“互斥操作“,即需要使用synchronized进行”封锁“机制。
再来讨论下为什么要使用消息机制来更新界面的文字信息呢?其实原因是这样的,渲染文字的工作实际上是主线程(也就是LunarView类)的父类View的工作,而并不属于工作线程LunarThread,因此在工作线程中式无法控制的。所以我们改为向主线程发送一个Message来代替,让主线程通过Handler对接收到的消息进行处理,从而更新界面文字信息。再回顾上一篇SnakeView里的文字信息更新,由于是SnakeView自己(就这一个线程)对其包含的TextView做控制,当然没有这样的问题了。
public
void setState(int mode, CharSequence message)
{
synchronized (mSurfaceHolder)
{
mMode =if (mMode ==//运行中,隐藏界面文字信息
Message msg = mHandler.obtainMessage();
Bundle b =
new"text", """viz", View.INVISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
else//根据当前状态设置文字信息
mRotating =
0=
false= mContext.getResources();
CharSequence str =
""if (mMode === res.getText(R.string.mode_ready);
else
if (mMode === res.getText(R.string.mode_pause);
else
if (mMode === res.getText(R.string.mode_lose);
else
if (mMode === res.getString(R.string.mode_win_prefix)
+ mWinsInARow +
"
"
+ res.getString(R.string.mode_win_suffix);
if (message !=
null= message +
"/n"
+ str;
}
if (mMode == STATE_LOSE)
mWinsInARow =
0= mHandler.obtainMessage();
Bundle b =
new"text", str.toString());
b.putInt("viz", View.VISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
}
}
下面就是LunaThread这个工作线程的执行函数了,它一直不断在重复做一件事情:锁定待绘制区域(这里是整个屏幕),若游戏还在进行状态,则更新底层的数据,然后直接强制界面重新绘制。
public
voidwhile (mRun)
{
Canvas c =
nulltry//锁定待绘制区域
c = mSurfaceHolder.lockCanvas(nullsynchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
updatePhysics();//更新底层数据,判断游戏状态
doDraw(c);//强制重绘制
}
}
finallyif (c !=
null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
强行自绘制
doDraw这段代码就是在自己的Canvas上进行绘制,具体的绘制就不解释了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代码是下面这个:
canvas.save();
canvas.rotate((float) mHeading, (float- (floatif (mMode == STATE_LOSE) {
mCrashedImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mCrashedImage.draw(canvas);
} else
if (mEngineFiring) {
mFiringImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mFiringImage.draw(canvas);
} else {
mLanderImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mLanderImage.draw(canvas);
}
canvas.restore();
在绘制火箭的前后,调用了save()和restore(),它是先保存当前矩阵,将其复制到一个私有堆栈上。然后接下来对rotate的调用还是在原有的矩阵上进行操作,但当restore调用后,以前保存的设置又重新恢复。不过,在这里还是看不出有什么用处。。。
LunarLancher的暂停其实并没有不再强制重绘制,而是没有对底层的数据做任何修改,依然绘制同一帧画面,而继续则是把mLastTime设置为当前时间+100毫秒的时间点,因为以前暂停时mLastTime就不再更新了,这样做事为了与当前时间同步起来。
public
void//暂停
synchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
setState(STATE_PAUSE);
}
}
public
void//// Move the real time clock up to now
synchronized (mSurfaceHolder)
{
mLastTime = System.currentTimeMillis() +
100;
}
setState(STATE_RUNNING);
}
这样做的目的是为了制造“延迟“的效果,都是因为updatePhysics函数里这两句
if (mLastTime > now) returndouble elapsed = (now - mLastTime) /
1000.0;
至于游戏的控制逻辑和判定部分就不介绍了,没有多大意思。
就界面Layout来说,这个程序其实和Snake没有什么不同,同样是采用了FrameLayout,而且游戏的主界面由一个自定义的View来实现,这里是LunarView。读过上一篇文章的朋友也许会发现,Snake的架构是“定时器+系统调用onDraw”来实现的,这里有一个最大的缺陷就是onDraw是由Android系统来调用的,我们只能依赖它,却无法自行控制。这就好比一个黑盒,当然,总是能把我们要的东西给做出来,可却无法控制其做事的细节,这对于游戏这样高效率的东西可是不利的,因此最好的解决之道当然是把绘制这部分工作自己”承包“过来,告别吃大锅饭的,进入”联产承包制”时代。
此外,由于游戏的本质就是连续两帧图片之间发生些许差异,那么要不断催生这种差异的发生,只要有某种连续不断发生的事件在进行就可以,例如Snake中使用的定时器,就是在不断地产生这种“差异源”,与此类似,一个线程也是不断在运行中,通过它也是可以不断产生这种“差异源”的。
SurfaceView初探
如果说Snake中使用的Layout加自定义View是一把小型武器的话,那在SurfaceView对于android中游戏的开发来说就算是重型武器了。我们使用前者时总是容易把游戏中某个对象(比如上文的每一个方格)当做一个小组件来处理,而后者则根本没有这种划分的概念,在它眼中,所有东西都是在Canvas(画布)中自行绘制出来的(背景,人物等)。
SurfaceView提供直接访问一个可画图的界面,可以控制在界面顶部的子视图层。SurfaceView是提供给需要直接画像素而不是使用窗体部件的应用使用的。Android图形系统中一个重要的概念和线索是surface。View及其子类(如TextView, Button)
要画在surface上。每个surface创建一个Canvas对象(但属性时常改变),用来管理view在surface上的绘图操作,如画点画线。还要注意的是,使用它的时候,一般都是出现在最顶层的:The view hierarchy will take care of correctly compositing
with the Surface any siblings of the SurfaceView that would normally appear on top of it.
使用的SurfaceView的时候,一般情况下还要对其进行创建,销毁,改变时的情况进行监视,这就要用到SurfaceHolder.Callback.
class LunarView extends SurfaceView implementspublic
void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
//在surface的大小发生改变时激发
public
void surfaceCreated(SurfaceHolder holder){}
//在创建时激发,一般在这里调用画图的线程。
public
void surfaceDestroyed(SurfaceHolder holder) {}
//销毁时激发,一般在这里将画图的线程停止、释放。
}
surfaceCreated会首先被调用,然后是surfaceChanged,当程序结束时会调用surfaceDestroyed。
下面来看看LunarView最重要的成员变量,也就是负责这个View所有处理的线程
private LunarThread thread; // 实际工作线程
thread =
new LunarThread(holder, context, new Handler() {
@Override
public
void handleMessage(Message m)
{
mStatusText.setVisibility(m.getData().getInt("viz"));
mStatusText.setText(m.getData().getString("text"
这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。
由于有了LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。
public
void surfaceChanged(SurfaceHolder holder, int format, int width,int height)
{
thread.setSurfaceSize(width, height);
}
public
void//启动工作线程结束
thread.setRunning(truepublic
void surfaceDestroyed(SurfaceHolder holder)
{
boolean retry =
truefalsewhiletry
{//等待工作线程结束,主线程才结束
thread.join();
retry =
falsecatch (InterruptedException e)
{
}
}
}
工作线程LunarThread
由于SurfaceHolder是一个共享资源,因此在对其操作时都应该实行“互斥操作“,即需要使用synchronized进行”封锁“机制。
再来讨论下为什么要使用消息机制来更新界面的文字信息呢?其实原因是这样的,渲染文字的工作实际上是主线程(也就是LunarView类)的父类View的工作,而并不属于工作线程LunarThread,因此在工作线程中式无法控制的。所以我们改为向主线程发送一个Message来代替,让主线程通过Handler对接收到的消息进行处理,从而更新界面文字信息。再回顾上一篇SnakeView里的文字信息更新,由于是SnakeView自己(就这一个线程)对其包含的TextView做控制,当然没有这样的问题了。
public
void setState(int mode, CharSequence message)
{
synchronized (mSurfaceHolder)
{
mMode =if (mMode ==//运行中,隐藏界面文字信息
Message msg = mHandler.obtainMessage();
Bundle b =
new"text", """viz", View.INVISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
else//根据当前状态设置文字信息
mRotating =
0=
false= mContext.getResources();
CharSequence str =
""if (mMode === res.getText(R.string.mode_ready);
else
if (mMode === res.getText(R.string.mode_pause);
else
if (mMode === res.getText(R.string.mode_lose);
else
if (mMode === res.getString(R.string.mode_win_prefix)
+ mWinsInARow +
"
"
+ res.getString(R.string.mode_win_suffix);
if (message !=
null= message +
"/n"
+ str;
}
if (mMode == STATE_LOSE)
mWinsInARow =
0= mHandler.obtainMessage();
Bundle b =
new"text", str.toString());
b.putInt("viz", View.VISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
}
}
下面就是LunaThread这个工作线程的执行函数了,它一直不断在重复做一件事情:锁定待绘制区域(这里是整个屏幕),若游戏还在进行状态,则更新底层的数据,然后直接强制界面重新绘制。
public
voidwhile (mRun)
{
Canvas c =
nulltry//锁定待绘制区域
c = mSurfaceHolder.lockCanvas(nullsynchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
updatePhysics();//更新底层数据,判断游戏状态
doDraw(c);//强制重绘制
}
}
finallyif (c !=
null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
强行自绘制
doDraw这段代码就是在自己的Canvas上进行绘制,具体的绘制就不解释了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代码是下面这个:
canvas.save();
canvas.rotate((float) mHeading, (float- (floatif (mMode == STATE_LOSE) {
mCrashedImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mCrashedImage.draw(canvas);
} else
if (mEngineFiring) {
mFiringImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mFiringImage.draw(canvas);
} else {
mLanderImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mLanderImage.draw(canvas);
}
canvas.restore();
在绘制火箭的前后,调用了save()和restore(),它是先保存当前矩阵,将其复制到一个私有堆栈上。然后接下来对rotate的调用还是在原有的矩阵上进行操作,但当restore调用后,以前保存的设置又重新恢复。不过,在这里还是看不出有什么用处。。。
LunarLancher的暂停其实并没有不再强制重绘制,而是没有对底层的数据做任何修改,依然绘制同一帧画面,而继续则是把mLastTime设置为当前时间+100毫秒的时间点,因为以前暂停时mLastTime就不再更新了,这样做事为了与当前时间同步起来。
public
void//暂停
synchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
setState(STATE_PAUSE);
}
}
public
void//// Move the real time clock up to now
synchronized (mSurfaceHolder)
{
mLastTime = System.currentTimeMillis() +
100;
}
setState(STATE_RUNNING);
}
这样做的目的是为了制造“延迟“的效果,都是因为updatePhysics函数里这两句
if (mLastTime > now) returndouble elapsed = (now - mLastTime) /
1000.0;
至于游戏的控制逻辑和判定部分就不介绍了,没有多大意思。
private LunarThread thread; // 实际工作线程
thread =
new LunarThread(holder, context, new Handler() {
@Override
public
void handleMessage(Message m)
{
mStatusText.setVisibility(m.getData().getInt("viz"));
mStatusText.setText(m.getData().getString("text"
这个线程由私有类LunarThread实现,它里面还有一个自己的消息队列处理器,用来接收游戏状态消息,并在屏幕上显示当前状态(而这个功能在Snake中是通过View自己控制其包含的TextView是否显示来实现的,相比之下,LunarThread的消息处理机制更为高效)。
由于有了LunarThread这个负责具体工作的对象,所以LunarView的大部分工作都委托给后者去执行。
public
void surfaceChanged(SurfaceHolder holder, int format, int width,int height)
{
thread.setSurfaceSize(width, height);
}
public
void//启动工作线程结束
thread.setRunning(truepublic
void surfaceDestroyed(SurfaceHolder holder)
{
boolean retry =
truefalsewhiletry
{//等待工作线程结束,主线程才结束
thread.join();
retry =
falsecatch (InterruptedException e)
{
}
}
}
工作线程LunarThread
由于SurfaceHolder是一个共享资源,因此在对其操作时都应该实行“互斥操作“,即需要使用synchronized进行”封锁“机制。
再来讨论下为什么要使用消息机制来更新界面的文字信息呢?其实原因是这样的,渲染文字的工作实际上是主线程(也就是LunarView类)的父类View的工作,而并不属于工作线程LunarThread,因此在工作线程中式无法控制的。所以我们改为向主线程发送一个Message来代替,让主线程通过Handler对接收到的消息进行处理,从而更新界面文字信息。再回顾上一篇SnakeView里的文字信息更新,由于是SnakeView自己(就这一个线程)对其包含的TextView做控制,当然没有这样的问题了。
public
void setState(int mode, CharSequence message)
{
synchronized (mSurfaceHolder)
{
mMode =if (mMode ==//运行中,隐藏界面文字信息
Message msg = mHandler.obtainMessage();
Bundle b =
new"text", """viz", View.INVISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
else//根据当前状态设置文字信息
mRotating =
0=
false= mContext.getResources();
CharSequence str =
""if (mMode === res.getText(R.string.mode_ready);
else
if (mMode === res.getText(R.string.mode_pause);
else
if (mMode === res.getText(R.string.mode_lose);
else
if (mMode === res.getString(R.string.mode_win_prefix)
+ mWinsInARow +
"
"
+ res.getString(R.string.mode_win_suffix);
if (message !=
null= message +
"/n"
+ str;
}
if (mMode == STATE_LOSE)
mWinsInARow =
0= mHandler.obtainMessage();
Bundle b =
new"text", str.toString());
b.putInt("viz", View.VISIBLE);
msg.setData(b);
mHandler.sendMessage(msg);
}
}
}
下面就是LunaThread这个工作线程的执行函数了,它一直不断在重复做一件事情:锁定待绘制区域(这里是整个屏幕),若游戏还在进行状态,则更新底层的数据,然后直接强制界面重新绘制。
public
voidwhile (mRun)
{
Canvas c =
nulltry//锁定待绘制区域
c = mSurfaceHolder.lockCanvas(nullsynchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
updatePhysics();//更新底层数据,判断游戏状态
doDraw(c);//强制重绘制
}
}
finallyif (c !=
null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
强行自绘制
doDraw这段代码就是在自己的Canvas上进行绘制,具体的绘制就不解释了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代码是下面这个:
canvas.save();
canvas.rotate((float) mHeading, (float- (floatif (mMode == STATE_LOSE) {
mCrashedImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mCrashedImage.draw(canvas);
} else
if (mEngineFiring) {
mFiringImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mFiringImage.draw(canvas);
} else {
mLanderImage.setBounds(xLeft, yTop, xLeft ++ mLanderHeight);
mLanderImage.draw(canvas);
}
canvas.restore();
在绘制火箭的前后,调用了save()和restore(),它是先保存当前矩阵,将其复制到一个私有堆栈上。然后接下来对rotate的调用还是在原有的矩阵上进行操作,但当restore调用后,以前保存的设置又重新恢复。不过,在这里还是看不出有什么用处。。。
LunarLancher的暂停其实并没有不再强制重绘制,而是没有对底层的数据做任何修改,依然绘制同一帧画面,而继续则是把mLastTime设置为当前时间+100毫秒的时间点,因为以前暂停时mLastTime就不再更新了,这样做事为了与当前时间同步起来。
public
void//暂停
synchronized (mSurfaceHolder)
{
if (mMode == STATE_RUNNING)
setState(STATE_PAUSE);
}
}
public
void//// Move the real time clock up to now
synchronized (mSurfaceHolder)
{
mLastTime = System.currentTimeMillis() +
100;
}
setState(STATE_RUNNING);
}
这样做的目的是为了制造“延迟“的效果,都是因为updatePhysics函数里这两句
if (mLastTime > now) returndouble elapsed = (now - mLastTime) /
1000.0;
至于游戏的控制逻辑和判定部分就不介绍了,没有多大意思。