Android 3D游戏开发技术详解与典型案例
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第3章 不积跬步,无以至千里—游戏开发基础知识

不积跬步,无以至千里。本章笔者将带大家进入Android游戏开发世界,结合有趣的小例子,为大家介绍游戏中的声音、音效、存储技术,以及2D场景绘制的利器—SurfaceView。

3.1 游戏中的声音

按照声音的功能不同,可以把它分做音乐和音效两部分,这两部分表面看似相同,其实现技术却迥然有异,这与其各自属性和用途有关,比如较长的音乐可以作为游戏的背景音乐,背景音乐启动通常较音效要慢,这是出于性能方面的考虑,因为较长的音乐如果时时加载在内存中,会造成内存资源紧张。

本节笔者将为读者分别介绍音乐和音效的加载技术,并结合例子进行说明。

3.1.1 迅雷不及掩耳的即时音效

如题目所示,迅雷不及掩耳的音效直接指出了音效的特点——快、短。所以这一类时间短但是要求反应迅速的音效,就不能用播放较长时间音乐的播放技术了,而应该采用android.media.SoundPool实现。

SoundPool类用于管理和播放应用程序的声音资源,该类将声音文件加载到内存中,出于性能考虑,一般只将时间小于7秒左右的声音文件用该技术播放。

在用SoundPool类播放文件时首先要得到一个SoundPool类的对象。可以通过其构造方法public SoundPool (int maxStreams, int streamType, int srcQuality)得到其对象,下面将介绍其构造方法中各参数的作用。

● maxStreams:该参数用于设置同时能够播放多少音效,如设置为4,则最多同时可以播放4首音效。

● streamType:该参数设置音频类型,在游戏中通常设置为:STREAM_MUSIC。

● srcQuality:该参数设置音频文件的质量,目前还没有效果,设置为0(默认值)。

有了SoundPool类对象后,就可以将音频文件加载到该对象中。

加载音频文件可用public int load(Context context, int resId, int priority),下面将介绍方法中各参数的作用。

● context:应用程序的上下文,即当前的Activity,可以理解为谁来调用这个方法。

● resId:该参数为给出资源的ID。

● priority:优先级,现在还没有作用,设置为1,以便和未来的方法兼容。

现在就可以播放音效了,可以通过public final int play (int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate)方法播放音效,下面将介绍方法中各参数的作用。

● soundID:播放的音乐ID。

● leftVolume:用来控制左声道音量。

● rightVolume:用来控制右声道音量。

● priority:优先级,0为最低。

● loop:循环次数,0为不循环,-1为永远循环。

● rate:回放速度,该值在0.5~2.0之间,1为正常速度。

当然在播放的同时也可以暂停播放,通过public final void pause (int streamID)方法可暂停播放音乐,其中的参数streamID是音效的ID。

纸上谈来终觉浅,下一小节我们将通过一个例子把以上知识点串联起来,使读者能够运用自如。

3.1.2 一个即时音效的例子

上一小节介绍了SoundPool类的常用方法,接下来将带领读者写一个即时音效的例子。具体步骤如下。

(1)打开Eclipse,导入本章源代码中名为Sample3_1的项目。

(2)在res目录下有一个名为raw的文件夹,其中包含本项目中所用到的声音文件,如图3-1所示。

图3-1 raw文件夹示意图

(3)下面将介绍layout目录下的main.xml文件的代码内容,在其中设置控件的布局。

代码位置:本书随书光盘中源代码\第3章\Sample3_1\res\layout\main.xml。

    1   <?xml version="1.0" encoding="utf-8"?>
    2   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3      android:orientation="vertical"
    4      android:layout_width="fill_parent"
    5      android:layout_height="fill_parent"
    6      >
    7        <Button
    8            android:text="播放音效1"
    9            android:id="@+id/Button01"
    10           android:layout_width="fill_parent"
    11           android:layout_height="wrap_content">
    12       </Button>                                    <!--添加一个Button控件-->
    13       <Button
    14           android:text="播放音效2"
    15           android:id="@+id/Button02"
    16           android:layout_width="fill_parent"
    17           android:layout_height="wrap_content">
    18       </Button>                                    <!--添加一个Button控件-->
    19       <Button
    20           android:text="暂停音效1"
    21           android:id="@+id/Button1Pause"
    22           android:layout_width="fill_parent"
    23           android:layout_height="wrap_content">
    24       </Button>                                    <!--添加一个Button控件-->
    25       <Button
    26           android:text="暂停音效2"
    27           android:id="@+id/Button2Pause"
    28           android:layout_width="fill_parent"
    29           android:layout_height="wrap_content">
    30       </Button>                                    <!--添加一个Button控件-->
    31   </LinearLayout>

● 第2~6行定义一个垂直方向上的线性布局。

● 第7~18行为一个播放音效1的按钮,以及一个播放音效2的按钮。

● 第19~30行为一个暂停播放音效1的按钮,以及一个暂停播放音效2的按钮。

(4)接着介绍在src目录下的wyf.zcl包中的MyActivity.java类。

代码位置:本书随书光盘中源代码\第3章\Sample3_1\src\wyf\zcl\MyActivity.java。

    1   package wyf.zcl;
    2   import java.util.HashMap;                       //引入相关包
    3   ……//此处省略部分不重要代码,读者可在光盘源代码中查看
    4   import android.widget.Button;
    5   public class MyActivity extends Activity {         //Activity创建时调用
    6        SoundPool sp;                            //得到一个声音池引用
    7        HashMap<Integer,Integer> spMap;              //得到一个map的引用
    8        Button b1;                               //声音播放控制按钮
    9        Button b1Pause;                           //声音暂停控制按钮
    10       Button b2;                               //声音播放控制按钮
    11       Button b2Pause;                           //声音暂停控制按钮
    12       @Override
    13       public void onCreate(Bundle savedInstanceState){
    14        super.onCreate(savedInstanceState);
    15        setContentView(R.layout.main);
    16        initSoundPool();                         //初始化声音池
    17        b1=(Button)findViewById(R.id.Button01);       //声音播放控制按钮实例化
    18        b2=(Button)findViewById(R.id.Button02);       //声音播放控制按钮实例化
    19        b1Pause=(Button)findViewById(R.id.Button1Pause);
    20        b2Pause=(Button)findViewById(R.id.Button2Pause);
    21        b1.setOnClickListener(new View.OnClickListener() {
    22                @Override
    23                public void onClick(View v) {
    24                    playSound(1,1);         //播放第一首音效,循环一遍
    25                Toast.makeText(MyActivity.this, "播放音效1", Toast.LENGTH_SHORT). show();
    26           }});
    27        b1Pause.setOnClickListener(new View.OnClickListener() {
    28                @Override
    29                public void onClick(View v) {
    30                    sp.pause(spMap.get(1));
    31                Toast.makeText(MyActivity.this, "暂停音效1", Toast.LENGTH_SHORT). show();
    32           }});
    33        b2.setOnClickListener(new View.OnClickListener() {
    34                @Override
    35                public void onClick(View v) {
    36                    playSound(2,1);         //播放第二首音效,循环一遍
    37                Toast.makeText(MyActivity.this, "播放音效2", Toast.LENGTH_SHORT). show();
    38           }});
    39        b2Pause.setOnClickListener(new View.OnClickListener() {
    40                @Override
    41                public void onClick(View v) {
    42                    sp.pause(spMap.get(2));
    43                Toast.makeText(MyActivity.this, "暂停音效2", Toast.LENGTH_SHORT). show();
    44           }});
    45     }
    46     public void initSoundPool(){              //初始化声音池
    47       sp=new SoundPool(
    48                5,                        //该参数为设置同时能够播放多少音效
    49                AudioManager.STREAM_MUSIC, //该参数设置音频类型
    50                0        //该参数设置音频文件的质量,目前还没有效果,设置为0(默认值)
    51       );
    52       spMap=new HashMap<Integer,Integer>();
    53       spMap.put(1, sp.load(this, R.raw.attack02, 1));
    54       spMap.put(2, sp.load(this, R.raw.attack14, 1));
    55     }
    56     public void playSound(int sound,int number){
    57       AudioManager am=                      //实例化AudioManager对象
    58   (AudioManager)this.getSystemService(this.AUDIO_SERVICE);
    59       float audioMaxVolumn=                  //返回当前AudioManager对象的最大音量值
    60   am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
    61       float audioCurrentVolumn=              //返回当前AudioManager对象的音量值
    62   am.getStreamVolume(AudioManager.STREAM_MUSIC);
    63       float volumnRatio=
    64   audioCurrentVolumn/audioMaxVolumn;
    65       sp.play(
    66                spMap.get(sound),            //播放的音乐ID
    67                volumnRatio,                //左声道音量
    68                volumnRatio,                //右声道音量
    69                1,                        //优先级,0为最低
    70                number,                    //循环次数,0无不循环,-1无永远循环
    71                1                         //回放速度,该值在0.5~2.0之间,1为正常速度
    72       );
    73   }}

