作者 吴亚峰 , 苏亚光 , 于复兴
2.4 壁纸的实现
上一节介绍了壁纸的框架,让读者对3D动态壁纸的整体框架有了初步认识,本节将要对动态壁纸的实现服务类GLWallpaperService和OpenGLES2WallpaperService以及自定义场景渲染器类MySurfaceView的开发进行详细介绍。
2.4.1 壁纸服务类——OpenGLES2WallpaperService
这两个类是本项目中最基础的类,没有这两个类就不可能使用壁纸。GLWallpaperService类为开发人员提供了壁纸服务,OpenGLES2WallpaperService通过继承GLWallpaperService类,重写此类中的方法来实现壁纸的后续开发。下面着重介绍一下OpenGLES2WallpaperService类中的onCreate方法和GLWallpaperService类中的触控响应事件onTouchEvent。
(1)首先是OpenGLES2WallpaperService类中的onCreate方法,onCreate方法是OpenGLES2 WallpaperService类的核心部分,其中包括获取当前手机的配置信息,并且判断其是否支持OPENGL ES2.0渲染技术等。具体代码如下所示。
1 public void onCreate(SurfaceHolder surfaceHolder) {
2 super.onCreate(surfaceHolder);
3 final ActivityManager activityManager = //创建Activity管理器
4 (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
5 final ConfigurationInfo configurationInfo = //获取当前机器配置信息
6 activityManager.getDeviceConfigurationInfo();
7 final boolean supportsEs2 = //获取判断结果
8 configurationInfo.reqGlEsVersion >= 0x20000;
9 if (supportsEs2) {
10 setEGLContextClientVersion(2); //设置使用OPENGL ES2.0
11 setPreserveEGLContextOnPause(true);//EGL跨越暂停/恢复界限来尝试和保存环境
12 setRenderer(getNewRenderer()); //设置渲染器
13 } else {return;}
14 }
第3~8行用于创建Activity管理器,获取配置信息,判断当前手机是否支持OPENGL ES2.0渲染技术,并将结果存储在supportsEs2中。
第9~13行用于判断supportsEs2中的值,如果当前手机支持OPENGL ES2.0渲染技术,则设置使用OPENGL ES2.0进行绘制,并且让EGL跨越暂停或恢复界限来尝试和保护环境,然后设置场景渲染器;如果不支持OPENGL ES2.0,则退出绘制。
(2)下面将对屏幕触控的响应事件进行介绍。屏幕的触控事件分为3部分:第一部分是滑动屏幕使背景图跟着屏幕左右移动,第二部分是点击屏幕下方修改标志位,最后一部分是手指抬起时判断是否进行喂食,具体代码如下所示。
1 private float mPreviousY; //上次的触控位置Y坐标
2 private float mPreviousX; //上次的触控位置X坐标
3 @Override
4 public void onTouchEvent(MotionEvent e) {
5 float y = e.getY(); //获取触控点Y坐标
6 float x = e.getX(); //获取触控点X坐标
7 switch (e.getAction()) {
8 case MotionEvent.ACTION_DOWN:
9 Constant.feeding=true;break; //喂食标志位设为true
10 case MotionEvent.ACTION_MOVE:
11 float dy = y - mPreviousY; //计算触控笔Y位移
12 float dx = x - mPreviousX; //计算触控笔X位移
13 if (dx < 0){ //摸左边x为正,摸右边x为负
14 if (Constant.CameraX <Constant.MaxCameraMove) { //判断是否超出移动范围
15 if(dx<-Constant.Thold){ Constant.feeding = false; }//喂食标志位置为false
16 Constant.CameraX = Constant.CameraX - dx / Constant.CameraMove_SCALE ;
17 Constant.TargetX=Constant.CameraX; //移动摄像机的坐标
18 }} else {
19 if (Constant.CameraX >-Constant.MaxCameraMove) { //判断是否超出移动范围
20 if(dx>Constant.Thold){ Constant.feeding =false;} //喂食标志位置为false
21 Constant.CameraX = Constant.CameraX - dx / Constant.CameraMove_SCALE ;
22 Constant.TargetX=Constant.CameraX; //移动摄像机的坐标
23 }}
24 MatrixState.setCamera( //将摄像机的位置信息存入到矩阵中
25 Constant.CameraX, Constant.CameraY, Constant.CameraZ,//摄像机的X、Y、Z位置
26 Constant.TargetX, Constant.TargetY, Constant.TargetZ,//观测点的X、Y、Z位置
27 Constant.UpX, Constant.UpY, Constant.UpZ); //up向量的X、Y、Z分量
28 break;
29 case MotionEvent.ACTION_UP:
30 if (Constant.feeding) { //标志位,开始喂食
31 if (Constant.isFeed) {
32 Constant.isFeed = false; //把标志位置为false
33 float[] AB = IntersectantUtil.calculateABPosition(
//通过矩阵变换获取世界坐标系中的点
34 x, y, //触控点X、Y坐标
35 Constant.SCREEN_WIDTH, Constant.SCREEN_HEGHT, //屏幕宽、长度
36 Constant.leftABS, Constant.topABS, //视角left、top值
37 Constant.nearABS, Constant.farABS); //视角near、far值
38 Vector Start = new Vector(AB[0], AB[1], AB[2]); //起点
39 Vector End = new Vector(AB[3], AB[4], AB[5]); //终点
40 if (MySurfaceView.feedfish != null) { //判断不为空启动
41 MySurfaceView.feedfish.startFeed(Start, End); //开始喂食
42 }}}
43 break;
44 }
45 mPreviousY = y; //记录触控笔Y位置
46 mPreviousX = x; //记录触控笔X位置
47 super.onTouchEvent(e);
48 }
第1~9行首先创建变量,用于记录触控笔上一次的触控X位置和Y位置,然后获取当前触控点的X坐标和Y坐标,并且响应屏幕的触控事件,对ACTION_DOWN事件进行监听,当触发时将喂食标志位设为true。
第10~28行对ACTION_MOVE事件进行监听,获取手指在屏幕上的移动距离,按照一定比例移动摄像机X坐标,同时,如果摄像机X坐标达到阈值,则摄像机不会向滑动方向移动;然后将摄像机的位置信息存入到矩阵中,设置摄像机的位置,观测点的坐标和up向量。
第30~37行是判断喂食的标志位,因为滑动屏幕不能喂食,当前喂的食物在没有消失之前也不能喂食,所以需要两个标志位对其进行控制。一个在点击喂食的时候为true,另一个在没有喂食之前为true,然后通过矩阵变换获取触控点在世界坐标系中的坐标。
第38~47行通过拾取计算获取触控点在世界坐标系中的起点(近平面点)坐标、终点(远平面点)坐标,判断feedfish不为空,则调用startFeed方法开始喂食;然后记录触控笔的X位置、Y位置,回调父类的方法。
2.4.2 自定义渲染器类——MySurfaceView
下面将详细介绍自定义的场景渲染器代码,在自定义的场景渲染器类里,可以进行鱼、鱼群、乌龟、珍珠贝、气泡、背景图、鱼食的初始化。初始化鱼类和乌龟的初始速度、初始位置以及加载纹理等,并且设置光源位置,初始化矩阵等。
(1)由于MySurfaceView类中绘制代码以及初始化代码比较多,在此首先介绍该类的绘制代码以及整体框架,使读者对此类有一个大致的了解。具体代码如下所示。
1 package wyf.lxg.mywallpaper;
2 .....//此处省略部分类和包的导入代码,请读者自行查阅随书附带光盘的源代码
3 public class MySurfaceView extends GLSurfaceView
4 implements GLSurfaceView.Renderer,OpenGLES2WallpaperService.Renderer {
5 public MySurfaceView(Context context) {
6 super(context); //获取上下文对象
7 this.setEGLContextClientVersion(2); //设置使用OPENGL ES2.0
8 }
9 .....//此处省略相关成员变量的声明代码,请读者自行查阅随书附带光盘的源代码
10 public void onDrawFrame(GL10 gl) {
11 GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT //清除深度缓冲与颜色缓冲
12 | GLES20.GL_COLOR_BUFFER_BIT);
13 MatrixState.pushMatrix(); //保护矩阵
14 if(bg!=null){ bg.drawSelf(back); } //绘制背景图
15 if(singlefood!=null) { singlefood.drawSelf();} //绘制鱼食
16 if (fishControl != null) { fishControl.drawSelf();} //绘制单条鱼和乌龟
17 if (fishSchool != null) { fishSchool.drawSelf();} //绘制鱼群
18 .....//此处绘制其他鱼群的代码与上述相似,故省略,请读者自行查阅随书附带光盘的源代码
19 MatrixState.pushMatrix(); //保护矩阵
20 MatrixState.translate(,-16,); //平移到指定位置
21 this.haibei.animate(time,dpm); //绘制珍珠贝
22 MatrixState.popMatrix(); //恢复矩阵
23 time += ;
24 if(time > this.ms3d.getTotalTime()) { //若当前播放时间大于总的动画时间
25 time = time - this.ms3d.getTotalTime();//则播放时间等于当前播放时间减去总的动画时间
26 }
27 MatrixState.popMatrix(); //恢复矩阵
28 GLES20.glEnable(GLES20.GL_BLEND); //开启混合
29 GLES20.glBlendFunc(GLES20.GL_SRC_COLOR, //设置混合因子c
30 GLES20.GL_ONE_MINUS_SRC_COLOR);
31 MatrixState.pushMatrix(); //保护矩阵
32 if(bubble!=null) { bubble.drawSelf();} //绘制气泡
33 MatrixState.popMatrix(); //恢复矩阵
34 GLES20.glDisable(GLES20.GL_BLEND); //关闭混合
35 }
36 public void onSurfaceChanged(GL10 gl, int width, int height) {
37 .....//此处省略设置摄像机的代码,将在后面详细介绍
38 }
39 public void onSurfaceCreated(GL10 gl, EGLConfig config) {
40 .....//此处省略初始化的代码,将在后面详细介绍
41 }
42 public int initTexture(Resources res,String pname)//textureId{
43 .....//此处省略加载纹理的代码,将在后面详细介绍
44 }}
第1~9行为声明包名,其中部分类和包的导入代码、相关成员变量的声明代码在此处省略,请读者自行查阅随书附带的光盘代码;然后创建构造方法,获取父类上下文对象,设置使用OPENGL ES2.0渲染技术进行绘制。
第11~18行首先清除深度缓冲与颜色缓冲,进行现场保护,判断背景、鱼食、单条鱼、乌龟以及鱼群的引用若不为空,依次绘制背景图、鱼食、单条鱼、乌龟以及鱼群。这里只给出了黄色小丑鱼群的绘制代码,其他鱼群绘制代码与之相似,请读者自行查阅随书附带光盘的源代码。
第19~27行为绘制珍珠贝的代码。首先保护现场,然后平移到指定位置,绘制珍珠贝,恢复现场。并且不断更新动画播放时间,若当前播放时间大于总的动画时间,则实际播放时间等于当前播放时间减去总的动画时间。
第28~34行为绘制气泡的代码。首先开启混合,设置混合因子,保护现场,判断气泡引用不为空,则进行气泡的绘制,然后恢复现场,关闭混合。
(2)下面介绍上面省略的onSurfaceChanged方法。重写该方法,主要作用是设置视口的大小及位置、计算GLSurfaceView的宽高比、通过计算产生投影矩阵以及摄像机9参数位置矩阵。该方法是场景渲染器类不可或缺的。具体代码如下所示。
1 public void onSurfaceChanged(GL10 gl, int width, int height) {
2 GLES20.glViewport(0, 0, width, height); //设置视窗大小及位置
3 float ratio = (float) width / height; //计算GLSurfaceView的宽高比
4 Constant.SCREEN_HEGHT=height; //获取屏幕高度
5 Constant.SCREEN_WIDTH=width; //获取屏幕宽度
6 Constant.leftABS=ratio*Constant.View_SCALE; //设置left值
7 Constant.topABS=1 * Constant.View_SCALE; //设置top值
8 Constant.SCREEN_SCALEX=Constant.View_SCALE*((ratio>1)?ratio:(1/ratio));
//设置缩放比
9 MatrixState.setProjectFrustum(-Constant.leftABS, Constant.leftABS,
//产生透视投影矩阵
10 -Constant.topABS, Constant.topABS, Constant.nearABS,Constant.farABS);
11 MatrixState.setCamera( //产生摄像机9参数位置矩阵
12 Constant.CameraX //摄像机的X位置
13 Constant.CameraY, //摄像机的Y位置
14 Constant.CameraZ, //摄像机的Z位置
15 Constant.TargetX, //观测点的X位置
16 Constant.TargetY, //观测点的Y位置
17 Constant.TargetZ, //观测点的Z位置
18 Constant.UpX, //up向量的X分量
19 Constant.UpY, //up向量的Y分量
20 Constant.UpZ); //up向量的Z分量
21 }
第1~10行用于设置视窗大小及位置,获取屏幕高度以及宽度,设置视角的left值以及top值,计算横屏竖屏缩放比,产生透视投影矩阵。这里使用透视投影矩阵是为了更真实地模拟现实世界,产生近大远小的效果。
第11~20行用于产生摄像机的9参数位置矩阵,分别设置摄像机的XYZ位置、观测点的XYZ位置以及up向量的XYZ分量,这里将摄像机位置矩阵的9参数都存放在Constant类中,是为了便于壁纸左右移动时修改摄像机的位置。
(3)下面介绍上面省略的onSurfaceCreated方法。重写该方法,主要作用是初始化光源位置,创建纹理管理器,加载纹理,获取ms3d文件的输入流,加载ms3d模型,创建鱼类、乌龟、珍珠贝等对象,开启深度检测等。具体代码如下所示。
1 public void onSurfaceCreated(GL10 gl, EGLConfig config){
2 GLES20.glClearColor(,,, ); //设置屏幕背景色RGBA
3 MatrixState.setInitStack(); //初始化矩阵
4 MatrixState.setLightLocation(0,9,13); //初始化光源位置
5 manager = new TextureManager(getResources()); //创建纹理管理器对象
6 dpm=initTexture(MySurfaceView.this.getResources(),"dpm.png");//加载明暗纹理
7 String name="ms3d/"; //获取ms3d文件的输入流
8 InputStream in=null;
9 ......//此处其他输入流的声明与上述相同,故省略,请读者自行查阅随书附带光盘的源代码
10 try{
in=getResources().getAssets().open(name+"fish0.ms3d");//鳐鱼
12 ......//此处其他获取输入流与上述相同,故省略,请读者自行查阅随书附带光盘的源代码
13 } catch(Exception e){
14 e.printStackTrace(); //打印异常栈信息
15 }
16 ms3d = MS3DModel.load(in,manager,MySurfaceView.this); //从输入流加载模型
17 ......//此处加载模型代码与上述相同,故省略,请读者自行查阅随书附带光盘的源代码
18 if(fishAl.size() ){
19 fishAl.add(new SingleFish(ms3d,dpm, //位置、速度、力、吸引力、重力
20 new Vector(-7, 5, -7), new Vector(, , ),
21 new Vector(0, 0, 0), new Vector(0, 0, 0), 150));
22 .....//此处添加鱼的代码与上述相同,故省略,请读者自行查阅随书附带光盘的源代码
23 }
24 back=initTexture(MySurfaceView.this.getResources(),"background.png");
//背景纹理
25 fishfood=initTexture(MySurfaceView.this.getResources(),"fishfood.png");
//鱼食纹理
26 bubbles=initTexture(MySurfaceView.this.getResources(),"bubble.png");//气泡纹理
27 GLES20.glEnable(GLES20.GL_DEPTH_TEST); //打开深度检测
28 bg=new Background(MySurfaceView.this); //创建背景对象
29 bubble = new BubbleControl(MySurfaceView.this,bubbles); //创建气泡对象
30 fishfoods=LoadUtil.loadFromFile("fishfood.obj", //鱼食对象
31 MySurfaceView.this.getResources(),MySurfaceView.this);
32 singlefood=new SingleFood(fishfood,fishfoods, MySurfaceView.this);//单个鱼食对象
33 feedfish=new FeedFish(MySurfaceView.this); //喂食
34 if (fishControl == null) { //创建对象鱼类的Control对象
35 fishControl = new FishControl(fishAl, MySurfaceView.this);
36 }
37 if (fishSchool == null) { //创建鱼群对象的Control
38 fishSchool = new FishSchoolControl(ms3d3,dpm,MySurfaceView.this,
39 new Vector(5, -2, 4),new Vector(, , ),50);//位置、速度、质量
40 }
41 ......//此处添加鱼群的代码与上述相同,故省略,请读者自行查阅随书附带光盘的源代码
42 GLES20.glDisable(GLES20.GL_CULL_FACE); //关闭背面剪裁
43 }
第1~6行为设置背景色的RGBA通道,初始化矩阵,只有初始化矩阵之后,保护矩阵、恢复矩阵等才能起作用。初始化光源位置,将光源置于场景的正上方。创建纹理管理器对象,用于加载纹理图,并且加载呈现明暗效果的纹理图。
第7~23行为获取ms3d文件的输入流,从输入流中加载ms3d模型,向鱼类列表中添加单条鱼、乌龟等。这里仅给出了fish0的加载代码,其他种类鱼、乌龟以及珍珠贝的加载代码与此相似,故省略,请读者自行查阅随书附带光盘的源代码。
第24~42行为加载背景、鱼食以及气泡的纹理,打开深度检测,创建背景、气泡、鱼食、喂食、鱼类以及鱼群的对象。其中在创建鱼群对象时,只给出了创建黄色小丑鱼鱼群的代码,其他鱼群的创建代码与此相似,故省略,请读者自行查阅随书附带光盘的源代码。
(4)下面详细介绍上面省略的initTexture方法。该方法的主要作用是通过输入流从assets中加载图片,生成纹理ID,设置纹理的拉伸方式,设置纹理采样方式等。具体代码如下所示。
1 public int initTexture(Resources res,String pname){ //初始化纹理
2 int[] textures = new int[1]; //生成纹理ID
3 GLES20.glGenTextures(
4 1, //产生的纹理id的数量
5 textures, //纹理id的数组
6 0 //偏移量
7 );
8 int textureId=textures[0];
9 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);//绑定纹理
10 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, //最近点采样
11 GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
12 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, //线性纹理过滤
13 GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
14 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, //纵向拉伸方式
15 GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_REPEAT);
16 GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, //横向拉伸方式
17 GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_REPEAT);
18 InputStream is = null; //创建输入流
19 String name="pic/"+pname;
20 try {
21 is = res.getAssets().open(name); //加载纹理图片
22 } catch (IOException e1) {
23 e1.printStackTrace(); //异常处理
24 }
25 Bitmap bitmapTmp; //创建Bitmap对象
26 try {
27 bitmapTmp = BitmapFactory.decodeStream(is); //对获取的图片解码
28 } finally {
29 try {
30 is.close(); //关闭输入流
31 }catch(IOException e) {
32 e.printStackTrace(); //异常处理
33 }}
34 GLUtils.texImage2D(
35 GLES20.GL_TEXTURE_2D, //纹理类型
36 0, //纹理的层次
37 bitmapTmp, //纹理图像
38 0 //纹理边框尺寸
39 );
40 bitmapTmp.recycle(); //释放Bitmap
41 return textureId;
42 }
第2~17行为定义纹理ID、生成纹理ID数组、以及绑定纹理。同时设置纹理的过滤方式分别为最近点采样过滤和线性纹理过滤,设置纹理的拉伸方式为纵向拉伸方式和横向拉伸方式并且都为重复拉伸方式。
第18~33行为创建输入流,从assets中加载纹理图片,创建Bitmap对象,对获取的图片进行解码,然后关闭输入流。其中关闭输入流是非常重要的,加载完图片,一定要记得关闭输入流,否则会造成资源浪费。
第34~41行为指定纹理。首先是纹理类型,在OpenGL ES中必须为GLES20.GL_ TEXTURE_2D;其次是纹理的层次,0表示基本图像层,可以理解为直接贴图;然后是纹理的图像以及边框尺寸;最后释放Bitmap,返回纹理ID。