SurfaceView有个很大的好处,就是可以在子线程中绘制UI,其他的View只能在主线程中更新UI,这或多或少给编程增加了些不便。而SurfaceVIew在子线程中可以绘制UI的特性,再加上其可以直接从内存或者DMA等硬件接口取得图像数据,这使得它适合2d游戏的开发。
SurfaceView使用步骤
SurfaceView的使用比较简单,可以总结为如下几个步骤:
1.继承SurfaceView并实现 SurfaceHolder.Callback方法
譬如:
public class StartAniSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
2.给SurfaceHolder对象注册回调方法
getHolder().addCallback(this);
SurfaceView中有个SurfaceHolder 的实例,这个实例是整个SurfaceView的核心,我们这个给这个SurfaceHolder实力添加回掉方法以后,就会导致surfaceCreated方法被回掉,用来通知你Surface已经准备好了,你可以绘图了。当你修改了SurfaceView的大小以后,surfaceChanged方法就会被会调,用来通知你Surface发生了改变,当你按下退出键,导致SurfaceView销毁的之前,surfaceDestroyed方法就会被调用,通知你Surface要销毁了,你赶紧手动释放需要释放的资源吧等等。
总之,addCallback这份方法一定要调用,不然回掉方法没有添加进去,就不可能有有毁掉方法调用的行为了。
3.在surfaceCreated方法中开始绘画
一般我们会在surfaceCreated方法中开启一个线程,让它来执行绘画的工作,当然不开启也没关系,直接绘画也可以。
准备好SurfaceView以后,SurfaceView的使用和其他的View就没有社么不同了,同门可以静态的在布局文件中使用,然后使用findViewById来获取到它的实例,也可以动态使用,直接new就可以了,相比也没什么好说的呢。
绘图
关于绘图,需要使用至少两个类:Canvas,和Paint,Paint可以理解成一个画笔,你在绘画前需要设置它的颜色,粗细等信息,Canvas可以理解成一个人,他手里拿着Paint,而且他有很多的技能,比如绘制矩形,椭圆等。我们说SurfaceView的核心是一个SurfaceHolder,所以,这个我们要绘图的话得有专业的画家,这个画家得向SurfaceHolder来要,它要是不给那就没办法了。因此,绘图的代码可以是这样:
Canvas canvas = holder.lockCanvas();
canvas.drawColor(Color.WHITE);
paint.setColor(bgColor);
canvas.drawRoundRect(0,0,getWidth(),getHeight(),cornerRadius,cornerRadius,paint);
。。。
holder.unlockCanvasAndPost(canvas);
可以看到我们首先向holder请求一个画家canvas,canvas会使用paint来绘画,画好以后化一定要交给画家的boss,也就是holder,使用holder.unlockCanvasAndPost(canvas);来实现,这样这幅画才能显示出来。
动画就是不断的这样话一幅又一幅的画,只不过这些画前后有联系,这样就能形成动画。
游戏的启动动画
下面以2048游戏的启动动画为例,具体介绍SurfaceView的使用。
在2048游戏的启动动画中,就是使用了SurfaceView来绘制动画的,这里再把效果图拿出来:
这个动画模仿了焰火的效果,实现过程如下:
FlyNumber
一个飞出去的数字用FlyNumber类来表示,她的定义如下:
public class FlyNumber {
public Point start;
public Point control;
public Point end;
public Point cur;
public float t;
public int number;
public int clolor;
public int textSize;
public FlyNumber(){
start = new Point();
control = new Point();
end = new Point();
cur = new Point();
t=0f;
}
public void setStart(int x,int y){
start.x=x;
start.y=y;
}
public void setControl(int x,int y){
control.x=x;
control.y=y;
}
public void setEnd(int x,int y){
end.x=x;
end.y=y;
}
}
类中定义了一个要飞出来的数字的值,颜色,大小,起始点,结束点,控制点以及时间等信息,说到起始点,结束点,控制点以及时间,大家应该已经想到了贝塞尔曲线了吧,是的每一个飞出去的数字使用贝塞尔曲线生成路线。
贝塞尔曲线
二次方公式
二次方贝兹曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
这里只用到二次贝塞尔曲线,所以只给出二次的公式,其他的就不深究了。根据这个公式,我们可以把它转换为java代码:
public void getBesselFlayNumber(FlyNumber number,float gatT){
number.t += gatT;
double x = (1-number.t)*(1-number.t)*number.start.x +2*(1-number.t)*number.t*number.control.x + number.t*number.t*number.end.x;
double y = (1-number.t)*(1-number.t)*number.start.y +2*(1-number.t)*number.t*number.control.y + number.t*number.t*number.end.y;
number.cur.x = (int)x;
number.cur.y = (int)y;
}
我们会通过时间t,以及起始点,控制点,和结束点,生成当前时刻FlyNumber的值,也就是FlyNumber的cur的值。然后再cur.x,cur.y的地方绘制一个数字,绘制方法为:
public void drawOneFlyNumber(Canvas canvas, Paint paint, FlyNumber number){
paint.setTextSize(number.textSize);
paint.setColor(number.clolor);
canvas.drawText(String.valueOf(number.number),number.cur.x,number.cur.y,paint);
}
调用可以想象我们需要不断的生成数字,然后绘制出所有生成的数字,当这个数字到达终点后,就把它销毁掉,怎么实现呢?用一个链表就可以实现,思路如下:
不断的构造FlyNumber对象,构造好它的起点,终点,控制点等信息以后,把它放到一个链表中,这样生成端什么都不考虑,只管生成。绘制端不断的从遍历链表,把通过贝塞尔曲线函数生成坐标,然后绘制它,当一个FlyNumber到达终点后,就把它从链表中移除。
根据这个思路,代码如下:
public void run() {
ArrayList<FlyNumber> arrayList=new ArrayList<>();
drawFlayNumbersBessel(holder,paint,arrayList,aniCount);
...
在绘制线程的run方法中构建一个链表:arrayList,然后把它传给drawFlayNumbersBessel方法继续处理:
final int MAX = 100;
public void drawFlayNumbersBessel(SurfaceHolder holder,Paint paint,ArrayList<FlyNumber> arrayList,final int count){
int countl = 0;
final float gapT = 1.0f/MAX;
for(int i=0;i<100;i++){
arrayList.add(generateRandomNumber());
}
Canvas canvas;
FlyNumber number;
while (countl++<(count)){
canvas = holder.lockCanvas();
if(countl<count-MAX){
arrayList.add(generateRandomNumber());
arrayList.add(generateRandomNumber());
}
if(canvas != null){
canvas.drawColor(Color.WHITE);
paint.setColor(bgColor);
canvas.drawRoundRect(0,0,getWidth(),getHeight(),cornerRadius,cornerRadius,paint);
if(arrayList.size()>0){
Iterator<FlyNumber> iterator = arrayList.iterator();
while (iterator.hasNext()){
number = iterator.next();
if(number.t>=1.0f){
iterator.remove();
}
getBesselFlayNumber(number,gapT);
drawOneFlyNumber(canvas,paint,number);
int dif = count-countl;
if(dif<50 && dif>0){
drawWelComeState(canvas,paint,255-(count-countl)*3,300-(count-countl)*6);
}
}
}
holder.unlockCanvasAndPost(canvas);
}
}
}
这份方法如下事情:
1.初始化100个数字
for(int i=0;i<100;i++){
arrayList.add(generateRandomNumber());
}
2.每次循环,如果循环次数countl
if(countl<count-MAX){
arrayList.add(generateRandomNumber());
arrayList.add(generateRandomNumber());
}
为什么是countl
getBesselFlayNumber(number,gapT);
4.绘制FlyNumber
drawOneFlyNumber(canvas,paint,number);
5.最后50次绘制不断放大的2048字样
drawWelComeState(canvas,paint,255-(count-countl)*3,300-(count-countl)*6);
drawWelComeState函数如下:
private void drawWelComeState(Canvas canvas,Paint paint,int alpha,int textSieze){
paint.setAlpha(alpha);
paint.setTextSize(textSieze);
paint.setColor(Color.WHITE);
String string = "2048";
float width = paint.measureText(string);
canvas.drawText(string,getWidth()/2-width/2,getHeight()/2+textSieze/3,paint);
}
大家在计算数字位置的时候,使用paint.measureText方法能准确的计算出字符串的宽度,这在绘制字符串的过程中十分常用。
这样2048游戏的开机动画就结束了。最后,把整个StartAniSurfaceView类贴出来,方便对比:
package com.jinwei.tvgame2048.ui;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import com.jinwei.tvgame2048.R;
import com.jinwei.tvgame2048.model.FlyNumber;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;
/**
* Created by Jinwei on 2016/10/27.
*/
public class StartAniSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private final int textSize = 60;
private final int bgColor = Color.BLACK;
private final int aniCount = 300;
private final int forbidArea = 200;
private final int gameNameSize = 300;
private final int cornerRadius = 20;
private Handler mHandler;
public interface AniOverListener{
public void onAniOver();
}
public void setHandler(Handler handler){
mHandler = handler;
}
AniOverListener mListener;
public StartAniSurfaceView(Context context) {
super(context);
}
public StartAniSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void init(){
getHolder().addCallback(this);
}
public void setAniOverListener(AniOverListener listener){
mListener = listener;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Paint paint = new Paint();
paint.setAntiAlias(true);
doDraw(holder,paint);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
public void getBesselFlayNumber(FlyNumber number,float gatT){
number.t += gatT;
double x = (1-number.t)*(1-number.t)*number.start.x +2*(1-number.t)*number.t*number.control.x + number.t*number.t*number.end.x;
double y = (1-number.t)*(1-number.t)*number.start.y +2*(1-number.t)*number.t*number.control.y + number.t*number.t*number.end.y;
number.cur.x = (int)x;
number.cur.y = (int)y;
}
public void drawOneFlyNumber(Canvas canvas, Paint paint, FlyNumber number){
paint.setTextSize(number.textSize);
paint.setColor(number.clolor);
canvas.drawText(String.valueOf(number.number),number.cur.x,number.cur.y,paint);
}
public void doDraw(final SurfaceHolder holder,final Paint paint){
new Thread(new Runnable() {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.game_name);
@Override
public void run() {
ArrayList<FlyNumber> arrayList=new ArrayList<>();
drawFlayNumbersBessel(holder,paint,arrayList,aniCount);
drawGameName(holder,paint);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if(mListener!=null){
mListener.onAniOver();
}
}
},2000);
}
}).start();
}
int numbers[] = {2,0,4,8};
public FlyNumber generateRandomNumber(){
FlyNumber number = new FlyNumber();
Random random = new Random();
number.setStart(getWidth()/2,getHeight());
number.setControl(getWidth()/2,0);
int endX = random.nextInt(getWidth());
int endY = random.nextInt(getHeight());
if(endY>getHeight()/2){
if (endX>forbidArea&&endX<getWidth()/2){
endX -= forbidArea;
}else if(endX>getWidth()/2 && endX<getWidth()-forbidArea){
endX+=forbidArea;
}
}
number.setEnd(endX,endY);
number.t = 0;
number.number = numbers[random.nextInt(4)];
number.clolor = Color.rgb(random.nextInt(256),random.nextInt(256),random.nextInt(256));
number.textSize = random.nextInt(textSize);
return number;
}
final int MAX = 100;
public void drawFlayNumbersBessel(SurfaceHolder holder,Paint paint,ArrayList<FlyNumber> arrayList,final int count){
int countl = 0;
final float gapT = 1.0f/MAX;
for(int i=0;i<100;i++){
arrayList.add(generateRandomNumber());
}
Canvas canvas;
FlyNumber number;
while (countl++<(count)){
canvas = holder.lockCanvas();
if(countl<count-MAX){
arrayList.add(generateRandomNumber());
arrayList.add(generateRandomNumber());
}
if(canvas != null){
canvas.drawColor(Color.WHITE);
paint.setColor(bgColor);
canvas.drawRoundRect(0,0,getWidth(),getHeight(),cornerRadius,cornerRadius,paint);
if(arrayList.size()>0){
Iterator<FlyNumber> iterator = arrayList.iterator();
while (iterator.hasNext()){
number = iterator.next();
if(number.t>=1.0f){
iterator.remove();
}
getBesselFlayNumber(number,gapT);
drawOneFlyNumber(canvas,paint,number);
int dif = count-countl;
if(dif<50 && dif>0){
drawWelComeState(canvas,paint,255-(count-countl)*3,300-(count-countl)*6);
}
}
}
holder.unlockCanvasAndPost(canvas);
}
}
}
private void drawWelComeState(Canvas canvas,Paint paint,int alpha,int textSieze){
paint.setAlpha(alpha);
paint.setTextSize(textSieze);
paint.setColor(Color.WHITE);
String string = "2048";
float width = paint.measureText(string);
canvas.drawText(string,getWidth()/2-width/2,getHeight()/2+textSieze/3,paint);
}
private void drawGameName(SurfaceHolder holder,Paint paint){
Canvas canvas = holder.lockCanvas();
canvas.drawColor(Color.WHITE);
paint.setColor(bgColor);
canvas.drawRoundRect(0,0,getWidth(),getHeight(),cornerRadius,cornerRadius,paint);
drawWelComeState(canvas,paint,255,gameNameSize);
holder.unlockCanvasAndPost(canvas);
}
}