● 第2~11行引入程序中所使用的相关包,以及声明相关引用。

● 第13行为Activity的onCreate()方法。当创建MyActivity时调用该方法。

● 第21~44行为播放音效1、播放音效2、暂停播放音效1、暂停播放音效2的初始化及监听方法。

● 第46~55行为初始化声音池,SoundPool调用load()方法可加载音频文件,然后将加载了音频文件的SoundPool添加到一个HashMap中,提供给以后的调用。

● 第56~73行为播放音效,调用SoundPool中的play方法,播放声音,参数sound是播放音效的ID,参数number是播放音效的次数。该方法的使用上一小节已经做了详细介绍,此处不再赘述了。

(5)最终效果如图3-2所示,单击播放音效1/播放音效2,将播放音效,单击暂停音效1/暂停音效2,将暂停正在播放的音效。

图3-2(a) 播放音效1效果图

图3-2(b) 暂停音效2效果图

3.1.3 背景音乐播放技术

背景音乐通常播放时间较长,且文件体积也相对较大。这类资源如果放在内存中,一方面给硬件资源本身就很紧缺的手机造成了负担,另一方面通常也没有这方面需求,放在内存中,在调用时播放速度较快,而长时音乐文件通常作为背景音乐,速度稍微慢一些并不会影响太大。

本小节将介绍Android中播放背景音乐的常用类MediaPlayer,及声音的控制需要用到的类AudioManager。

android.media.MediaPlayer类是Android中用来控制音频,以及视频的类。应用该类,可以轻松实现对音频的处理,接下来将为读者介绍MediaPlayer类的状态图及常用方法。

小提示: MediaPlayer类的状态图较为复杂,对于初学者来说可能会十分费力,在此笔者只讲解我们下一小节例子要用到的几个状态之间的关系,读者学习之后可以参照API来学习其他状态的关系,这样就能事半功倍。

MediaPlayer各状态及状态间关系如图3-3所示,Idle状态为空闲态,Initialized状态为初始化态,Idle状态加载了音频资源后就进入Initialized状态,之后调用prepare()方法就可以进入Prepared状态。在Prepared状态调用start()方法便可播放音乐,至于Paused和Stopped状态与其他几个状态之间的关系,在图3-3中已经详细给出,这里不再赘述了。

图3-3 MediaPlayer各状态及状态间关系示意图

对于MediaPlayer类中的各个方法,使用较为简单,聪明的读者在看到方法名后,就能猜出十之八九,下一小节我们将讲述一个简单的控制音乐播放的案例,读者可通过这个小例子,来学习以上方法。

音量的控制是播放音乐中常常用到的,Android中通过AudioManager类来控制音量,接下来就为读者介绍AudioManager中常用的方法。

● AudioManager通过Context.getSystemService(Context.AUDIO_SERVICE)方法得到调用其他类的实例。

● public void adjustVolume (int direction, int flags)方法可以调节音量,direction为调节的方向,ADJUST_LOWER减小音量、ADJUST_RAISE增大音量、ADJUST_SAME保持音量不变。flags为标记,通常设置为0。

● public int getStreamMaxVolume (int streamType)方法得到最大音量,该方法在上一小节即时音效的例子中用到过,读者可结合例子学习。

● public int getStreamVolume (int streamType) 方法得到当前音量,该方法在上一小节即时音效的例子中用到过,读者可结合例子学习。

提示:MediaPlayer与AudioManager还有很多实用的方法,由于篇幅有限,这里不再一一列出,读者可自行查阅API。

3.1.4 简单音乐播放器实现

上一小节介绍了MediaPlayer类与AudioManager类的常用方法,接下来将带领读者写一个简单音乐播放器的例子。具体步骤如下。

(1)打开Eclipse,导入名为Sample3_2的项目。

(2)打开DDMS,在File Explorer视图中找到sdcard文件夹,然后将我们要用到的音频文件dl.mid拖到该文件夹下,如图3-4所示。

图3-4 sdcard文件夹位置示意图

提示:本例中要用的音频文件位于本书随书光盘中源代码\第3章\Sample3_2中的音频资源\dl.mid中。

(3)下面介绍layout目录下的main.xml文件。

代码位置:本书随书光盘中源代码\第3章\Sample3_2\res\layout\main.xml。

    1   <?xml version="1.0" encoding="utf-8"?>
    2   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3      android:orientation="vertical"
    4      android:layout_width="fill_parent"
    5      android:layout_height="fill_parent"
    6      >
    7        <Button
    8            android:text="播放音乐"
    9            android:id="@+id/ButtonPlay"
    10           android:layout_width="fill_parent"
    11           android:layout_height="wrap_content">
    12       </Button>                               <!--播放音乐按钮-->
    13       <Button
    14           android:text="暂停音乐"
    15           android:id="@+id/ButtonPause"
    16           android:layout_width="fill_parent"
    17           android:layout_height="wrap_content">
    18       </Button>                               <!--暂停音乐按钮-->
    19       <Button
    20           android:text="停止音乐"
    21           android:id="@+id/ButtonStop"
    22           android:layout_width="fill_parent"
    23           android:layout_height="wrap_content">
    24       </Button>                               <!--停止音乐按钮-->
    25       <Button
    26           android:text="增大音乐"
    27           android:id="@+id/ButtonVAdd"
    28           android:layout_width="fill_parent"
    29           android:layout_height="wrap_content">
    30       </Button>                               <!--增大音乐按钮-->
    31       <Button
    32           android:text="降低音乐"
    33           android:id="@+id/ButtonVReduce"
    34           android:layout_width="fill_parent"
    35           android:layout_height="wrap_content">
    36       </Button>                               <!--降低音乐按钮-->
    37   </LinearLayout>

● 第2~6行定义一个垂直方向上的线性布局。

● 第7~12行为一个播放音乐的按钮,水平方向充满父窗口,竖直方向上与内容等高。

● 第13~18行为一个暂停播放音乐的按钮,水平方向充满父窗口,竖直方向上与内容等高。

● 第19~24行为一个停止播放音乐的按钮,水平方向充满父窗口,竖直方向上与内容等高。

● 第25~30行为一个增大音量的按钮,水平方向充满父窗口,竖直方向上与内容等高。

● 第31~36行为一个降低音量的按钮,水平方向充满父窗口,竖直方向上与内容等高。

(4)接着介绍src目录下的wyf.zcl包中的MyActivity.java类。

