воскресенье, 29 июля 2012 г.

Я хочу чтобы картинка ожила

После того как проходит эйфория, вызванная успешным приручением Box2d, мы замечаем, что обе точки подвеса нашей "нити" намертво прибиты гвоздями к "земле". Понятно, что как-то пошевелить их в этом состоянии вряд-ли удастся (а нам бы очень этого хотелось). К счастью, нам не придется выдумывать никаких костылей, поскольку разработчики Box2d эту трудность предусмотрели.
Мы воспользуемся не сильно документированным, но активно используемым, для интерактивного управления объектами, в тестовой среде Testbed (исходные текст которого доступны для анализа) MouseJoint-ом. Нам понадобиться два постоянно действующих MouseJoint (никто и нигде не говорил, что MouseJoint может быть только один), для привязки каждого из концов "нити" к некоторой позиции на экране.
Помимо этого, мы несколько усложним конструкцию "нити", добавив к каждому RevoluteJoint (позволяющему соединенным объектам поворачиваться относительно точки соединения) DistanceJoint (позволяющий зафиксировать неизменное расстояние, между соединенными объектами). 
Кроме того, мы принесем жертву богам инкапсуляции и полиморфизма и абстрагируем высокоуровневые методы (такие как отрисовка объекта или обработка им событий) в интерфейс IObject, что позволит нам работать с "нитью" как с единым целым, а не набором звеньев, а также добавлять другие типы обрабатываемых объектов (в конце концов, одна единственная нить на экране, это не очень интересно, будь она хоть трижды реалистична):

package com.WhiteRabbit.YoYo.objects;

import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.joints.Joint;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.MotionEvent;

public interface IObject {
    void addBody(Body b);
    void addHandle(Joint j, Body b);
    void draw(Canvas c, Paint p);
    boolean onTouchEvent(MotionEvent event);
}

Измененный класс Task будет выглядеть следующим образом:

package com.WhiteRabbit.YoYo;

import java.util.Timer;
import java.util.TimerTask;

import org.jbox2d.collision.shapes.PolygonShape;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.BodyType;
import org.jbox2d.dynamics.FixtureDef;
import org.jbox2d.dynamics.World;
import org.jbox2d.dynamics.joints.DistanceJointDef;
import org.jbox2d.dynamics.joints.MouseJointDef;
import org.jbox2d.dynamics.joints.RevoluteJointDef;

import com.WhiteRabbit.YoYo.objects.Cord;
import com.WhiteRabbit.YoYo.objects.IObject;

public class Task extends TimerTask  {
   
    private final static int CHAIN_CNT = 30;

    private static long SECONDS_INTERVAL = 1000L;
    private static long FRAME_RATE = 15L;
   
    private YoYoActivity ctx;
    private Timer timer = new Timer();
   
    private World             world;
    private Body              ground;
   
    public Task(YoYoActivity ctx) {
        this.ctx  = ctx;
        configure();
    }
   
    private void configure() {
        MainView v = ctx.getView();
        float X = v.SZ_X / 2;
        float Y = v.SZ_Y - (v.SZ_Y / 5);
       
        // Создать мир
        Vec2 gravity = new Vec2(0.0f, 10.0f);
        world = new World(gravity, true);
        BodyDef bd = new BodyDef();
        ground = world.createBody(bd);
       
        // Создать нить
        PolygonShape shape = new PolygonShape();
        shape.setAsBox(0.5f, 0.125f);
        FixtureDef fd = new FixtureDef();
        fd.shape = shape;
        fd.density = 20.0f;
        fd.friction = 0.2f;
        IObject cord = new Cord(v);
        v.addObj(cord);
        Body prevBody  = ground;
        Body firstBody = null;
        for (int i = 0; i < CHAIN_CNT; i++) {
            bd = new BodyDef();
            bd.type = BodyType.DYNAMIC;
            bd.position.set(X - 14.5f + (1.0f * i), Y);
            bd.linearDamping = 1;
            Body body = world.createBody(bd);
            body.createFixture(fd);
            cord.addBody(body);
            if (firstBody == null) {
                firstBody = body;
            } else {
                RevoluteJointDef jd = new RevoluteJointDef();
                jd.collideConnected = false;
                Vec2 anchor = new Vec2(X - 15.0f + (1.0f * i), Y);
                jd.initialize(prevBody, body, anchor);
                world.createJoint(jd);
                DistanceJointDef dd = new DistanceJointDef();
                dd.initialize(prevBody, body, 
                              prevBody.getWorldCenter(), body.getWorldCenter());
                dd.dampingRatio = 0;
                dd.frequencyHz = 0;
                dd.collideConnected = false;
                world.createJoint(dd);
            }
            prevBody = body;
        }
       
        MouseJointDef mj = new MouseJointDef();
        mj.bodyA  = ground;
        mj.bodyB  = firstBody;
        mj.target.set(new Vec2(X - 14.5f, Y));
        mj.maxForce = 10000.0f * prevBody.getMass();
        mj.dampingRatio = 1;
        mj.frequencyHz = 100;
        cord.addHandle(world.createJoint(mj), firstBody);
        mj.bodyB  = prevBody;
        mj.target.set(new Vec2(X - 15.0f + (1.0f * CHAIN_CNT), Y));
        mj.maxForce = 10000.0f * prevBody.getMass();
        cord.addHandle(world.createJoint(mj), prevBody);

        // Запустить рассчет
        timer.scheduleAtFixedRate(this, 0, SECONDS_INTERVAL / FRAME_RATE);
    }

    public void terminate() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }

    @Override
    public void run() {
        // Выполнить расчет итерации
        float timeStep = 2.0f / FRAME_RATE;
        world.step(timeStep, 10, 10);
        // Обновить view
        ctx.getView().update();
    }
}
В добавленных Joint-ах, стоит обратить внимание на параметры frequencyHz и dampingRatio, используя которые можно задавать частоту колебаний "пружинного" соединения и коэффициент затухания этих колебаний, соответственно. 
Реализация метода onDraw в MainView упроститься. Также добавиться обработка событий MotionEvent:

package com.WhiteRabbit.YoYo;

import java.util.ArrayList;
import java.util.List;

import com.WhiteRabbit.YoYo.objects.IObject;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MainView extends SurfaceView implements SurfaceHolder.Callback {
   
    private YoYoActivity ctx;
   
    public final float SZ_X = 35;    // Физические размеры
    public float SZ_Y;  
   
    private int maxX = 10000;
    private int maxY = 10000;
    private int szX;                  // Экранные размеры
    private int szY;
   
    private List<IObject> objs = new ArrayList<IObject>();
    private Paint paint = new Paint();
   
    public MainView(YoYoActivity ctx) {
        super(ctx);
        this.ctx = ctx;
        this.getHolder().addCallback(this);
    }
   
    public void addObj(IObject obj) {
        objs.add(obj);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, 
                               int width, int height) {}

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        update();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {}
   
    public void update() {
        Canvas c = null;
        try {
            c = this.getHolder().lockCanvas();
            if (c != null) {
                onDraw(c);
            }
        } catch (Exception e) {
        } finally {
            if (c != null) {
                this.getHolder().unlockCanvasAndPost(c);
            }
        }
    }
   
    public float xFromWorld(float x) {
        return (x * szX) / SZ_X;
    }
   
    public float yFromWorld(float y) {
        return (y * szY) / SZ_Y;
    }
   
    public float xToWorld(float x) {
        return (x * SZ_X) / szX;
    }
   
    public float yToWorld(float y) {
        return (y * SZ_Y) / szY;
    }
   
    public float getScaledX(float x) {
        return (x * szX) / 320f;
    }
   
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if ((MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) ||
            (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED))
        {
            setMeasuredDimension(maxX, maxY);
        } else {
            szX = MeasureSpec.getSize(widthMeasureSpec);
            szY = MeasureSpec.getSize(heightMeasureSpec);
            if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
                if (szX > maxX) {
                    szX = maxX;
                }
            }
            if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
                if (szY > maxY) {
                    szY = maxY;
                }
            }
            SZ_Y = (szY * SZ_X)/szX;
            setMeasuredDimension(szX, szY);
            ctx.createTask();
        }
    }
   
    @Override
    protected void onDraw(Canvas c) {
        c.drawColor(Color.BLACK);
        paint.setColor(Color.WHITE);
        paint.setAntiAlias(true);
        for (IObject o: objs) {
            o.draw(c, paint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        for (IObject o: objs) {
            if (o.onTouchEvent(event)) return true;
        }
        return false;
    }
}
Поскольку точек подвеса у нас две, нам понадобиться обрабатывать события Multitouch, уже рассматривавшиеся нами ранее. Нам по прежнему лень возиться с масками action события MotionEvent, поэтому мы будем ассоциировать id каждой точки касания с одной из точек подвеса, а если этот id будет пропадать из списка, разрывать связь. Также, как и в прошлый раз, особой обработки потребует событие отрыва последнего касания, но поскольку, в этот момент, касание будет всего одно, мы будем иметь дело с банальным ACTION_CANCEL, без каких либо дополнительных замаскированных сообщений. Всю логику отрисовки нити (состоящей из соединенных фрагментов), а также обработки событий MotionEvent будет выполнять класс Cord, реализующий интерфейс IObject:

package com.WhiteRabbit.YoYo.objects;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.joints.Joint;

import com.WhiteRabbit.YoYo.MainView;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.MotionEvent;

public class Cord implements IObject {
   
    private MainView view;
    private List<Body> bodies = new ArrayList<Body>();
    private List<Handle> handles = new ArrayList<Handle>();
   
    public Cord(MainView view) {
        this.view = view;
    }
   
    @Override
    public void addBody(Body b) {
        bodies.add(b);
    }
   
    @Override
    public void addHandle(Joint j, Body b) {
        handles.add(new Handle(view, j, b));
    }
   
    @Override
    public void draw(Canvas c, Paint p) {
        boolean f = true;
        float sx = 0, sy = 0, ex = 0, ey = 0;
        float px = 0, py = 0;
        for (Body b: bodies) {
            Vec2 pos = b.getPosition();
            float x = view.xFromWorld(pos.x);
            float y = view.yFromWorld(pos.y);
            if (!f) {
                c.drawLine(px, py, x, y, p);
            } else {
                sx = x;
                sy = y;
            }
            px = x;
            py = y;
            ex = x;
            ey = y;
            f = false;
        }
        float r = view.getScaledX(Handle.RADIUS);
        c.drawCircle(sx, sy, r, p);
        c.drawCircle(ex, ey, r, p);
    }
   
    public boolean onTouchEvent(MotionEvent event) {
        boolean r = false;
        Set<Handle> listToClear = new HashSet<Handle>();
        for (Handle h: handles) {
            listToClear.add(h);
        }
        int cnt = event.getPointerCount();
        if (!((cnt == 1)&&((event.getAction() == MotionEvent.ACTION_UP)||
                           (event.getAction() == MotionEvent.ACTION_CANCEL)))) {
            for (int i = 0; i < cnt; i++) {
                float x = event.getX(i);
                float y = event.getY(i);
                int   pointerId = event.getPointerId(i);
                boolean isFound = false;
                for (Handle h: handles) {
                    Integer id = h.getPointerId();
                    if (id == null) continue;
                    if (pointerId == id) {
                        h.moveTo(x, y);
                        listToClear.remove(h);
                        isFound = true;
                        r = true;
                    }
                }
                if (isFound) continue;
                for (Handle h: handles) {
                    if (h.isCapturing(x, y)) {
                        h.moveTo(x, y);
                        h.setPointerId(pointerId);
                        listToClear.remove(h);
                        r = true;
                    }
                }
            }
        }
        for (Handle h: listToClear) {
            if (h.getPointerId() != null) {
                h.clearPointId();
            }
        }
        return r;
    }
}
Используемый в нем вспомогательный класс Handle, выглядит следующим образом:

package com.WhiteRabbit.YoYo.objects;

import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.joints.Joint;
import org.jbox2d.dynamics.joints.MouseJoint;

import com.WhiteRabbit.YoYo.MainView;

public class Handle {
   
    public final static float RADIUS = 10f;
   
    private MainView view;
    private MouseJoint joint;
    private Body body;
   
    private Integer pointerId = null;
   
    public Handle(MainView view, Joint joint, Body body) {
        this.view  = view;
        this.joint = (MouseJoint)joint;
        this.body  = body;
    }
   
    public Integer getPointerId() {
        return pointerId;
    }
   
    public void setPointerId(int id) {
        pointerId = id;
    }
   
    public boolean isCapturing(float x, float y) {
        Vec2 pos = body.getPosition();
        float r = view.getScaledX(RADIUS) * 10;
        float X = view.xFromWorld(pos.x);
        float Y = view.yFromWorld(pos.y);
        float dist = (float)Math.sqrt((double)((X - x) * (X - x) + 

                                               (Y - y) * (Y - y)));
        return (dist <= r);
    }
   
    public void moveTo(float x, float y) {
        joint.setTarget(new Vec2(view.xToWorld(x), view.yToWorld(y)));
    }
   
    public void clearPointId() {
        joint.setTarget(body.getPosition());
        pointerId = null;
    }
}


Запустив измененный проект под отладчиком, убедимся, что точки подвеса можно двигать, после чего, со спокойной совестью экспортируем apk на устройство и убедимся, что Multitouch также работает.

P.S. Те кому лениво воспроизводить все описанные манипуляции руками (лень крайне уважаемое качество программиста) могут забрать готовый проект здесь.

Комментариев нет:

Отправить комментарий