Мы воспользуемся не сильно документированным, но активно используемым, для интерактивного управления объектами, в тестовой среде 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);
}
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,
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();
}
}
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,
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))
@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;
}
}
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)||
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, выглядит следующим образом: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;
}
}
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. Те кому лениво воспроизводить все описанные манипуляции руками (лень крайне уважаемое качество программиста) могут забрать готовый проект здесь.