代码位置:本书随书光盘中源代码\第3章\Sample3_2\src\wyf\zcl\MyActivity.java。

    1   package wyf.zcl;
    2   import android.app.Activity;                         //引入相关包
    3   import android.widget.Button;
    4   public class MyActivity extends Activity {              //Activity创建时调用
    5        private Button bPlay;                          //播放按钮
    6        private Button bPause;                         //暂停按钮
    7        private Button bStop;                          //停止按钮
    8        private Button bAdd;                           //增大音量
    9        private Button bReduce;                         //降低音量
    10       private boolean pauseFlag=false;                 //暂停标记,true暂停态,false非暂停态
    11       MediaPlayer mp;                               //MediaPlayer引用
    12       AudioManager am;                              //AudioManager引用
    13     @Override
    14     public void onCreate(Bundle savedInstanceState) {      //Activity创建时调用
    15        super.onCreate(savedInstanceState);
    16        setContentView(R.layout.main);                  //设置Activity的显示内容
    17        bPlay=(Button)findViewById(R.id.ButtonPlay);       //播放按钮的实例化
    18        bPause=(Button)findViewById(R.id.ButtonPause);      //暂停按钮的实例化
    19        bStop=(Button)findViewById(R.id.ButtonStop);       //停止按钮的实例化
    20        bAdd=(Button)findViewById(R.id.ButtonVAdd);        //增大音量按钮的实例化
    21        bReduce  =(Button)findViewById(R.id.ButtonVReduce); //降低音量按钮的实例化
    22        mp=new MediaPlayer();
    23        am=(AudioManager) this.getSystemService(this.AUDIO_SERVICE);
    24        bPlay.setOnClickListener(new View.OnClickListener() {//播放按钮的监听器
    25                @Override
    26                public void onClick(View v) {
    27                    try{
    28                    mp.setDataSource("/sdcard/dl.mid"); //加载音频进入Initialized状态
    29                    }catch(Exception e){e.printStackTrace();}
    30                    try{
    31                        mp.prepare();               //进入Prepared状态
    32                    }catch(Exception e){e.printStackTrace();}
    33                    mp.start();                         //播放音乐
    34                Toast.makeText(MyActivity.this, "播放音乐", Toast.LENGTH_SHORT).show(); }});
    35        bPause.setOnClickListener(new View.OnClickListener() {    //暂停按钮添加监听器
    36                @Override
    37                public void onClick(View v) {
    38                    if(mp.isPlaying()){                   //如果是在播放状态
    39                        mp.pause();                     //调用暂停方法
    40                        pauseFlag=true;                  //设置暂停标记
    41                    }else if(pauseFlag){
    42                        mp.start();                     //播放音乐
    43                        pauseFlag=false;             //设置暂停标记
    44                Toast.makeText(MyActivity.this, "暂停播放", Toast.LENGTH_SHORT).show(); }}});
    45        bStop.setOnClickListener(new View.OnClickListener() {//停止按钮添加监听器
    46                @Override
    47                public void onClick(View v) {
    48                    mp.stop();                          //停止播放
    49                   mp.reset();                           //重置状态到uninitialized状态
    50                   try{
    51                        mp.setDataSource("/sdcard/dl.mid");  //加载音频进入Initialized状态
    52                   }catch(Exception e){e.printStackTrace();}
    53                    try{
    54                        mp.prepare();                    //进入Prepared状态
    55                    }catch(Exception e){e.printStackTrace();}
    56                Toast.makeText(MyActivity.this, "停止播放", Toast.LENGTH_SHORT).show(); }}});
    57        bAdd.setOnClickListener(new View.OnClickListener() {     //音量增大按钮添加监听器
    58                @Override
    59                public void onClick(View v) {
    60                am.adjustVolume(AudioManager.ADJUST_RAISE, 0);  //增大音量
    61                System.out.println("faaa");
    62                Toast.makeText(MyActivity.this, "增大音量", Toast.LENGTH_SHORT). show(); }}});
    63        bReduce.setOnClickListener(new View.OnClickListener() {   //音量降低按钮添加监听器
    64                @Override
    65                public void onClick(View v) {
    66                am.adjustVolume(AudioManager.ADJUST_LOWER, 0);  //减小音量
    67                Toast.makeText(MyActivity.this, "减小音量", Toast.LENGTH_SHORT).show(); }}});
    68   }}

● 第5~12行,定义了播放按钮、停止按钮、暂停按钮、增大音量按钮、减小音量按钮及MediaPlayer和AudioManager类的引用。

● 第16行,设置Activity的显示内容为main.xml文件。

● 第17~21行,实例播放按钮、停止按钮、暂停按钮、增大音量按钮及减小音量按钮。

● 第24~34行,为播放按钮的监听器,当单击播放按钮后,将调用该方法。其中的setDataSource()用于加载文件,文件加载后,MediaPlayer类实例进入Initialized状态,调用prepare()方法,进入Prepared状态,之后就可以调用start()方法来播放音乐了。

● 第35~44行,为暂停播放按钮的监听器,当单击暂停按钮后,首先判断是否在播放中,如果在播放中,则调用pause()方法,如果不是则调用start()方法。

提示:暂停播放按钮的设计目的是当用户在播放状态单击暂停按钮后,暂停播放,如果用户再次单击暂停按钮,则应用程序继续播放音乐。

● 第45~56行,为停止播放按钮的监听器,当单击停止播放按钮后,将调用该方法。使用stop()方法,是MediaPlayer类实例进入Stopped状态,之后再重置MediaPlayer类实例,然后重新加载音频文件并且调用prepare()方法,便可恢复到Prepared状态。

● 第57~68行,为增加音量和减小音量按钮的监听器,通过AudioManager类中的adjustVolume()方法实现。

(5)最终效果如图3-5所示。单击播放音乐按钮可播放设置好的音乐。在播放状态单击暂停音乐按钮,则暂停播放,再次单击,则继续播放。

图3-5(a) 播放状态效果图

图3-5(b) 停止状态效果图

图3-5(c) 增大音量效果图

单击停止音乐按钮,停止播放,并设置状态为Prepared状态。单击增大音乐和降低音乐按钮可以调节声音大小。

3.2 手机中的数据库——SQLite

本节将介绍Android中的数据库管理系统SQLite,它是一个微型的关系型数据库,支持事务等操作,在接下来的一节中将带领读者从创建数据库开始学习如何使用SQLite增删改查数据。

3.2.1 SQLite数据库简介

SQLite是一款轻型的数据库,其遵守ACID的关联式数据库管理系统,它最初就是为嵌入式设计的,其占用资源非常地低,在嵌入式设备中,可能只需要几百KB的内存就够了,同时SQLite还支持事务处理功能,根据相关资料可知SQLite的处理速度比Mysql、PostgreSQL等著名的开源数据库管理系统更快。如图3-6所示为SQLite的LOGO。

图3-6 SQLite的LOGO图

SQLite虽然体积小巧,但是其具有关系型数据库的很多特征,下面介绍较为重要的特征。

● ACID事务。

● 零配置——无须安装和管理配置。

● 储存在单一磁盘文件中的一个完整的数据库。

● 数据库文件可以在不同字节顺序的机器间自由地共享。

● 支持数据库大小至2TB。

● 足够小,大致3万行C代码,250KB。

● 比一些流行的数据库在大部分普通数据库操作上要快。

● 简单、轻松的API。

● 包含TCL绑定,同时通过Wrapper支持其他语言的绑定。

● 良好注释的源代码,并且有着90%以上的测试覆盖率。

● 独立:没有额外依赖。

● Source完全地Open,你可以用于任何用途,包括出售它。

● 支持多种开发语言,C、PHP、Perl、Java、ASP、.NET、Python。

提示:SQLite的官方网站是http://www.sqlite.org/,登录该网站可以了解更多关于SQLite的消息。

3.2.2 SQLite数据库的基本操作

本小节介绍SQLite的使用,笔者将介绍两个非常重要的操作SQLite的类android. database.sqlite.SQLiteDatabase及android.database.sqlite.SQLiteOpenHelper。

SQLiteDatabase中提供了大量方法供操作数据库使用,下面将介绍较为重要的方法。

● public long insert(String table, String nullColumnHack, ContentValues values)方法的作用是向数据库中插入数据。table为待插入的表名,nullColumnHack通常设置为null,values为待插入的数据,它是一个ContentValues类型的数据,关于它的使用,读者可以通过下一小节数据库操作的例子学习。

● public int update(String table, ContentValues values, String whereClause, String[] whereArgs)方法为向数据库中更新内容,table为待更新的表名,values为待更新内容,whereClause为选择通过那个字段来更新,whereArgs为whereClause字段要查询的值。

