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 forces = new LinkedList(); LinkedList impulses = new LinkedList(); 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 mBodies = new LinkedList(); 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 mFingers = new HashMap(); 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