前言
公认的,游戏开发比内容性APP开发要复杂和困难,除了应用状态的保存和还原、数据存储交互、游戏逻辑复杂性等等,更主要的就是视图界面的显示和处理的复杂性——在Android系统提供的控件中,几乎找不到合适的组件,都要自己动手去自定义。其实不管光是游戏开发,平时我们在开发中要实现一个比较美观或炫酷的效果,都是需要自己动手实现的。之前写过几篇关于自定控件的文章——Android自定义控件开发系列,虽然效果不错,但是从学习和提高的角度来看,有点舍本逐末了:在学习中我们的重点是掌握原理,了解机制,而不是看最终效果怎么样;至于学习是否有成果,就是看学完之后能不能根据需要实现心中的效果。所以今天,我就来返回头来从最初的起跑线开始补上之前基本功的缺失——借游戏开发来说说Android View和SurfaceView视图框架(下文“游戏开发”包含自定义控件的意义)。
*注* 本文和解下来的Android 视图框架系列2/3——SurfaceView视图框架、Android 视图框架系列3/3——View和SurfaceView之间的抉择是一个模块,有对比才有特点和区别。
Android游戏开发中常用的3种视图是 View、SurfaceView、GLSurfaceView,它们的继承关系如下:
不过还是建议看Android自定义控件开发系列(零)——基础原理篇中那个唯一的大图!
- View:Android中一个超类,是所有视图类组件的父类,用于显示视图,内置画布,提供图形绘制、触屏回调、按键回调等事件;
- SurfaceView:基于View进行拓展,擅长于不断自主变换外观的控件,适合2D游戏开发;
- GLSurfaceView:基于SurfaceView再次拓展,支持硬件加速,专用于3D游戏开发(在此不予讨论,主要是我也不会)。
View
本篇主要说说View视图框架:
View 是 Android 开发中最基础也是最本质的视图基类,在开发中要想实现自定义的控件,大多都是直接继承自View,因为那些可以继承View的子类(甚至是子类的子类)的控件其实都是修修改改,只是接触到自定义控件的皮毛而已。具体还是看Android自定义控件开发系列(零)——基础原理篇吧,不必再重复了。
不管是游戏开发还是内容性 APP 开发,继承 View 主要还是重写 onDraw(Canvas canvas)、onTouchEvent(MotionEvent event)、onKeyDown(int keyCode, KeyEvent event) 方法。看不少游戏开发入门的书,在讲 View 这块时总是在 onKeyDown(int keyCode, KeyEvent event) 上做文章,其实现在这个方法现在也就只用来重写 Home、BACK 键的响应了,什么方向键字母键,手机上根本没有(除非你在电脑上用模拟器),重点还是在绘图和触屏事件的响应。
基础的
onDraw(Canvas canvas) 方法入参canvas由 Android 系统框架提供,绘图时直接拿来用就好(这就是所谓的内置画布),有人说有画布为什么不提供 Paint 呢?在 Android 中,绘图 API 都置于 Canvas 类的方法中,系统在提供这个画布时其实做了很多我们看不到的复杂工作(具体我也不了解),并创建一个 Bitmap 内存区域用来保存你画出的内容,而 Paint 只是 drawXxx()各自图形时的一个参数而已,其所能做的也只是设置Paint自身的一些属性,比如 Color、抗锯齿抖动、字体大小……等,可以随时用随时 new 。举个不慎恰当的例子,就像我们这些猿,让我们去哪工作都可以,首先你得给我一个工作平台——相当于画布,我们工作用什么呢?我的能力和心胸中的知识就是我们的画笔,什么时候用、怎么用,到我用的时候再拿出来整理思路去实现——就是设置我们的画笔(神笔马良啊)
onTouchEvent(MotionEvent event)、onKeyDown(int keyCode, KeyEvent event) 、onKeyUp(int keyCode, KeyEvent event) ……就是上边所说的提供图形绘制、触屏回调、按键回调等事件。
从图像到动画的升级
画布就像一张纸,在你各种画的时候系统只给了你一张纸,Oh ,Shit ! 我做上边这个动画需要那么厚一摞纸,怎么办,难道不能用 View ?Too Young Too Simple,谷哥哥的大神们怎么会想不到这个问题!Android 系统给我们提供了神器—— invalidate() 方法。
invalidate() 是 Android 系统提供给我们的“一键重绘”的方法。通过不断调用这个方法,就可以不断把纸上的内容擦干净重画,擦干净重画。想象一下:还是一张纸,但是你画和擦的速度是以毫秒ms为单位的,当你还在回味上一张图的时候下一张图已经画好在你眼前了,你接下来的反应是继续品味这一张图,然而下一张图又画好在你眼前……就像上图中下边一张纸消失的时候你还没有看到,它又画好了跑到上边翻下来了,是不是就实现了这样类似的动画了呢?我们只要开一个子线程不断计算要画的内容和画的位置,然后把计算结果让 invalidate() 告诉 onDraw(Canvas cnavas)即可,是吗?
然而,并不是这样!invalidate() 这个方法是不能在子线程(非 UI 线程)中使用的,否则会报错。要怎么做呢?给个例子代码吧
public class MyView extends View implements Runnable{
/**
* 两个构造函数,先不用看
*/
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); //根据需要改变的绘制内容
paint = new Paint();
new Thread(this).start(); //运行子线程
}
/**
* 从这里开始看
*/
private int startX = 0, startY = 0; //绘制起点的X、Y坐标
private Bitmap bitmap; //要绘制的内容
private Paint paint;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.BLACK);
canvas.drawBitmap(bitmap, startY, startY, paint);
}
@Override
public void run() {
int i = 0;
while (i < 100) {
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); //根据需要改变的绘制内容
startX += 10; //根据需要变
startY += 10;
//invalidate(); //报错,错误看截图
handler.sendEmptyMessage(0x001); //给主线程发消息,让主线程来完成invalidate()方法的调用
i++;
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Handler handler = new Handler(){
public void handleMessage(Message msg) {
if(msg.what == 0x001){
invalidate(); //在主线程的Handler调用,通知onDraw()重绘
}
};
};
}
简单解释一下:开子线程要完成绘制内容和位置的计算,计算完后就要通知 onDraw() 方法进行重绘了,但是由于 invalidate() 方法不能在子线程中调用(否则报下图错误),所以我们通过给 UI 线程的 Handler 发消息,让 UI 线程调用 invalidate() 方法。
Oh ! Shit ! 这么麻烦?费劲死了!有没有简单方法呢?恭喜你,谷哥哥也有同样的想法,所以Android还提供了一个在子线程中可以直接调用的通知重绘的方法 postInvalidate(),有次方法,不必再通过 UI 线程了,子线程中计算完结果后直接调用 postInvalidate() 即可,是不是简单多了?
其实 invalidate()、 postInvalidate() 这两种方法并没有什么本质上的区别,区别只在于应用环境不一样罢了,功能上和效率上是没有什么区别的,因地制宜合理利用就好。
加上前边提及的一些回调和按钮、触屏的事件响应,View 视图框架模型基本上就完成了。接下来看下一篇Android 视图框架系列2/3——SurfaceView视图框架,看看他们有哪些不一样。