blob: f761858bcdcec68b75ae49aa5f97fe96f016b863 [file] [log] [blame]
package com.android.dreamtheater;
import android.animation.PropertyValuesHolder;
import android.animation.TimeAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import java.util.LinkedList;
import java.util.HashMap;
public class BouncyDroid extends Activity {
static final boolean DEBUG = true;
static final boolean CENTER_DROID = true;
public static class BouncyView extends FrameLayout
{
boolean mShowDebug = false;
static final int RADIUS = 100;
static final boolean HAS_INITIAL_IMPULSE = true;
static final boolean HAS_GRAVITY = true;
static final boolean HAS_FRICTION = false;
static final boolean HAS_EDGES = true;
static final boolean STICKY_FINGERS = true;
static final float MAX_SPEED = 5000f;
static final float RANDOM_IMPULSE_PROB = 0.001f;
public static class World {
public static final float PX_PER_METER = 100f;
public static final float GRAVITY = 500f;
public static class Vec {
float x;
float y;
public Vec() {
x = y = 0;
}
public Vec(float _x, float _y) {
x = _x;
y = _y;
}
public Vec add(Vec v) {
return new Vec(x + v.x, y + v.y);
}
public Vec mul(float a) {
return new Vec(x * a, y * a);
}
public Vec sub(Vec v) {
return new Vec(x - v.x, y - v.y);
}
public float mag() {
return (float) Math.hypot(x, y);
}
public Vec norm() {
float k = 1/mag();
return new Vec(x*k, y*k);
}
public String toString() {
return "(" + x + "," + y + ")";
}
}
public static class Body {
float m, r;
Vec p = new Vec();
Vec v = new Vec();
LinkedList<Vec> forces = new LinkedList<Vec>();
LinkedList<Vec> impulses = new LinkedList<Vec>();
public Body(float _m, Vec _p) {
m = _m;
p = _p;
}
public void applyForce(Vec f) {
forces.add(f);
}
public void applyImpulse(Vec f) {
impulses.add(f);
}
public void clearForces() {
forces.clear();
}
public void removeForce(Vec f) {
forces.remove(f);
}
public void step(float dt) {
p = p.add(v.mul(dt));
for (Vec f : impulses) {
v = v.add(f.mul(dt/m));
}
impulses.clear();
for (Vec f : forces) {
v = v.add(f.mul(dt/m));
}
}
public String toString() {
return "Body(m=" + m + " p=" + p + " v=" + v + ")";
}
}
LinkedList<Body> mBodies = new LinkedList<Body>();
public void addBody(Body b) {
mBodies.add(b);
}
public void step(float dt) {
for (Body b : mBodies) {
b.step(dt);
}
}
}
TimeAnimator mAnim;
World mWorld;
ImageView mBug;
View mShowDebugView;
HashMap<Integer, World.Vec> mFingers = new HashMap<Integer, World.Vec>();
World.Body mBody;
World.Vec mGrabSpot;
int mGrabbedPointer = -1;
public BouncyView(Context context, AttributeSet as) {
super(context, as);
/*
setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
if (visibility == View.STATUS_BAR_VISIBLE) {
((Activity)getContext()).finish();
}
}
});
*/
setBackgroundColor(0xFF444444);
mBug = new ImageView(context);
mBug.setScaleType(ImageView.ScaleType.MATRIX);
addView(mBug, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
if (DEBUG) {
Button b = new Button(getContext());
b.setText("Debugzors");
b.setBackgroundColor(0); // very hard to see! :)
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setDebug(!mShowDebug);
}
});
addView(b, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
Gravity.TOP|Gravity.RIGHT));
}
}
public void setDebug(boolean d) {
if (d != mShowDebug) {
if (d) {
mShowDebugView = new DebugView(getContext());
mShowDebugView.setLayoutParams(
new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
addView(mShowDebugView);
mBug.setBackgroundColor(0x2000FF00);
} else {
if (mShowDebugView != null) {
removeView(mShowDebugView);
mShowDebugView = null;
}
mBug.setBackgroundColor(0);
}
invalidate();
mShowDebug = d;
}
}
private void reset() {
mWorld = new World();
final float mass = 100;
mBody = new World.Body(mass, new World.Vec(200,200));
mBody.r = RADIUS;
mWorld.addBody(mBody);
mGrabbedPointer = -1;
mAnim = new TimeAnimator();
mAnim.setTimeListener(new TimeAnimator.TimeListener() {
public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
if (deltaTime > 0) {
int STEPS = 5;
final float dt = deltaTime / (float) STEPS;
while (STEPS-->0) {
mBody.clearForces();
if (HAS_INITIAL_IMPULSE) {
// initial oomph
if (totalTime == 0) {
mBody.applyImpulse(new World.Vec(400000, -200000));
}
}
if (HAS_GRAVITY) {
// gravity points down
mBody.applyForce(new World.Vec(0, mass * World.GRAVITY));
}
if (mGrabbedPointer >= 0) {
World.Vec finger = mFingers.get(mGrabbedPointer);
if (finger == null) {
// let go!
mGrabbedPointer = -1;
} else {
// never gonna let you go
World.Vec newPos = finger.add(mGrabSpot);
mBody.v = mBody.v.add(newPos.sub(mBody.p).mul(dt));
mBody.p = newPos;
}
} else {
// springs
// ideal Hooke's Law plus a maximum force and a minimum length cutoff
for (Integer i : mFingers.keySet()) {
World.Vec finger = mFingers.get(i);
World.Vec springForce = finger.sub(mBody.p);
float mag = springForce.mag();
if (STICKY_FINGERS && mag < mBody.r*0.75) {
// close enough; we'll call this a "stick"
mGrabbedPointer = i;
mGrabSpot = mBody.p.sub(finger);
mBody.v = new World.Vec(0,0);
break;
}
final float SPRING_K = 30000;
final float FORCE_MAX = 10*SPRING_K;
mag = (float) Math.min(mag * SPRING_K, FORCE_MAX); // Hooke's law
// float mag = (float) (FORCE_MAX / Math.pow(springForce.mag(), 2)); // Gravitation
springForce = springForce.norm().mul(mag);
mBody.applyForce(springForce);
}
}
if (HAS_FRICTION) {
// sliding friction opposes movement
mBody.applyForce(mBody.v.mul(-0.01f * mBody.m));
}
if (HAS_EDGES) {
if (mBody.p.x - mBody.r < 0) {
mBody.v.x = (float) Math.abs(mBody.v.x) *
(HAS_FRICTION ? 0.95f : 1f);
} else if (mBody.p.x + mBody.r > getWidth()) {
mBody.v.x = (float) Math.abs(mBody.v.x) *
(HAS_FRICTION ? -0.95f : -1f);
}
if (mBody.p.y - mBody.r < 0) {
mBody.v.y = (float) Math.abs(mBody.v.y) *
(HAS_FRICTION ? 0.95f : 1f);
} else if (mBody.p.y + mBody.r > getHeight()) {
mBody.v.y = (float) Math.abs(mBody.v.y) *
(HAS_FRICTION ? -0.95f : -1f);
}
}
if (MAX_SPEED > 0) {
if (mBody.v.mag() > MAX_SPEED) {
mBody.v = mBody.v.norm().mul(MAX_SPEED);
}
}
// ok, Euler, do your thing
mWorld.step(dt / 1000f); // dt is in sec
}
}
mBug.setTranslationX(mBody.p.x - mBody.r);
mBug.setTranslationY(mBody.p.y - mBody.r);
Matrix m = new Matrix();
m.setScale(
(mBody.v.x < 0) ? -1 : 1,
(mBody.v.y > 1500) ? -1 : 1, // AAAAAAAAAAAAAAAA
RADIUS, RADIUS);
mBug.setImageMatrix(m);
if (CENTER_DROID) {
mBug.setImageResource(
(Math.abs(mBody.v.x) < 25)
? R.drawable.bouncy_center
: R.drawable.bouncy);
}
if (mShowDebug) mShowDebugView.invalidate();
}
});
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
reset();
mAnim.start();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mAnim.cancel();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int i;
for (i=0; i<event.getPointerCount(); i++) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_POINTER_DOWN:
mFingers.put(event.getPointerId(i),
new World.Vec(event.getX(i), event.getY(i)));
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
mFingers.remove(event.getPointerId(i));
break;
case MotionEvent.ACTION_CANCEL:
mFingers.clear();
break;
}
}
// expired pointers
// for (; i<mFingers.length; i++) {
// mFingers[i] = null;
// }
return true;
}
@Override
public boolean isOpaque() {
return true;
}
class DebugView extends View {
public DebugView(Context ct) {
super(ct);
}
private void drawVector(Canvas canvas,
float x, float y, float vx, float vy,
Paint pt) {
final float mag = (float) Math.hypot(vx, vy);
canvas.save();
Matrix mx = new Matrix();
mx.setSinCos(-vx/mag, vy/mag);
mx.postTranslate(x, y);
canvas.setMatrix(mx);
canvas.drawLine(0,0, 0, mag, pt);
canvas.drawLine(0, mag, -4, mag-4, pt);
canvas.drawLine(0, mag, 4, mag-4, pt);
canvas.restore();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint pt = new Paint(Paint.ANTI_ALIAS_FLAG);
pt.setColor(0xFFCC0000);
pt.setTextSize(30f);
pt.setStrokeWidth(1.0f);
for (Integer id : mFingers.keySet()) {
World.Vec v = mFingers.get(id);
float x = v.x;
float y = v.y;
pt.setStyle(Paint.Style.FILL);
canvas.drawText("#"+id, x+38, y-38, pt);
pt.setStyle(Paint.Style.STROKE);
canvas.drawLine(x-40, y, x+40, y, pt);
canvas.drawLine(x, y-40, x, y+40, pt);
canvas.drawCircle(x, y, 40, pt);
}
pt.setStyle(Paint.Style.STROKE);
if (mBody != null) {
float x = mBody.p.x;
float y = mBody.p.y;
float r = mBody.r;
pt.setColor(0xFF6699FF);
RectF bounds = new RectF(x-r, y-r, x+r, y+r);
canvas.drawOval(bounds, pt);
pt.setStrokeWidth(3);
drawVector(canvas, x, y, mBody.v.x/100, mBody.v.y/100, pt);
pt.setColor(0xFF0033FF);
for (World.Vec f : mBody.forces) {
drawVector(canvas, x, y, f.x/1000, f.y/1000, pt);
}
}
}
}
}
@Override
public void onStart() {
super.onStart();
setContentView(new BouncyView(this, null));
}
}