android仿摩拜贴纸碰撞|气泡碰撞

转载请注明出处

准备

气泡碰撞最重要的就是边缘检测,气泡的运动涉及到重力,方向,重心,线速度,角速度,,等等一系列因素,想要在android 用view描述现实世界中的气泡实在是难度很大.网上查找资料后,找到了一个正好能满足我们需求的库:jbox2d是一个2D物理引擎,原版是Box2D,采用c++编写,jbox2d是原版的java版.在github下载项目编译生成jar包,生成过程可以参考SyncAny-JBox2D运用-打造摩拜单车贴纸动画效果,感谢SyncAny提供的思路,不过使用gradle打包时报错,通过mvn install成功生成jar包.不想麻烦可以点此 链接 下载,密码:oqwo

JBox2D初步了解点击查看详细介绍
  1. World类(世界) 用于创建物理世界
  2. Body类(刚体)理解为生活中的物体
  3. BodyDef类(刚体描述)位置,状态等
  4. Vec2类(二维向量)可以用来描述运动的方向,力的方向,位置,速度等
  5. FixtureDef类( 夹具,我的理解是物体是属性,包括,shape形状,density密度,friction摩擦系数[0,1],restitution恢复系数[0,1]等)
  6. Shape的子类(描述刚体的形状,有PolygonShape,CircleShape)

有了上面的准备工作,和对JBox2D初步了解,我们就可以开始撸代码啦...
还是先看效果演示: demo.APK预览

ball.gif

实现原理:

通过JBox2D提供的类和方法描述物体的运动状态然后改变android中view状态
android中改变view的位置可以通过实时改变view的坐标位置实现,而这个不停变化的坐标值,我们可以通过Body类获取.可以这样理解,body就是生活中的一个物体,而view则是这个物体的影子,物体向左移动,影子也会跟着向左移动,前提是有光照,而我们要做的就是让这个物体和影子建立联系 绑定起来 ,如何找到 "光"?

就是绑定起来,使用view.setTag()把body绑定起来
实现步骤:
  1. 创建物理世界
if (world == null) {
            world = new World(new Vec2(0f, 10f));//创建世界,设置重力方向,y向量为正数重力方向向下,为负数则向上
        }
  1. 设置世界的边界
    可以采用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设置一个没有重量,没有速度的边界


2108-5-19.png

由上图可以看见,因为红色区域没有重量,没有速度,当物体运动到边界时,无法继续进入红色区域,只能在白色区域运动,由于重力方向向下,最终物体会静止在白色区域底部.
下面看一下如何设置零重力边界:

 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时,会融入其中.
上面就是一个矩形边界,创建四个围成一个封闭的边界,

  1. 创建圆形刚体
    上面我们已经创建了一个多边形刚体,圆形刚体的创建方式和上面相同
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);
  1. 让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可以传入随机数,产生随机的速度

  1. 绑定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()));//线性运动
    }
  1. 实时绘制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()方法中调用,后面回帖出源码

  1. 自定义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/Sphere-collision
box2d 圆形边界的创建 :https://www.jianshu.com/p/1522d97c5b39
转载请注明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 开门见山,一针见血~~先来一张图片再说! 前言 JBox2D是开源的物理引擎Box2D的Java版本,可以直接用于...
    红黑军团号阅读 4,469评论 4 7
  • BYXIAOBAI·2018年4月8日 前端开发者丨HTML5 https://www.rokub.com 前言虽...
    麋鹿_720a阅读 1,478评论 1 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • 做生活的高手,专注你的目标,专注你的梦想,不要被你的生活所打倒,不要把你的眼睛交给你的敌人,要把眼睛交给你的梦想,...
    珣珣爸阅读 95评论 0 0
  • 对桃花醉脸熏熏的日子已远去只留下背影,看如今飞绵滚滚迷了路人的眼睛。我一双老花改了半天卷子格外仿佛上下眼皮粘乎乎有...