● query(…),该方法被重载,读者可以自行查阅API。

提示:SQLiteDatabase中还有很多很有用的方法,由于篇幅有限,读者可查阅API,在这里就不再一一赘述了。

SQLiteOpenHelper类是一个SQLiteDatabase的辅助类,通过它可以更加方便地创建和链接数据库,下面将介绍较为重要的方法。

● public abstract void onCreate(SQLiteDatabase db),当数据库创建时调用该方法。

● public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion),当数据库更新时调用该方法。

● public synchronized SQLiteDatabase getReadableDatabase() 得到一个可读SQLiteDatabase对象。

● public synchronized SQLiteDatabase getWritableDatabase() 得到一个可写SQLiteDatabase对象。

3.2.3 SQLite操作数据库的简单应用

上一小节介绍了SQLiteDatabase类与SQLiteOpenHelper类的常用方法,接下来将带领读者写一个简单数据库应用的例子。具体步骤如下。

(1)打开Eclipse,导入名为Sample3_3的项目。

(2)下面介绍layout目录下的main.xml文件。

代码位置:本书随书光盘中源代码\第3章\Sample3_3\res\layout\main.xml。

    1   <?xml version="1.0" encoding="utf-8"?>
    2   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3      android:orientation="vertical"
    4      android:layout_width="fill_parent"
    5      android:layout_height="fill_parent"
    6      >
    7        <Button
    8            android:text="创建数据库"
    9            android:id="@+id/ButtonCreate"
    10           android:layout_width="fill_parent"
    11           android:layout_height="wrap_content">
    12       </Button>                                    <!--添加一个Button控件-->
    13           <Button
    14           android:text="增加数据"
    15           android:id="@+id/ButtonInsert"
    16           android:layout_width="fill_parent"
    17           android:layout_height="wrap_content">
    18       </Button>                                    <!--添加一个Button控件-->
    19           <Button
    20           android:text="更新数据"
    21           android:id="@+id/ButtonUpdate"
    22           android:layout_width="fill_parent"
    23           android:layout_height="wrap_content">
    24       </Button>                                    <!--添加一个Button控件-->
    25           <Button
    26           android:text="查询数据"
    27           android:id="@+id/ButtonQuery"
    28           android:layout_width="fill_parent"
    29           android:layout_height="wrap_content">
    30       </Button>                                    <!--添加一个Button控件-->
    31   </LinearLayout>

● 第2~6行定义一个垂直方向上的线性布局。

● 第7~30行为各种操作的按钮,水平方向充满父窗口,竖直方向上与内容等高。

(3)接着介绍src目录下的wyf.zcl包中的MyActivity.java类。

代码位置:本书随书光盘中源代码\第3章\Sample3_3\src\wyf\zcl\MyActivity.java。

    1   package wyf.zcl;
    2   import wyf.zcl.sqlitedb.SqLiteDBHelper;
    3   ……//此处省略部分不重要代码,读者可在光盘源代码中查看
    4   public class MyActivity extends Activity {              //Activity创建时调用
    5        private Button createButton;                     //创建数据库按钮
    6        private Button insertBut;                       //增加数据库记录按钮
    7        private Button updateBut;                       //更新数据库记录按钮
    8        private Button queryBut;                        //查询数据库记录按钮
    9      @Override
    10     public void onCreate(Bundle savedInstanceState) {
    11        super.onCreate(savedInstanceState);
    12        setContentView(R.layout.main);
    13        createButton=(Button)findViewById(R.id.ButtonCreate);//实例化创建数据库按钮
    14        insertBut=(Button)findViewById(R.id.ButtonInsert);  //实例化插入数据库按钮
    15        updateBut=(Button)findViewById(R.id.ButtonUpdate);  //实例化更新数据库按钮
    16        queryBut=(Button)findViewById(R.id.ButtonQuery);    /实例化查询数据库按钮
    17        createButton.setOnClickListener(new View.OnClickListener() {   //创建数据库时调用
    18                @Override
    19                public void onClick(View v) {
    20                    SqLiteDBHelper dh=new SqLiteDBHelper(MyActivity.this,"testdb",null,1);
    21                    System.out.println("create or open database success!");
    22                    SQLiteDatabase sld=dh.getReadableDatabase();
                                                            //得到一个SQLiteDatabase对象
    23           Toast.makeText(MyActivity.this, "创建或打开数据库", Toast.LENGTH_SHORT). show();}});
    24        insertBut.setOnClickListener(new View.OnClickListener() { //增加数据库记录时调用
    25                @Override
    26                public void onClick(View v) {
    27                    ContentValues cv=new ContentValues();//得到ContentValues对象
    28                    cv.put("uid", 1);        //放入键值对,键要与列名一致,值要与列的数据类型一致
    29                    cv.put("uname", "zcl");
                                              //放入键值对,键要与列名一致,值要与列的数据类型一致
    30                    SqLiteDBHelper dh=new SqLiteDBHelper(MyActivity.this,"testdb",null,1); //创建
    31                    Toast.makeText(MyActivity.this, "插入记录", Toast.LENGTH_SHORT).show();
    32                    SQLiteDatabase sld=dh.getWritableDatabase();
                                                            //得到一个SQLiteDatabase对象
    33                    sld.insert("sqlitetest", null, cv);      //增加数据库记录
    34                    System.out.println("success insert a new content!");
    35           }});
    36        updateBut.setOnClickListener(new View.OnClickListener() { //更新数据库记录时调用
    37                @Override
    38                public void onClick(View v) {
    39                    SqLiteDBHelper dh=new SqLiteDBHelper(MyActivity.this,"testdb",null,1);
                                                            //创建数据库
    40                    SQLiteDatabase sld=dh.getWritableDatabase();
                                                            //得到一个SQLiteDatabase对象
    41                    ContentValues cv = new ContentValues();   //得到ContentValues对象
    42                    Toast.makeText(MyActivity.this, "更新记录", Toast.LENGTH_SHORT).show();
    43                    cv.put("uname", "zcl_update");
    44                    sld.update("sqlitetest", cv, "uid=?", new String[]{"1"});//更新数据库记录
    45                    System.out.println("success updata the content!");
    46           }});
    47        queryBut.setOnClickListener(new View.OnClickListener() {  //查询数据库记录时调用
    48                @Override
    49                public void onClick(View v) {
    50                    SqLiteDBHelper dh=new SqLiteDBHelper(MyActivity.this,"testdb",null,1);
    51                    Toast.makeText(MyActivity.this, "查询记录", Toast.LENGTH_SHORT). show();
    52                    SQLiteDatabase sld=dh.getReadableDatabase();
                                                            //得到一个SQLiteDatabase对象
    53                    Cursor cursor=sld.query("sqlitetest", new String[]{"uid","uname"},
    54                            "uid=?", new String[]{"1"}, null, null, null);
    55                    while(cursor.moveToNext()){             //打印输出
    56                        String name=cursor.getString(cursor.getColumnIndex("uname"));
    57                        System.out.println("query result:"+name);
    58           }}});
    59     }}

● 第2~8行,引入相关包,以及声明各个按键引用。

● 第17~23行,创建数据库按钮监听器,按下后会创建或者打开一个数据库,返回一个SQLiteDatabase对象。

● 第24~35行,插入数据库按钮监听器,按下后会创建或者打开一个数据库,返回一个SQLiteDatabase对象,并且插入一条数据。

● 第36~46行,更新数据库按钮监听器,按下后会创建或者打开一个数据库,返回一个SQLiteDatabase对象,并且更新一条数据。

● 第47~58行,查询数据库按钮监听器,按下后会创建或者打开一个数据库,返回一个SQLiteDatabase对象,并且查询一条数据。

(4)然后介绍src目录下的wyf.zcl. sqlitedb包中的SqLiteDBHelper.java类。

