在学习了一点点Android之后,觉得有必要记录下来,于是就开了这个新坑,慢慢来填吧。
1.运动体Movable类
本例主要模拟了一组大小不一的球以一定的水平初速度从高处落下的运动轨迹。其中的小球为一个可移动物体Movable对象,该类中除了包含小球图片对象之外,还包括了如位置坐标、水平速度、垂直速度等一系列用于模拟小球运动的成员变量和一些方法。
Movable类:
package com.ball;
import android.graphics.Bitmap;
import android.graphics.Canvas;
public class Movable {
int startX = 0; // 初始X坐标
int startY = 0; // 初始Y坐标
int x; // 实时X坐标
int y; // 实时Y坐标
float startVX = 0f; // 初始水平方向的速度
float startVY = 0f; // 初始竖直方向的速度
float v_x = 0f; // 实时水平方向的速度
float v_y = 0f; // 实时竖直方向的速度
int r; // 可移动物体半径
double timeX; // X方向上的运动时间
double timeY; // Y方向上的运动时间
Bitmap bitmap=null; // 可移动物体图片
BallThread bt=null; // 负责小球移动
boolean bFall=false; // 小球是否已经从木板上下落
float impactFactor=0.25f; // 小球撞地后速度的损失系数
public Movable(int x,int y,int r,Bitmap bitmap) {
this.startX=x;
this.x=x;
this.startY=y;
this.y=y;
this.r=r;
this.bitmap=bitmap;
timeX=System.nanoTime(); // 获取系统时间初始化timeX
this.v_x=BallView.V_MIN+(int)((BallView.V_MAX-BallView.V_MIN)*Math.random());
bt=new BallThread(this); // 创建并启动BallThread
bt.start();
}
public void drawSelf(Canvas canvas) {
canvas.drawBitmap(bitmap,x,y,null);
}
}
- startX和startY变量记录每一个运动阶段(如从最高点下落到最低点)开始时小球的初始X、Y坐标,在随后的物理计算中,小球的实时X、Y坐标将会是初始坐标加上这段时间内的位移。
- startVX和startVY是小球每一个运动阶段初始时刻在水平方向X和竖直方向Y方向上的速度,两者将用于计算小球的实时速度v_x和v_y。
- timeX和timeY分别代表小球在水平和竖直方向上运动的持续时间,当小球从一个阶段运行到下一个阶段时(如从下落阶段弹起后转入上抛阶段),timeX和timeY将会被重置。
- BallThread对象继承自Thread线程类,起到了物理引擎的作用,负责根据物理公式对球的位置坐标等属性进行修改,从而改变球的运动轨迹。
- 布尔变量bFall用于标识小球是否已经从木板上落下,在程序运行时屏幕的左上部分会有一个木板,所有的小球从木板开始向右进行平抛运动。bFall为false时代表小球仍然在木板上移动,还未落下。
- float变量impactFactor作用是当小球撞到地面上后根据其值对小球水平和竖直方向的速度进行衰减。
- 构造函数中对部分成员变量进行初始化,并启动物理引擎。
- 构造函数中BallView类的两个常量V_MIN和V_MAX分别代表小球水平方向速度的最小值和最大值,此处用于生成小球的随机水平速度。
2.小球物理引擎BallThread类
首先解释一下此物理引擎的工作机制,了解其是如何改变小球的运动轨迹的。
- 运动阶段,本例中将小球的运动按照竖直方向的速度分为若干个阶段,每个阶段中小球在竖直方向上的速度的大小或者是一直增大(下落),或者是一直减小(上升)。即每当小球在竖直方向上的速度发生改变时(如撞地或达到空中最高点),小球就结束该阶段的运动进入一个新的阶段。
- 数值计算,在每个阶段开始,都会记录下开始的时间,同时还会记录在这个阶段小球的初始X、Y坐标,初始X、Y方向上的速度等物理量。之后在这个阶段的运动中,小球的各项实时数据都根据这些记录的物理量以及当前时间计算得出。
- 为零判断,在小球上升的运动中和小球撞击地面后,都需要判断小球的速度是否为零。但是不同于真实的世界,在程序中小球的各项物理量都是离散的(即每隔固定的时间计算出这些物理量的值),小球实际的运动轨迹为一个个离散点。这种情况下如果还采用判断是否为零的方式就有可能出现错误(如在前一次的计算中小球速度为正,下一次的计算为负,跳过了速度为零这个转折点,小球将永远不可能出现为零这个时刻)。所以在程序中使用了阈值的方式,小球的速度一旦小于某个阈值,就将其认定为零。
BallThread类:
package com.ball;
public class BallThread extends Thread {
Movable father; // Movable对象引用
boolean flag = false; // 线程执行标识位
int sleepSpan = 40; // 休眠时间
float g = 200; // 球下落的加速度
double current; // 记录当前时间
public BallThread(Movable father) {
this.father = father;
this.flag = true;
}
@Override
public void run() {
while (flag) {
current = System.nanoTime(); // 获取当前时间,单位为纳秒,处理水平方向上的运动
double timeSpanX = (double) ((current - father.timeX) / 1000 / 1000 / 1000); // 获取水平方向走过的时间
father.x = (int) (father.startX + father.v_x * timeSpanX);
if (father.bFall) { // 处理竖直方向上的运动,判断球是否已经移出挡板
double timeSpanY = (double) ((current - father.timeY) / 1000 / 1000 / 1000);
father.y = (int) (father.startY + father.startVY * timeSpanY + timeSpanY
* timeSpanY * g / 2);
father.v_y = (float) (father.startVY + g * timeSpanY);
//此处先省略检测和处理特殊事件的代码,随后补全
} else if (father.x + father.r / 2 >= BallView.WOOD_EDGE) {// 通过X坐标判断球是否移出了挡板
father.timeY = System.nanoTime();
father.bFall = true; // 确定下落
}
try {
Thread.sleep(sleepSpan);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
-
代表重力加速度的变量g初始化为200,此值是经过测试得出的较为合理的值。
-
run方法处理小球在水平方向的运动时,先根据当前时间获得本阶段中从开始到现在小球在水平方向上运动的时间,然后用该阶段中到目前为止小球的位移加上小球在该阶段的初始位置,求出小球此时的X坐标。
-
run方法处理小球在竖直方向的运动时,先检查小球的bFall是否为true,如果为true表明小球已经可以下落,进行物理计算;如果为false则使用X坐标进行判断是否需要将其设置为true。
-
使用到的BallView类的常量WOOD_EDGE记录着木板图片的最右边的X坐标值,如果小球的水平位置超过该值就需要下落了。
刚才省略的检测代码如下:
// 判断小球是否到达最高点
if (father.startVY < 0 && Math.abs(father.v_y) <= BallView.UP_ZERO) {
father.timeY = System.nanoTime();
father.v_y = 0;
father.startVY = 0;
father.startY = father.y;
}
// 判断小球是否撞地
if (father.y + father.r * 2 >= BallView.GROUND_LING && father.v_y > 0) {
father.v_x = father.v_x * (1 - father.impactFactor); // 衰减水平方向的速度
father.v_y = 0 - father.v_y * (1 - father.impactFactor); // 衰减竖直方向的速度并改变方向
if (Math.abs(father.v_y) < BallView.DOWN_ZERO) { // 判断撞地衰减后的速度,太小就停止运动
this.flag = false;
} else {
// 撞地后的速度还可以弹起继续下一阶段的运动
father.startX = father.x;
father.timeX = System.nanoTime();
father.startY = father.y;
father.timeY = System.nanoTime();
father.startVY = father.v_y;
}
}
3.视图类BallView
BallView是负责画面渲染的视图类,其中声明了一些物理计算时要使用的静态常量,同时还声明了程序中要绘制的图片资源以及要绘制的小球对象列表。BallView类继承自android.view包下SurfaceView类。SurfaceView不同于普通的View,其具有不同的绘制机理,适合用于开发游戏程序。使用SurfaceView需要实现SurfaceHolder.Callback接口,该接口可以对SurfaceView进行编辑等操作,还可以监控SurfaceView的变化。
BallView类:
package com.ball;
import java.util.ArrayList;
import java.util.Random;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
import com.bp.R;
public class BallView extends SurfaceView implements Callback {
public static final int V_MAX=35;
public static final int V_MIN=15;
public static final int WOOD_EDGE=60;
public static final int GROUND_LING=450; //代表地面的Y坐标,小球下落到此会弹起
public static final int UP_ZERO=30; //小球在上升过程中,速度小于该值就算0
public static final int DOWN_ZERO=60; //小球在撞击地面后,速度小于该值就算0
Bitmap[] bitmapArray =new Bitmap[6];
Bitmap bmpBack; //背景图片
Bitmap bmpWood; // 挡板图片
String fps="FPS:N/A"; //用于显示帧速率的字符串
int ballNumber =8; //小球数目
ArrayList<Movable> alMovable=new ArrayList<Movable>(); //小球对象数组
DrawThread dt; //后台屏幕绘制线程
public BallView(Context activity) {
super(activity);
getHolder().addCallback(this);
initBitmaps(getResources()); //初始化图片
initMovables(); //初始化小球
dt=new DrawThread(this,getHolder()); //初始化重绘线程
}
public void initBitmaps(Resources r) {
bitmapArray[0]=BitmapFactory.decodeResource(r, R.drawable.ball_red_small);
bitmapArray[1]=BitmapFactory.decodeResource(r, R.drawable.ball_purple_small);
bitmapArray[2]=BitmapFactory.decodeResource(r, R.drawable.ball_green_small);
bitmapArray[3]=BitmapFactory.decodeResource(r, R.drawable.ball_red);
bitmapArray[4]=BitmapFactory.decodeResource(r, R.drawable.ball_purple);
bitmapArray[5]=BitmapFactory.decodeResource(r, R.drawable.ball_green);
bmpBack=BitmapFactory.decodeResource(r, R.drawable.back);
bmpWood=BitmapFactory.decodeResource(r, R.drawable.wood);
}
public void initMovables() {
Random r=new Random();
for(int i=0;i<ballNumber;i++) {
int index=r.nextInt(32);
Bitmap tempBitmap=null;
if(i<ballNumber/2) { //如果是初始化前一半球,就从大球中随机找一个
tempBitmap=bitmapArray[3+index%3];
} else { //如果是初始化前一半球,就从小球中随机找一个
tempBitmap=bitmapArray[index%3];
}
//创建Movable对象
Movable m=new Movable(0, 70-tempBitmap.getHeight(), tempBitmap.getWidth()/2, tempBitmap);
alMovable.add(m); //加入列表中
}
}
public void doDraw(Canvas canvas) { //绘制程序中需要的图片等信息
canvas.drawBitmap(bmpBack, 0, 0,null);
canvas.drawBitmap(bmpWood, 0, 60,null);
for (Movable m : alMovable) { //遍历绘制每个Movable对象
m.drawSelf(canvas);
}
Paint p=new Paint();
p.setColor(Color.BLUE);
p.setTextSize(18);
p.setAntiAlias(true); //设置抗锯齿
canvas.drawText(fps, 30, 30, p); //画出帧速率字符串
}
@Override
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
}
@Override
public void surfaceCreated(SurfaceHolder arg0) {
if(!dt.isAlive()) {
dt.start();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder arg0) {
dt.flag=false;
dt=null;
}
}
- 因为bitmapArray数组中的图片分为大尺寸和小尺寸,为了绘制图片时,小尺寸图片不会被挡住,所以先使用大尺寸图片。
- doDraw方法会在DrawThread中调用,用于绘制图片和帧速率。
4.绘制线程DrawThread类
DrawThread类:
package com.ball;
import android.graphics.Canvas;
import android.view.SurfaceHolder;
public class DrawThread extends Thread {
BallView bv;
SurfaceHolder surfaceHolder;
boolean flag=false;
int sleepSpan=30;
long start =System.nanoTime(); //记录起始时间,该变量用于计算帧速率
int count=0 ; //记录帧数
public DrawThread(BallView bv,SurfaceHolder surfaceHolder) {
this.bv=bv;
this.surfaceHolder=surfaceHolder;
this.flag=true;
}
public void run() {
Canvas canvas=null;
while(flag) {
try {
canvas=surfaceHolder.lockCanvas(null); //获取BallView的画布
synchronized (surfaceHolder) {
bv.doDraw(canvas);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(canvas!=null) {
surfaceHolder.unlockCanvasAndPost(canvas); // surfaceHolder解锁,并将画布传回
}
}
this.count++;
if(count==20) { //计满20帧时计算一次帧速率
count=0;
long tempStamp=System.nanoTime();
long span=tempStamp-start;
start=tempStamp;
double fps=Math.round(100000000000.0/span*20)/100.0;
bv.fps="FPS:"+fps;
}
try {
Thread.sleep(sleepSpan);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 代码中调用了BallView的屏幕重绘函数doDraw,其实现机制是现将BalView的画布加锁,然后调用BallView的doDraw方法对BallView的画布进行重新绘制。最后解锁BallView的画布并将其传回。
- 计算帧速率的方法是,首先求出程序绘制20帧所消耗的时间span,然后计算100s内能够包含几个span。100s内包含的span个数乘以20就能得出100s内能够绘制几帧,再除以100就可求得1s内绘制的帧数。
5.MainActivity类
MainActivity类:
package com.ball;
import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
public class MainActivity extends Activity {
BallView bv;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE); //设置不显示标题
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); //设置全屏
bv=new BallView(this);
setContentView(bv);
}
}
运行效果图:
使用到的资源文件: