转载请注明出处
准备
气泡碰撞最重要的就是边缘检测,气泡的运动涉及到重力,方向,重心,线速度,角速度,等等一系列因素,想要在android 用view描述现实世界中的气泡实在是难度很大.网上查找资料后,找到了一个正好能满足我们需求的库:jbox2d是一个2D物理引擎,原版是Box2D,采用c++编写,jbox2d是原版的java版.在github下载项目编译生成jar包,生成过程可以参考SyncAny-JBox2D运用-打造摩拜单车贴纸动画效果,感谢SyncAny提供的思路,不过使用gradle打包时报错,通过mvn install
成功生成jar包.不想麻烦可以点此 链接 下载,密码:oqwo
JBox2D初步了解点击查看详细介绍
- World类(世界) 用于创建物理世界
- Body类(刚体)理解为生活中的物体
- BodyDef类(刚体描述)位置,状态等
- Vec2类(二维向量)可以用来描述运动的方向,力的方向,位置,速度等
- FixtureDef类( 夹具,我的理解是物体是属性,包括,shape形状,density密度,friction摩擦系数[0,1],restitution恢复系数[0,1]等)
- Shape的子类(描述刚体的形状,有PolygonShape,CircleShape)
有了上面的准备工作,和对JBox2D初步了解,我们就可以开始撸代码啦…
还是先看效果演示: demo.APK预览
#####实现原理:
通过JBox2D提供的类和方法描述物体的运动状态然后改变android中view状态
android中改变view的位置可以通过实时改变view的坐标位置实现,而这个不停变化的坐标值,我们可以通过Body类获取.可以这样理解,body就是生活中的一个物体,而view则是这个物体的影子,物体向左移动,影子也会跟着向左移动,前提是有光照,而我们要做的就是让这个物体和影子建立联系 绑定起来 ,如何找到 “光”?
######就是绑定起来,使用view.setTag()把body绑定起来
#####实现步骤:
- 创建物理世界
if (world == null) {
world = new World(new Vec2(0f, 10f));//创建世界,设置重力方向,y向量为正数重力方向向下,为负数则向上
}
- 设置世界的边界
可以采用AABB类设置边界,这里我们通过Body刚体的属性(BodyType类)来框定一个边界.
BodyType类源码:
/**
* The body type.
* static: zero mass, zero velocity, may be manually moved
* kinematic: zero mass, non-zero velocity set by user, moved by solver
* dynamic: positive mass, non-zero velocity determined by forces, moved by solver
*
* @author daniel
*/
public enum BodyType {
STATIC, KINEMATIC, DYNAMIC
}
定义了一个枚举,STATIC:0重量,0速度;KINEMATIC:零质量,非零速度由用户设定;DYNAMIC:非零速度,正质量,非零速度由力决定
所有我们可以通过STATIC设置一个没有重量,没有速度的边界
由上图可以看见,因为红色区域没有重量,没有速度,当物体运动到边界时,无法继续进入红色区域,只能在白色区域运动,由于重力方向向下,最终物体会静止在白色区域底部.
下面看一下如何设置零重力边界:
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyType.STATIC;//设置零重力,零速度
PolygonShape polygonShape1 = new PolygonShape();//创建多边形实例
polygonShape1.setAsBox(bodyWidth, bodyRatio);//多边形设置成为盒型也就是矩形,传入的参数为宽高
FixtureDef fixtureDef = new FixtureDef();
fixtureDef.shape = polygonShape1;//形状设置为上面的多边形(盒型)
fixtureDef.density = 1f;//物质密度随意
fixtureDef.friction = 0.3f;//摩擦系数[0,1]
fixtureDef.restitution = 0.5f;//恢复系数[0,1]
bodyDef.position.set(0, -bodyRatio);//边界的位置
Body bodyTop = world.createBody(bodyDef);//世界中创建刚体
bodyTop.createFixture(fixtureDef);//刚体添加夹具
对于恢复系数restitution 等于1时,为完全弹性碰撞,在(0,1)之间为非完全弹性碰撞,等于0时,会融入其中.
上面就是一个矩形边界,创建四个围成一个封闭的边界,
3. 创建圆形刚体
上面我们已经创建了一个多边形刚体,圆形刚体的创建方式和上面相同
CircleShape circleShape = new CircleShape();
circleShape.setRadius(radius);//设置半径
FixtureDef fixture = new FixtureDef();
fixture.setShape(shape);
fixture.density = density;
fixture.friction = friction;
fixture.restitution = restitution;
Body body = world.createBody(bodyDef);//用世界创建出刚体
body.createFixture(fixture);
- 让body刚体动起来
查看Body的源码可以知道,提供了一些运动的方法
/**
* Set the linear velocity of the center of mass.
*
* @param v the new linear velocity of the center of mass.
*/
public final void setLinearVelocity(Vec2 v) {
if (m_type == BodyType.STATIC) {
return;
}
if (Vec2.dot(v, v) > 0.0f) {
setAwake(true);
}
m_linearVelocity.set(v);
}
通过调用setLinearVelocity(Vec2 v)给body的重心设置一个线性速度
/**
* Apply an impulse at a point. This immediately modifies the velocity. It also modifies the
* angular velocity if the point of application is not at the center of mass. This wakes up the
* body if 'wake' is set to true. If the body is sleeping and 'wake' is false, then there is no
* effect.
*
* @param impulse the world impulse vector, usually in N-seconds or kg-m/s.
* @param point the world position of the point of application.
* @param wake also wake up the body
*/
public final void applyLinearImpulse(Vec2 impulse, Vec2 point, boolean wake) {
if (m_type != BodyType.DYNAMIC) {
return;
}
if (!isAwake()) {
if (wake) {
setAwake(true);
} else {
return;
}
}
m_linearVelocity.x += impulse.x * m_invMass;
m_linearVelocity.y += impulse.y * m_invMass;
m_angularVelocity +=
m_invI * ((point.x - m_sweep.c.x) * impulse.y - (point.y - m_sweep.c.y) * impulse.x);
}
调用applyLinearImpulse(Vec2 impulse, Vec2 point, boolean wake) 给某一点施加一个脉冲,会立刻修改速度,如果作用点不在重心就会修改角速度.Vec2可以传入随机数,产生随机的速度
5. 绑定view
我们获取viewGroup的子控件view的个数,创建对用数量的body,并且给view设置tag为body
...
int childCount = mViewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View childAt = mViewGroup.getChildAt(i);
Body body = (Body) childAt.getTag(R.id.body_tag);
if (body == null || haveDifferent) {
createBody(world, childAt);
}
}
...
/**
* 创建刚体
*/
private void createBody(World world, View view) {
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyType.DYNAMIC;//有重量,有速度
bodyDef.position.set(view.getX() + view.getWidth() / 2 ,view.getY() + view.getHeight() / 2);
Shape shape = null;
Boolean isCircle = (Boolean) view.getTag(R.id.circle_tag);
if (isCircle != null && isCircle) {
shape = createCircle(view);
}
FixtureDef fixture = new FixtureDef();
fixture.setShape(shape);
fixture.friction = friction;
fixture.restitution = restitution;
fixture.density = density;
Body body = world.createBody(bodyDef);
body.createFixture(fixture);
view.setTag(R.id.body_tag, body);//给view绑定body
body.setLinearVelocity(new Vec2(random.nextFloat(), random.nextFloat()));//线性运动
}
- 实时绘制view的位置
实时获取body刚体的位置,然后设置给view,调用invalidate()重绘
public void onDraw(Canvas canvas) {
if (!startEnable)
return;
world.step(dt,velocityIterations,positionIterations);
int childCount = mViewGroup.getChildCount();
for(int i = 0; i < childCount; i++){
View view = mViewGroup.getChildAt(i);
Body body = (Body) view.getTag(R.id.body_tag); //从view中获取绑定的刚体
if(body != null){
view.setX(body.getPosition().x - view.getWidth() / 2);//获取刚体的位置信息
view.setY(body.getPosition().y - view.getHeight() / 2);
view.setRotation(radiansToDegrees(body.getAngle() % 360));//设置旋转角度
}
}
mViewGroup.invalidate();//更新view的位置
}
这个方法在重写的自定义父控件的onDraw()方法中调用,后面回帖出源码
7. 自定义ViewGroup
代码很简单,需要注意的是,我们需要重写onDraw()方法,所以要清除WillNotDraw标记,这样onDraw(0方法才会执行.
public class PoolBallView extends FrameLayout {
private BallView ballView;
public PoolBallView(Context context) {
this(context,null);
}
public PoolBallView(Context context, AttributeSet attrs) {
this(context, attrs,-1);
}
public PoolBallView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setWillNotDraw(false);//重写ondraw需要
ballView = new BallView(context, this);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
ballView.onLayout(changed);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
ballView.onDraw(canvas);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
ballView.onSizeChanged(w,h);
}
public BallView getBallView(){
return this.ballView;
}
}
使用
xml布局中添加此ViewGroup容器
java操作代码
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
for (int i = 0; i < imgs.length; i++) {
ImageView imageView = new ImageView(this);//创建imageView控件
imageView.setImageResource(imgs[i]);//设置资源图片
imageView.setTag(R.id.circle_tag, true);//设置tag
poolBall.addView(imageView, layoutParams);//把imageView添加到父控件
imageView.setOnClickListener(new View.OnClickListener() {//设置点击事件
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "点击了气泡", Toast.LENGTH_SHORT).show();
}
});
ok至此,小球就会自由落体运动了,后面可以再加入陀螺仪方向感应,等控制操作,下面贴出小球创建控制类代码:
public class BallView {
private Context context;
private World world;//世界
private int pWidth;//父控件的宽度
private int pHeight;//父控件的高度
private ViewGroup mViewGroup;//父控件
private float density = 0.5f;//物质密度
private float friction = 0.5f;//摩擦系数
private float restitution = 0.5f;//恢复系数
private final Random random;
private boolean startEnable = true;//是否开始绘制
private int velocityIterations = 3;//迭代速度
private int positionIterations = 10;//位置迭代
private float dt = 1f / 60;//刷新时间
private int ratio = 50;//物理世界与手机虚拟比例
public BallView(Context context, ViewGroup viewGroup) {
this.context = context;
this.mViewGroup = viewGroup;
random = new Random();
}
public void onDraw(Canvas canvas) {
if (!startEnable)
return;
world.step(dt, velocityIterations, positionIterations);
int childCount = mViewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View view = mViewGroup.getChildAt(i);
Body body = (Body) view.getTag(R.id.body_tag); //从view中获取绑定的刚体
if (body != null) {
//获取刚体的位置信息
view.setX(metersToPixels(body.getPosition().x) - view.getWidth() / 2);
view.setY(metersToPixels(body.getPosition().y) - view.getHeight() / 2);
view.setRotation(radiansToDegrees(body.getAngle() % 360));
}
}
mViewGroup.invalidate();//更新view的位置
}
/**
* @param b
*/
public void onLayout(boolean b) {
createWorld(b);
}
/**
* 创建物理世界
*/
private void createWorld(boolean haveDifferent) {
if (world == null) {
world = new World(new Vec2(0f, 10f));//创建世界,设置重力方向
initWorldBounds();//设置边界
}
int childCount = mViewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View childAt = mViewGroup.getChildAt(i);
Body body = (Body) childAt.getTag(R.id.body_tag);
if (body == null || haveDifferent) {
createBody(world, childAt);
}
}
}
/**
* 创建刚体
*/
private void createBody(World world, View view) {
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyType.DYNAMIC;
//设置初始参数,为view的中心点
bodyDef.position.set(pixelsToMeters(view.getX() + view.getWidth() / 2),
pixelsToMeters(view.getY() + view.getHeight() / 2));
Shape shape = null;
Boolean isCircle = (Boolean) view.getTag(R.id.circle_tag);
if (isCircle != null && isCircle) {
shape = createCircle(view);
}
FixtureDef fixture = new FixtureDef();
fixture.setShape(shape);
fixture.friction = friction;
fixture.restitution = restitution;
fixture.density = density;
//用世界创建出刚体
Body body = world.createBody(bodyDef);
body.createFixture(fixture);
view.setTag(R.id.body_tag, body);
//初始化物体的运动行为
body.setLinearVelocity(new Vec2(random.nextFloat(), random.nextFloat()));
}
/**
* 设置世界边界
*/
private void initWorldBounds() {
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyType.STATIC;//设置零重力,零速度
float bodyWidth = pixelsToMeters(pWidth);
float bodyHeight = pixelsToMeters(pHeight);
float bodyRatio = pixelsToMeters(ratio);
PolygonShape polygonShape1 = new PolygonShape();
polygonShape1.setAsBox(bodyWidth, bodyRatio);
FixtureDef fixtureDef = new FixtureDef();
fixtureDef.shape = polygonShape1;
fixtureDef.density = 1f;//物质密度
fixtureDef.friction = 0.3f;//摩擦系数
fixtureDef.restitution = 0.5f;//恢复系数
bodyDef.position.set(0, -bodyRatio);
Body bodyTop = world.createBody(bodyDef);//世界中创建刚体
bodyTop.createFixture(fixtureDef);//刚体添加夹具
bodyDef.position.set(0, bodyHeight + bodyRatio);
Body bodyBottom = world.createBody(bodyDef);//世界中创建刚体
bodyBottom.createFixture(fixtureDef);
PolygonShape polygonShape2 = new PolygonShape();
polygonShape2.setAsBox(bodyRatio, bodyHeight);
FixtureDef fixtureDef2 = new FixtureDef();
fixtureDef2.shape = polygonShape2;
fixtureDef2.density = 0.5f;//物质密度
fixtureDef2.friction = 0.3f;//摩擦系数
fixtureDef2.restitution = 0.5f;//恢复系数
bodyDef.position.set(-bodyRatio, bodyHeight);
Body bodyLeft = world.createBody(bodyDef);//世界中创建刚体
bodyLeft.createFixture(fixtureDef2);//刚体添加物理属性
bodyDef.position.set(bodyWidth + bodyRatio, 0);
Body bodyRight = world.createBody(bodyDef);//世界中创建刚体
bodyRight.createFixture(fixtureDef2);//刚体添加物理属性
}
/**
* 创建圆形描述
*/
private Shape createCircle(View view) {
CircleShape circleShape = new CircleShape();
circleShape.setRadius(pixelsToMeters(view.getWidth() / 2));
return circleShape;
}
/**
* 随机运动
* 施加一个脉冲,立刻改变速度
*/
public void rockBallByImpulse() {
int childCount = mViewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
Vec2 mImpulse = new Vec2(random.nextInt(1000), random.nextInt());
View view = mViewGroup.getChildAt(i);
Body body = (Body) view.getTag(R.id.body_tag);
if (body != null) {
body.applyLinearImpulse(mImpulse, body.getPosition(), true);
Log.e("btn", "有脉冲");
} else {
Log.e("btn", "body == null");
}
}
}
/**
* 向指定位置移动
*/
public void rockBallByImpulse(float x, float y) {
int childCount = mViewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
Vec2 mImpulse = new Vec2(x, y);
View view = mViewGroup.getChildAt(i);
Body body = (Body) view.getTag(R.id.body_tag);
if (body != null) {
body.applyLinearImpulse(mImpulse, body.getPosition(), true);
}
}
}
public float metersToPixels(float meters) {
return meters * ratio;
}
public float pixelsToMeters(float pixels) {
return pixels / ratio;
}
/**
* 弧度转角度
*
* @param radians
* @return
*/
private float radiansToDegrees(float radians) {
return radians / 3.14f * 180f;
}
/**
* 大小发生变化
* @param pWidth
* @param pHeight
*/
public void onSizeChanged(int pWidth, int pHeight) {
this.pWidth = pWidth;
this.pHeight = pHeight;
}
private void setStartEnable(boolean b) {
startEnable = b;
}
public void onStart() {
setStartEnable(true);
}
public void onStop() {
setStartEnable(false);
}
}
项目地址:https://github.com/truemi/ballPull
转载请注明出处