代码位置:本书随书光盘中源代码\第3章\Sample3_3\src\wyf\zcl\sqlitedb\SqLiteDBHelper. java。

    1   package wyf.zcl.sqlitedb;
    2   import android.content.Context;          //引入相关包
    3   ……//该处省略了部分类的导入,读者可自行查看随书光盘中的源代码
    4   public class SqLiteDBHelper extends SQLiteOpenHelper{
    5        public SqLiteDBHelper(Context context, String name, CursorFactory factory,
    6                int version) {          //继承SQLiteOpenHelper的类,必须有该构造函数
    7            super(context, name, factory, version);
    8        }
    9        @Override      //在调用getReadableDatabase()或getWritableDatabase()后调用
    10       public void onCreate(SQLiteDatabase db) {
    11           db.execSQL("create table sqlitetest(uid long,uname varchar(25))");
    12           System.out.println("already create a database:sqlitetest.");
    13       }
    14       @Override      //升级数据库时调用
    15       public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    16       }}

● 第5~8行,为SqLiteDBHelper类的构造函数,继承SQLiteOpenHelper的类,必须有该构造函数。

● 第9~13行,创建数据库时调用,此方法是在调用了getReadableDatabase()或getWritableDatabase()后才调用。

● 第14~16行,升级数据库时调用该方法,读者可根据需要,添加相关操作。

(5)最终效果如图3-7所示。

在上述程序中,单击创建数据库按钮则创建或打开数据库;单击增加数据按钮,则增加一条记录;单击更新数据按钮,则更新符合要求的记录;单击查询数据按钮,则在LogCat中打印数据库中的所有记录。

如图3-8所示,在LogCat中打印的相关记录,操作为单击了一次创建数据库按钮,然后先单击一次增加数据按钮,再单击一次更新数据按钮,之后再单击增加数据按钮,最后单击查询数据按钮。

图3-8 LogCat中打印的相关记录与操作示意图

图3-7(a) 创建或打开数据库效果图

图3-7(b) 插入记录效果图

图3-7(c) 更新记录效果图

3.3 文件I/O

开发中有时需要访问手机中的存储文件夹,这就需要用到文件I/O技术。本节介绍位于手机中不同位置的文件I/O。

3.3.1 轻松访问SD卡

Android平台可以在两个地方对文件进行读写操作,一个是SD卡中,另一个在手机的存储文件夹中。本小节将通过一个小例子来介绍如何访问SD卡中的数据。

访问SD卡中的数据与在Java SE中进行文件的读取操作十分类似,要注意正确地设置文件的位置及文件名。接下来将带领读者一起来写一个读取SD卡中文本文件的内容的例子。

(1)首先将待读取的文本文件进行正确编码,创建一个文本文件,然后输入内容,之后选择另存为命令,在编码中选择UTF-8,如图3-9所示。

图3-9 文件编码示意图

(2)创建新项目Sample3_4,将待显示的文本文件AndroidSummary.txt拖到DDMS的FileExploer中的sdcard中,如图3-10所示。

图3-10 将文件放到指定位置示意图

AndroidSummary.txt的位置:本书随书光盘中源代码\第3章\Sample3_4中的资源文件\AndroidSummary.txt。

(3)然后向读者介绍layout目录下的main.xml文件。

代码位置:本书随书光盘中源代码\第3章\Sample3_4\res\layout\main.xml。

    1   <?xml version="1.0" encoding="utf-8"?>
    2   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3      android:orientation="vertical"
    4      android:layout_width="fill_parent"
    5      android:layout_height="fill_parent"
    6      >
    7     <Button
    8       android:text="打开"
    9       android:id="@+id/Button01"
    10      android:layout_width="fill_parent"
    11      android:layout_height="wrap_content">
    12    </Button>                         <!--添加一个Button -->
    13    <ScrollView
    14      android:id="@+id/ScrollView01"
    15      android:layout_width="fill_parent"
    16      android:layout_height="wrap_content">
    17      <EditText
    18          android:editable="false"
    19          android:id="@+id/EditText01"
    20          android:layout_width="fill_parent"
    21          android:layout_height="wrap_content">
    22        </EditText>                     <!--添加一个EditText -->
    23    </ScrollView>                      <!--添加一个ScrollView -->
    24   </LinearLayout>

● 第2~6行,定义一个垂直方向上的线性布局。

● 第7~12行,为一个打开文件的按钮。

● 第13行,添加了一个ScrollView,用来包裹EditText,使EditText的显示可根据内容的多少动态变化。

● 第17~22行,添加了一个EditText,用来显示待显示的文本文件内容。

(4)接着介绍src目录下的wyf.zcl包中的MyActivity.java类。

代码位置:本书随书光盘中源代码\第3章\Sample3_4\src\wyf\zcl\MyActivity.java。

    1   package wyf.zcl;
    2   import java.io.File;                                   //引入相关包
    3   ……//此处省略部分不重要代码,读者可在光盘源代码中查看
    4   public class MyActivity extends Activity {                  //Activity创建时调用
    5        Button but;                                      //打开按钮引用
    6      @Override
    7      public void onCreate(Bundle savedInstanceState) {
    8        super.onCreate(savedInstanceState);
    9        setContentView(R.layout.main);
    10        but=(Button)findViewById(R.id.Button01);               //打开按钮初始化
    11        but.setOnClickListener(new View.OnClickListener() {      //为打开按钮添加监听器
    12                @Override
    13                public void onClick(View v) {
    14   String contentResult=loadContentFromSDCard("AndroidSummary.txt");
                                                            //调用读取文件方法获得文件内容
    15                    EditText etContent=(EditText)findViewById(R.id.EditText01); //实例化EditText
    16                    etContent.setText(contentResult);        //设置EditText的内容
    17                }});
    18     }
    19     public String loadContentFromSDCard(String fileName){       //从SD卡读取内容
    20       String content=null;                               //SD卡的内容字符串
    21       try{
    22           File f=new File("/sdcard/"+fileName);             //待读取的文件
    23           int length=(int)f.length();
    24           byte[] buff=new byte[length];
    25           FileInputStream fis=new FileInputStream(f);
    26           fis.read(buff); //从此输入流中将byte.length个字节的数据读入一个byte数组中
    27           fis.close();        //关闭此输入流并释放与此流关联的所有系统资源
    28           content=new String(buff,"UTF-8");
    29       }catch(Exception e){
    30           Toast.makeText(this, "对不起,没有找到文件",Toast.LENGTH_SHORT).show();
    31       }
    32           return content;
    33     }}

● 第2~3行,引入程序中所使用的相关包。

● 第10~17行,实例化了打开按钮对象,并且为其添加了监听器。

● 第19~33行,该方法是从SD卡中通过给出的文件名,返回该文件内容的字符串,该方法中全部用Java SE的相关方法实现。

(5)单击打开按钮,如果成功打开则如图3-11所示,在EditText中显示文本文件内容;如果失败,则显示如图3-12所示,弹出一个Toast。

图3-11 成功显示内容效果图

图3-12 没有找到文件提示图

3.3.2 访问手机中的存储文件夹

打开DDMS,找到FileExplorer视图,然后依次打开data文件夹\data文件夹,这里由很多两级以上包名组成的文件夹,如图3-13所示。

图3-13 FileExplorer视图

在Android中同样的包名的应用程序只允许存在一个,而每个应用程序的相关文件均可存放在data\data\包名文件夹中,这样就唯一保证了每个应用程序要用的资源、文件等都有唯一的一个地方存放,比如上一小节的例子中要用到的资源就可以存放在data\data\wyf.zcl文件夹下。

访问手机中存储的文件其实就是访问data\data\包名文件夹下的资源,对于上一小节中的例子,进行简单改造就可以访问手机中存储的文件,具体步骤如下。

首先将MyActivity.java类中第36行的内容替换为如下代码:

代码位置:本书随书光盘中源代码\第3章\Sample3_4\src\wyf\zcl\MyActivity.java。

1 File f=new File("/data/data/wyf.zcl/"+fileName);

然后将AndroidSummary.txt文件拖放到data\data\wyf.zcl文件夹下,单击运行按钮,效果与上例相同。

提示:不管是访问SD卡中的内容,还是访问手机中的内容,其操作都是Java SE中的文件IO,只是存放位置不同而已。

3.3.3 读取assets中的文件

在AndroidProject包中有一个文件夹assets一直没有用到,该文件夹中也可以存放文件,下面带领大家来实现一个访问assets文件夹内容的例子。

(1)创建新项目Sample3_5,然后将上一小节中用到的AndroidSummary.txt文件拖到assets文件夹中,如图3-14所示。

图3-14 assets文件夹

AndroidSummary.txt的位置:本书随书光盘中源代码\第3章\Sample3_5中的资源文件\AndroidSummary.txt。

(2)下面介绍layout目录下的main.xml文件。

代码位置:本书随书光盘中源代码\第3章\Sample3_5\res\layout\main.xml。

    1   <?xml version="1.0" encoding="utf-8"?>
    2   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3      android:orientation="vertical"
    4      android:layout_width="fill_parent"
    5      android:layout_height="fill_parent"
    6      >
    7        <Button
    8       android:text="打开"
    9       android:id="@+id/Button01"
    10      android:layout_width="fill_parent"
    11      android:layout_height="wrap_content">
    12    </Button>                                  <!--添加Button按钮-->
    13    <ScrollView
    14      android:id="@+id/ScrollView01"
    15      android:layout_width="fill_parent"
    16      android:layout_height="wrap_content">
    17      <EditText
    18          android:editable="false"
    19          android:id="@+id/EditText01"
    20          android:layout_width="fill_parent"
    21          android:layout_height="wrap_content">
    22        </EditText>                             <!--添加EditText -->
    23    </ScrollView>                               <!--添加ScrollView -->
    24   </LinearLayout>

● 第2~12行,定义一个垂直方向上的线性布局,以及添加一个打开文件的按钮。

● 第13行,添加了一个ScrollView,用来包裹EditText,使EditText的显示可根据内容的多少动态变化。

● 第17~22行,添加了一个EditText,用来显示待显示的文本文件内容。

(3)接着介绍src目录下的wyf.zcl包中的MyActivity.java类。

代码位置:本书随书光盘中源代码\第3章\Sample3_5\src\wyf\zcl\MyActivity.java。

    1   package wyf.zcl;
    2   import java.io.ByteArrayOutputStream;//引入相关包,有所省略,读者可自行查看源代码
    3   public class MyActivity extends Activity { //Activity创建时调用
    4        private Button but;               //打开按钮
    5      @Override
    6      public void onCreate(Bundle savedInstanceState) {
    7        super.onCreate(savedInstanceState);
    8        setContentView(R.layout.main);
    9        but=(Button)findViewById(R.id.Button01);                   //打开按钮实例化
    10        but.setOnClickListener(new View.OnClickListener() {          //打开按钮添加监听器
    11                @Override
    12                public void onClick(View v) {
    13                    String contentResult=loadFromAssert("AndroidSummary.txt");
    14                    EditText etContent=(EditText)findViewById(R.id.EditText01);
    15                    etContent.setText(contentResult);
    16                }});
    17     }
    18     public String loadFromAssert(String fileName){
    19       String content=null;                                   //结果字符串
    20       try{
    21           InputStream is=this.getResources().getAssets().open(fileName);  //打开文件
    22           int ch=0;
    23            ByteArrayOutputStream baos = new ByteArrayOutputStream(); //实现了一个输出流
    24            while((ch=is.read())!=-1){
    25                baos.write(ch);     //将指定的字节写入此byte数组输出流
    26            byte[] buff=baos.toByteArray();//以byte数组的形式返回此输出流的当前内容
    27            baos.close();               //关闭流
    28            is.close();                     //关闭流
    29            content=new String(buff,"UTF-8");                    //设置字符串编码
    30       }catch(Exception e){
    31           Toast.makeText(this, "对不起,没有找到指定文件!", Toast.LENGTH_SHORT).show();
    32       }
    33       return content;
    34     }}

● 第2行,引入程序中所使用的相关包。

● 第9~17行,实例化了打开按钮对象,并且为其添加了监听器。

● 第18~34行,该方法是从assets中通过给出的文件名返回该文件内容的字符串,该方法中全部用Java SE的相关方法实现。

提示:该例与本节中轻松访问SD卡小节的例子运行效果相同,这里不再占用篇幅给出效果图。

3.4 存储简单数据的利器——Preferences

前面几节中介绍了一个轻量级的数据库SQLite、访问手机SD卡的方法、访问手机中存储文件夹的方法及访问手机中assets文件夹的方法。这些方法对于处理数据量较多的数据和存储较大的文件来说是非常好的选择。但有时我们需要存储一些简单的数据,如字符串等,这就需要用到本小节为读者介绍的另一种存储机制——Preferences。

3.4.1 Preferences简介

Preferences是一种轻量级的数据存储机制,对于boolean、int、float、long及string等数据,以键值对的形式存储在应用程序的Preferences目录中,该目录位于(data\data\包名\shared_prefs\)中。

游戏中的得分、应用程序记录的上次登录时间等,这些数据量很小的操作使用Preferences可以带来很多好处,首先相比SQLite这种数据库,Preferences系统开销小很多,其次使用Preferences简单、快捷。

下一小节将带领读者使用Preferences来实现一个显示应用程序上次登录时间的例子。

3.4.2 Preferences实现显示上次登录时间

本小节将带领读者实现一个显示应用程序上次登录时间的例子,具体步骤如下。

(1)导入项目Sample3_6。

(2)然后介绍layout目录下的main.xml文件。

代码位置:本书随书光盘中源代码\第3章\Sample3_6\res\layout\main.xml。

    1   <?xml version="1.0" encoding="utf-8"?>
    2   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3      android:orientation="vertical"
    4      android:layout_width="fill_parent"
    5      android:layout_height="fill_parent"
    6      >
    7   <TextView
    8      android:layout_width="fill_parent"
    9      android:layout_height="wrap_content"
    10     android:textSize="25dip"
    11     android:id="@+id/TextView01"
    12     />                                       <!--添加TextView -->
    13   </LinearLayout>

● 第2~6行,定义一个垂直方向上的线性布局。

● 第7~12行,为一个TextView,其中的textSize用于设置字体大小。

(3)接着介绍src目录下的wyf.zcl包中的MyActivity.java类。

代码位置:本书随书光盘中源代码\第3章\Sample3_6\src\wyf\zcl\MyActivity.java。

    1   package wyf.zcl;
    2   import java.util.Date;                     //引入相关包,有所省略,读者可自行查看源代码
    3   public class MyActivity extends Activity { //Activity创建时调用
    4        private TextView tv;
    5      @Override
    6      public void onCreate(Bundle savedInstanceState) {
    7        super.onCreate(savedInstanceState);
    8        setContentView(R.layout.main);
    9        SharedPreferences sp=this.getSharedPreferences("sharePre", Context.MODE_PRIVATE);
    10        String lastLogin=sp.getString( //从SharedPreferences中读取上次访问的时间
    11                "ll",                 //键值
    12                null                  //默认值
    13        );
    14        if(lastLogin==null){
    15           lastLogin="欢迎您,您是第一次访问本Preferences";
    16        }else{
    17           lastLogin="欢迎回来,您上次于"+lastLogin+"登录";
    18        }
    19        SharedPreferences.Editor editor=sp.edit();    //向SharedPreferences中写本次访问时间
    20        editor.putString("ll", new Date().toLocaleString());     //向editor中放入现在的时间
    21        editor.commit();                                  //提交editor
    22        tv=(TextView)this.findViewById(R.id.TextView01);
    23        tv.setText(lastLogin);
    24     }}

● 第2行,引入程序中所使用的相关包。

● 第9行,返回一个SharedPreferences实例,第一个参数是Preferences名字,第二个参数是使用默认的操作。

● 第10~13行,从SharedPreferences中读取上次访问的时间。

● 第14~18行,判断用户是否是首次登录,并且设置相应情况显示的字符串。

● 第19~24行,向SharedPreferences中写本次访问时间。

(4)程序运行效果如图3-15所示,图3-15(a)为程序首次运行效果,图3-15(b)为程序非首次运行效果。

图3-15(a) 为程序首次运行效果

图3-15(b) 为程序非首次运行效果

3.5 SurfaceView在游戏中的使用

SurfaceView继承自View,但它与View不同,View是在UI的主线程中更新画面,而SurfaceView是在一个新线程中更新画面。View的特性决定了其不适合做动画,因为如果更新画面时间过长,那么主UI线程就会被正在画的函数阻塞。所以Android中通常用SurfaceView显示动画效果。

3.5.1 SurfaceView简单操作

新建一个类,然后继承SurfaceView,同时实现SurfaceHolder.Callback接口,就创建了带有生命周期回调函数的并实现了SurfaceView的类。本小节将介绍SurfaceView类的生命周期回调函数及其重要函数。

接下来介绍实现了SurfaceHolder.Callback接口后,SurfaceView类的3个生命周期回调函数。

● public abstract void surfaceCreated (SurfaceHolder holder)在SurfaceView创建时调用该函数。

● public abstract void surfaceChanged (SurfaceHolder holder, int format, int width, int height),在SurfaceView改变时调用该函数。

● public abstract void surfaceDestroyed (SurfaceHolder holder)在SurfaceView销毁前调用。

SurfaceView中最重要的方法应该是protected void onDraw(Canvas canvas),其是用来绘制SurfaceView画面的,其中的参数canvas是该SurfaceView的画笔。每一次SurfaceView中画面改变都是调用了该方法。

提示:通常实现动画效果的方法是,新建一个线程类,其每隔一个周期调用一次SurfaceView的OnDraw()方法,而OnDraw()方法中的画面也有一个线程类在时时改变,这样后一个线程类实现了OnDraw()画面中显示内容的变化,前一个线程类时时刷新画面,这就形成了动画。

下一小节我们将带领读者编写一个简单的动画场景。

3.5.2 简单动画场景的绘制

本小节我们将带领读者编写一个简单的动画场景,最终效果为:首先有一幅图片从屏幕的左下角开始向右上方运动,当图片上沿与手机屏幕上沿相撞时,图片的水平速度大小与方向均不变,竖直方向上速度大小不变,方向相反;当下沿相撞后,同样效果,直到图片飞出屏幕。之后屏幕渐渐地显示一幅图片。

该场景有5个Java类,其作用如下。

● Constant.java,常量类,该类把本场景中用到的常量全部写入其中。

● MyActivity.java,Activity类,该类是应用程序的Activity类。

● MySurfaceView.java,SurfaceView类,该类是应用程序的SurfaceView类。

● PicRunThread.java,OnDraw绘制线程类,用来时时改变OnDraw()方法中待显示的图像。

● OnDrawThread.java,OnDraw刷新线程类,用来时时调用OnDraw()方法,以刷新画面。

下面将带领读者一步步来编写这个动画场景。

(1)导入项目Sample3_7。

(2)接着介绍src目录下的wyf.zcl包中的MyActivity.java类。

代码位置:本书随书光盘中源代码\第3章\Sample3_7\src\wyf\zcl\MyActivity.java。

    1   package wyf.zcl;
    2   import android.app.Activity;
    3   ……//该处省略了部分包的导入,读者可自行查看随书光盘中的源代码
    4   public class MyActivity extends Activity {              //Activity创建时调用
    5        private MySurfaceView msv;                      //得到surfaceView的引用
    6        @Override
    7        public void onCreate(Bundle savedInstanceState) {    //Activity创建时调用
    8        super.onCreate(savedInstanceState);
    9        msv=new MySurfaceView(MyActivity.this);           //实例化MySurfaceView的对象
    10           requestWindowFeature(Window.FEATURE_NO_TITLE);  //设置屏幕显示没有title栏
    11           getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN ,
    12                    WindowManager.LayoutParams.FLAG_FULLSCREEN);        //设置全屏
    13        this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
                                                        //设置为横屏
    14        setContentView(msv);                          //设置Activity显示的内容为msv
    15     }}

● 第2~3行,引入程序中所使用的相关包。

● 第11~12行,设置屏幕为全屏显示,读者在创建自己的Activity类时,只要将该段

代码复制粘贴到自己的Activity中,即可实现全屏显示功能。

● 第13行,设置屏幕只允许横屏显示。读者在创建自己的Activity类时,只要将该段代码复制粘贴到自己的Activity中,即可实现横屏显示功能。

● 第14行,设置Activity显示的内容。

(3)接着介绍src目录下的wyf.zcl包中名为MySurfaceView.java的类。

代码位置:本书随书光盘中源代码\第3章\Sample3_7\src\wyf\zcl\MySurfaceView.java。

    1   package wyf.zcl;
    2   import android.content.Context;              //引入相关包,有所省略,读者可自行查看源代码
    3   public class MySurfaceView extends SurfaceView
    4   implements SurfaceHolder.Callback{
    5        int dy=Display.DEFAULT_DISPLAY;
    6        MyActivity ma;                       //得到MyActivity的引用
    7        Paint paint;                         //画笔的引用
    8        OnDrawThread odt;                     //OnDrawThread类引用
    9        PicRunThread prt;                     //图片运动的Thread类引用
    10       private float picX=0;                  //图片x坐标
    11       private float picY=0;                  //图片y坐标
    12       boolean picAlphaFlag=false;             //图片变暗效果的标记false为不显示,true为显示
    13       int picAlphaNum=0;                    //图片变暗效果中画笔的Alpha值
    14       public MySurfaceView(Context context) {
    15           super(context);
    16           this.ma=(MyActivity) context;
    17           this.getHolder().addCallback(this);  //注册回调接口
    18           paint=new Paint();                //实例化画笔
    19           odt=new OnDrawThread(this);         //实例化OnDrawThread类
    20           prt=new PicRunThread(this);         //实例化PicRunThread类
    21           prt.start();
    22       }
    23       public void setPicX(float picX) {        //图片x坐标的设置器
    24           this.picX = picX;
    25       }
    26       public void setPicY(float picY) {        //图片y坐标的设置器
    27           this.picY = picY;
    28       }
    29       public void setPicAlphaNum(int picAlphaNum) {   //图片变暗效果Alpha参数设置器
    30           this.picAlphaNum = picAlphaNum;
    31       }
    32       @Override
    33       protected void onDraw(Canvas canvas) {    //onDraw方法,此方法用于绘制图形图像等
    34           super.onDraw(canvas);
    35           paint.setColor(Color.WHITE);        //设置画笔为白色
    36           canvas.drawRect(0, 0, Constant.SCREENWIDTH, Constant.SCREENHEIGHT, paint);
    37           Bitmap bitmapDuke=BitmapFactory.decodeResource(ma.getResources(), R.drawable.duke);
    38           canvas.drawBitmap(bitmapDuke, picX, picY, paint);        //图片渐暗效果
    39           if(picAlphaFlag){
    40                Bitmap bitmapBG=BitmapFactory.decodeResource(ma.getResources(), R.drawable.jpg1);
    41                paint.setAlpha(picAlphaNum);
    42                canvas.drawBitmap(bitmapBG, 0,0, paint);
    43           }}
    44       @Override
    45       public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) {
    46       }
    47       @Override
    48       public void surfaceCreated(SurfaceHolder holder) {       //此方法在surfaceView创建时调用
    49           odt.start();                                 //启动onDraw的绘制线程
    50       }
    51       @Override
    52       public void surfaceDestroyed(SurfaceHolder holder) {      //此方法在surfaceView销毁前调用
    53       }}

● 第4行,此处实现SurfaceHolder.Callback接口,为surfaceView添加生命周期回调函数。

● 第5~13行,声明相关引用及标志位。

● 第16行,将ma的引用指向调用了该Surfaceview类构造器方法的对象,本例为MyActivity。

● 第23~31行,duke图片xy坐标及背景图片Alpha值的设置器。

● 第32~43行,onDraw方法,此方法用于绘制图形,图像。

● 第35行,此处画了一个白色的全屏幕的矩形,目的是设置背景为白色,同时每次重绘时清除背景

● 第36~37行,该处进行平面贴图,先将图片初始化,再用canvas将图片绘制到指定位置。

● 第38~43行,图片渐暗效果代码段,根据背景图片是否显示的标记位来决定是否显示背景图片。

● 第44~46行,此方法当surfaceView改变时调用,如屏幕大小改变。

● 第47~50行,此方法在surfaceView创建时调用,其中第64行代码的作用是启动onDraw的绘制线程。

● 第51~53行,此方法为在surfaceView销毁前调用。

(4)然后介绍src目录下的wyf.zcl包中名为OnDrawThread.java的类。

代码位置:本书随书光盘中源代码\第3章\Sample3_7\src\wyf\zcl\OnDrawThread.java。

    1   package wyf.zcl;
    2   import android.graphics.Canvas;              //引入相关包
    3   import android.view.SurfaceHolder;
    4   public class OnDrawThread extends Thread{      //该类的作用是时时刷新onDraw,进行画面的重绘
    5        MySurfaceView msv;                    //得到MySurfaceView的引用
    6        SurfaceHolder sh;                     //SurfaceHolder引用
    7        public OnDrawThread(MySurfaceView msv) {
    8            super();
    9            this.msv = msv;              //构造方法中,将msv引用指向调用了该类的MySurfaceView的对象
    10           sh=msv.getHolder();
    11       }
    12       @Override
    13       public void run() {
    14           super.run();
    15           Canvas canvas = null;           //Canvas的引用
    16           while(true){
    17                try{
    18                    canvas=sh.lockCanvas(null);
                                            //将canvas的引用指向surfaceView的canvas的对象
    19                    synchronized(this.sh){      //绘制过程,可能带来同步方面的问题,加锁
    20                        if(canvas!=null){
    21                        msv.onDraw(canvas);
    22                        }}}
    23                finally{
    24                    try{
    25                            if(sh!=null){
    26                                 sh.unlockCanvasAndPost(canvas); //绘制完后解锁
    27                            }}
    28                    catch(Exception e){e.printStackTrace();
    29                    }}
    30                try{
    31                    Thread.sleep(Constant.ONDRAWSPEED);           //休息ONDRAWSPEED秒钟
    32                    }
    33                catch(Exception e){
    34                    e.printStackTrace();
    35                    }}}}

● 第2~6行,引入相关包,以及创建MySurfaceView与SurfaceHolder的引用。

● 第9行,将该类的MySurfaceView类型的引用指向调用了该类的MySurfaceView的对象。

● 第18行,将canvas的引用指向surfaceView的canvas的对象。

● 第19行,绘制过程,可能带来同步方面的问题,加锁。

● 第21行,调用OnDraw()方法,绘制画面。

● 第26行,绘制完成后解锁,同时休息ONDRAWSPEED秒钟。

(5)接着介绍在src目录下的wyf.zcl包中名为PicRunThread.java的类。

代码位置:本书随书光盘中源代码\第3章\Sample3_7\src\wyf\zcl\PicRunThread.java。

    1   package wyf.zcl;
    2   public class PicRunThread extends Thread{          //该类是控制duke图片运动的类
    3        MySurfaceView msv;                        //MySurfaceView的引用
    4        private float picX=0;                      //图片x坐标
    5        private float picY=Constant.SCREENHEIGHT-Constant.PICHEIGHT;    //图片y坐标
    6        boolean yRunFlag=false;   //y方向上的运动标记,false时y=y+speed,true时y=y-speed
    7        int picAlphaNum=0;       //图片变暗效果中画笔的Alpha值
    8        public PicRunThread(MySurfaceView msv) {
    9            super();
    10           this.msv = msv;     //将该线程类的引用指向调用其的MySurfaceView的对象
    11       }
    12       @Override
    13       public void run() {
    14           super.run();
    15           while(true){                                 //控制duke图片的运动
    16                while(this.picX<Constant.SCREENWIDTH){        //当图片完全超过屏幕的右边时循环结束
    17                    msv.setPicX(picX);
    18                    msv.setPicY(picY);
    19                    picX=picX+Constant.PICXSPEED;
    20                    if(yRunFlag){                        //应该向上运动,自减
    21                        picY=picY-Constant.PICYSPEED;
    22                    }else{                             //应该向下运动,自加
    23                        picY=picY+Constant.PICYSPEED;
    24                    }
    25                    if(picY<=0){                         //到达屏幕上沿
    26                        yRunFlag=false;
    27                    }else if(picY>Constant.SCREENHEIGHT-Constant.PICHEIGHT){ //到达屏幕下沿
    28                        yRunFlag=true;
    29                    }
    30                    try{
    31                        Thread.sleep(Constant.PICRUNSPEED);
    32                    }catch(Exception e){e.printStackTrace();
    33                        }}                            //图片变暗效果演示
    34                msv.picAlphaFlag=true;                     //开启图片变暗效果
    35                for(picAlphaNum=0;picAlphaNum<=255;picAlphaNum++){
    36                    if(picAlphaNum==255){
    37                        msv.picAlphaFlag=false;            //当图片变暗效果结束,标记重置
    38                        picX=0;                        //图片x坐标
    39                        picY=Constant.SCREENHEIGHT-Constant.PICHEIGHT;  //图片y坐标
    40                        System.out.println(msv.picAlphaFlag+"picX:"+picX+"picY:"+picY);
    41                    }
    42                    msv.setPicAlphaNum(picAlphaNum);
    43                    try{
    44                        Thread.sleep(Constant.PICALPHASPEED);
    45                    }catch(Exception e){e.printStackTrace();}
    46                }}}}

● 第3~5行,声明MySurfaceView的引用,记录图片xy坐标。

● 第6行,图片y方向上的运动标记,为false时y=y+speed,为true时y=y-speed。

● 第7行,图片变暗效果中画笔的Alpha值。

● 第16行,当图片的左边完全超过屏幕的右边时,循环结束,

● 第17~29行,控制duke图片的运动及图片与屏幕的碰撞处理。

● 第34~46行,控制背景图片变暗。此处通过改变Alpha中的值来实现背景图片由暗变亮的效果。

(6)然后介绍src目录下的wyf.zcl包中名为Constant.java的类。

代码位置:本书随书光盘中源代码\第3章\Sample3_7\src\wyf\zcl\Constant.java。

    1   package wyf.zcl;
    2   import android.view.Display;                //引入相关包
    3   public class Constant {                    //Constant.java常量类,将常量全部写在该类中
    4        public static int SCREENWIDTH=480;       //屏幕宽(本程序为横屏)
    5        public static int SCREENHEIGHT=320;      //屏幕高
    6        public static int PICWIDTH=64;          //图片宽度
    7        public static int PICHEIGHT=64;          //图片高度
    8        public static int ONDRAWSPEED=30;        //onDraw线程类的绘制间隔时间
    9        public static float PICXSPEED=1.5f;      //图片水平移动速度
    10       public static float PICYSPEED=2;         //图片垂直移动速度
    11       public static int PICRUNSPEED=30;        //图片的运动线程的刷新速度
    12       public static int PICALPHASPEED=20;      //图片渐暗效果演示刷新速度
    13   }

提示:该类为常量类,将常量全部写在该类中,有利于程序的调试与后期维护。

最终效果是首先duke先在屏幕上运动,如图3-16所示,当duke运动出屏幕右边时,显示背景图片,如图3-17所示。

图3-16(a) duke的运动轨迹1

图3-16(b) duke的运动轨迹2

通过图片灰度的渐变,图3-17(a)明显比图3-17(b)显得模糊。渐变效果可以用于实现Logo的闪屏效果。

图3-17(a) 背景图片渐变显示1

图3-17(b) 背景图片渐变显示2

3.6 本章小结

本章介绍了游戏开发的基础知识,前4小节的例子均较为简单,且都只用了Button、TextView等简单控件,笔者这么做的目的是希望读者将全部精力投入本章介绍的知识点中。有兴趣的读者可以根据本章中的内容,将程序中的简单控件换成图片或动画等。