BouncyDroid.java revision 26e4a6ce116b0a756dcaf9c80c466903789cac45
1package com.android.dreamtheater; 2 3import android.animation.PropertyValuesHolder; 4import android.animation.TimeAnimator; 5import android.app.Activity; 6import android.content.Context; 7import android.content.Intent; 8import android.dreams.BasicDream; 9import android.graphics.Canvas; 10import android.graphics.Matrix; 11import android.graphics.Paint; 12import android.graphics.RectF; 13import android.os.Bundle; 14import android.util.AttributeSet; 15import android.util.Log; 16import android.view.MotionEvent; 17import android.view.View; 18import android.view.Gravity; 19import android.view.ViewGroup; 20import android.widget.Button; 21import android.widget.FrameLayout; 22import android.widget.ImageView; 23 24import java.util.LinkedList; 25import java.util.HashMap; 26 27public class BouncyDroid extends Activity { 28 static final boolean DEBUG = true; 29 static final boolean CENTER_DROID = true; 30 31 public static class BouncyView extends FrameLayout 32 { 33 boolean mShowDebug = false; 34 35 static final int RADIUS = 128; 36 37 static final boolean HAS_INITIAL_IMPULSE = true; 38 static final boolean HAS_GRAVITY = true; 39 static final boolean HAS_FRICTION = false; 40 static final boolean HAS_EDGES = true; 41 42 static final boolean STICKY_FINGERS = true; 43 44 static final float MAX_SPEED = 5000f; 45 46 public static class World { 47 public static final float PX_PER_METER = 100f; 48 public static final float GRAVITY = 1000f; 49 public static class Vec { 50 float x; 51 float y; 52 public Vec() { 53 x = y = 0; 54 } 55 public Vec(float _x, float _y) { 56 x = _x; 57 y = _y; 58 } 59 public Vec add(Vec v) { 60 return new Vec(x + v.x, y + v.y); 61 } 62 public Vec mul(float a) { 63 return new Vec(x * a, y * a); 64 } 65 public Vec sub(Vec v) { 66 return new Vec(x - v.x, y - v.y); 67 } 68 public float mag() { 69 return (float) Math.hypot(x, y); 70 } 71 public Vec norm() { 72 float k = 1/mag(); 73 return new Vec(x*k, y*k); 74 } 75 public String toString() { 76 return "(" + x + "," + y + ")"; 77 } 78 } 79 public static class Body { 80 float m, r; 81 Vec p = new Vec(); 82 Vec v = new Vec(); 83 LinkedList<Vec> forces = new LinkedList<Vec>(); 84 LinkedList<Vec> impulses = new LinkedList<Vec>(); 85 public Body(float _m, Vec _p) { 86 m = _m; 87 p = _p; 88 } 89 public void applyForce(Vec f) { 90 forces.add(f); 91 } 92 public void applyImpulse(Vec f) { 93 impulses.add(f); 94 } 95 public void clearForces() { 96 forces.clear(); 97 } 98 public void removeForce(Vec f) { 99 forces.remove(f); 100 } 101 public void step(float dt) { 102 p = p.add(v.mul(dt)); 103 for (Vec f : impulses) { 104 v = v.add(f.mul(dt/m)); 105 } 106 impulses.clear(); 107 for (Vec f : forces) { 108 v = v.add(f.mul(dt/m)); 109 } 110 } 111 public String toString() { 112 return "Body(m=" + m + " p=" + p + " v=" + v + ")"; 113 } 114 } 115 LinkedList<Body> mBodies = new LinkedList<Body>(); 116 public void addBody(Body b) { 117 mBodies.add(b); 118 } 119 120 public void step(float dt) { 121 for (Body b : mBodies) { 122 b.step(dt); 123 } 124 } 125 } 126 127 128 TimeAnimator mAnim; 129 World mWorld; 130 ImageView mBug; 131 View mShowDebugView; 132 HashMap<Integer, World.Vec> mFingers = new HashMap<Integer, World.Vec>(); 133 World.Body mBody; 134 World.Vec mGrabSpot; 135 int mGrabbedPointer = -1; 136 137 public BouncyView(Context context, AttributeSet as) { 138 super(context, as); 139 140 /* 141 setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() { 142 @Override 143 public void onSystemUiVisibilityChange(int visibility) { 144 if (visibility == View.STATUS_BAR_VISIBLE) { 145 ((Activity)getContext()).finish(); 146 } 147 } 148 }); 149 */ 150 151 mBug = new ImageView(context); 152 mBug.setScaleType(ImageView.ScaleType.MATRIX); 153 addView(mBug, new ViewGroup.LayoutParams( 154 ViewGroup.LayoutParams.WRAP_CONTENT, 155 ViewGroup.LayoutParams.WRAP_CONTENT)); 156 157 if (DEBUG) { 158 Button b = new Button(getContext()); 159 b.setText("Debugzors"); 160 b.setBackgroundColor(0); // very hard to see! :) 161 b.setOnClickListener(new View.OnClickListener() { 162 @Override 163 public void onClick(View v) { 164 setDebug(!mShowDebug); 165 } 166 }); 167 addView(b, new FrameLayout.LayoutParams( 168 ViewGroup.LayoutParams.WRAP_CONTENT, 169 ViewGroup.LayoutParams.WRAP_CONTENT, 170 Gravity.TOP|Gravity.RIGHT)); 171 } 172 } 173 174 public void setDebug(boolean d) { 175 if (d != mShowDebug) { 176 if (d) { 177 mShowDebugView = new DebugView(getContext()); 178 mShowDebugView.setLayoutParams( 179 new ViewGroup.LayoutParams( 180 ViewGroup.LayoutParams.MATCH_PARENT, 181 ViewGroup.LayoutParams.MATCH_PARENT 182 )); 183 addView(mShowDebugView); 184 185 mBug.setBackgroundColor(0x2000FF00); 186 } else { 187 if (mShowDebugView != null) { 188 removeView(mShowDebugView); 189 mShowDebugView = null; 190 } 191 mBug.setBackgroundColor(0); 192 } 193 invalidate(); 194 mShowDebug = d; 195 } 196 } 197 198 private void reset() { 199 mWorld = new World(); 200 final float mass = 100; 201 mBody = new World.Body(mass, new World.Vec(200,200)); 202 mBody.r = RADIUS; 203 mWorld.addBody(mBody); 204 mGrabbedPointer = -1; 205 206 mAnim = new TimeAnimator(); 207 mAnim.setTimeListener(new TimeAnimator.TimeListener() { 208 public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { 209 if (deltaTime > 0) { 210 int STEPS = 5; 211 final float dt = deltaTime / (float) STEPS; 212 while (STEPS-->0) { 213 mBody.clearForces(); 214 215 if (HAS_INITIAL_IMPULSE) { 216 // initial oomph 217 if (totalTime == 0) { 218 mBody.applyImpulse(new World.Vec(400000, -200000)); 219 } 220 } 221 222 if (HAS_GRAVITY) { 223 // gravity points down 224 mBody.applyForce(new World.Vec(0, mass * World.GRAVITY)); 225 } 226 227 if (mGrabbedPointer >= 0) { 228 World.Vec finger = mFingers.get(mGrabbedPointer); 229 if (finger == null) { 230 // let go! 231 mGrabbedPointer = -1; 232 } else { 233 // never gonna let you go 234 World.Vec newPos = finger.add(mGrabSpot); 235 mBody.v = mBody.v.add(newPos.sub(mBody.p).mul(dt)); 236 mBody.p = newPos; 237 } 238 } else { 239 // springs 240 // ideal Hooke's Law plus a maximum force and a minimum length cutoff 241 for (Integer i : mFingers.keySet()) { 242 World.Vec finger = mFingers.get(i); 243 World.Vec springForce = finger.sub(mBody.p); 244 float mag = springForce.mag(); 245 246 if (STICKY_FINGERS && mag < mBody.r*0.75) { 247 // close enough; we'll call this a "stick" 248 mGrabbedPointer = i; 249 mGrabSpot = mBody.p.sub(finger); 250 mBody.v = new World.Vec(0,0); 251 break; 252 } 253 254 final float SPRING_K = 300000; 255 final float FORCE_MAX = 10*SPRING_K; 256 mag = (float) Math.min(mag * SPRING_K, FORCE_MAX); // Hooke's law 257 // float mag = (float) (FORCE_MAX / Math.pow(springForce.mag(), 2)); // Gravitation 258 springForce = springForce.norm().mul(mag); 259 mBody.applyForce(springForce); 260 } 261 } 262 263 if (HAS_FRICTION) { 264 // sliding friction opposes movement 265 mBody.applyForce(mBody.v.mul(-4f * mBody.m)); 266 } 267 268 if (HAS_EDGES) { 269 if (mBody.p.x - mBody.r < 0) { 270 mBody.v.x = (float) Math.abs(mBody.v.x) * 271 (HAS_FRICTION ? 0.7f : 1f); 272 } else if (mBody.p.x + mBody.r > getWidth()) { 273 mBody.v.x = (float) Math.abs(mBody.v.x) * 274 (HAS_FRICTION ? -0.7f : -1f); 275 } 276 if (mBody.p.y - mBody.r < 0) { 277 mBody.v.y = (float) Math.abs(mBody.v.y) * 278 (HAS_FRICTION ? 0.7f : 1f); 279 } else if (mBody.p.y + mBody.r > getHeight()) { 280 mBody.v.y = (float) Math.abs(mBody.v.y) * 281 (HAS_FRICTION ? -0.7f : -1f); 282 } 283 } 284 285 if (MAX_SPEED > 0) { 286 if (mBody.v.mag() > MAX_SPEED) { 287 mBody.v = mBody.v.norm().mul(MAX_SPEED); 288 } 289 } 290 291 // ok, Euler, do your thing 292 mWorld.step(dt / 1000f); // dt is in sec 293 } 294 } 295 mBug.setTranslationX(mBody.p.x - mBody.r); 296 mBug.setTranslationY(mBody.p.y - mBody.r); 297 298 Matrix m = new Matrix(); 299 m.setScale( 300 (mBody.v.x < 0) ? -1 : 1, 301 (mBody.v.y > 1500) ? -1 : 1, // AAAAAAAAAAAAAAAA 302 RADIUS, RADIUS); 303 mBug.setImageMatrix(m); 304 if (CENTER_DROID) { 305 mBug.setImageResource( 306 (Math.abs(mBody.v.x) < 25) 307 ? R.drawable.bouncy_center 308 : R.drawable.bouncy); 309 } 310 311 if (mShowDebug) mShowDebugView.invalidate(); 312 } 313 }); 314 } 315 316 @Override 317 protected void onAttachedToWindow() { 318 super.onAttachedToWindow(); 319 setSystemUiVisibility(View.STATUS_BAR_HIDDEN); 320 321 reset(); 322 mAnim.start(); 323 } 324 325 @Override 326 protected void onDetachedFromWindow() { 327 super.onDetachedFromWindow(); 328 mAnim.cancel(); 329 } 330 331 @Override 332 public boolean onTouchEvent(MotionEvent event) { 333 int i; 334 for (i=0; i<event.getPointerCount(); i++) { 335 switch (event.getActionMasked()) { 336 case MotionEvent.ACTION_DOWN: 337 case MotionEvent.ACTION_MOVE: 338 case MotionEvent.ACTION_POINTER_DOWN: 339 mFingers.put(event.getPointerId(i), 340 new World.Vec(event.getX(i), event.getY(i))); 341 break; 342 343 case MotionEvent.ACTION_UP: 344 case MotionEvent.ACTION_POINTER_UP: 345 mFingers.remove(event.getPointerId(i)); 346 break; 347 348 case MotionEvent.ACTION_CANCEL: 349 mFingers.clear(); 350 break; 351 } 352 } 353 // expired pointers 354 // for (; i<mFingers.length; i++) { 355 // mFingers[i] = null; 356 // } 357 return true; 358 } 359 360 @Override 361 public boolean isOpaque() { 362 return true; 363 } 364 365 class DebugView extends View { 366 public DebugView(Context ct) { 367 super(ct); 368 } 369 370 private void drawVector(Canvas canvas, 371 float x, float y, float vx, float vy, 372 Paint pt) { 373 final float mag = (float) Math.hypot(vx, vy); 374 375 canvas.save(); 376 Matrix mx = new Matrix(); 377 mx.setSinCos(-vx/mag, vy/mag); 378 mx.postTranslate(x, y); 379 canvas.setMatrix(mx); 380 381 canvas.drawLine(0,0, 0, mag, pt); 382 canvas.drawLine(0, mag, -4, mag-4, pt); 383 canvas.drawLine(0, mag, 4, mag-4, pt); 384 385 canvas.restore(); 386 } 387 388 @Override 389 protected void onDraw(Canvas canvas) { 390 super.onDraw(canvas); 391 392 Paint pt = new Paint(Paint.ANTI_ALIAS_FLAG); 393 pt.setColor(0xFFCC0000); 394 pt.setTextSize(30f); 395 pt.setStrokeWidth(1.0f); 396 397 for (Integer id : mFingers.keySet()) { 398 World.Vec v = mFingers.get(id); 399 float x = v.x; 400 float y = v.y; 401 pt.setStyle(Paint.Style.FILL); 402 canvas.drawText("#"+id, x+38, y-38, pt); 403 pt.setStyle(Paint.Style.STROKE); 404 canvas.drawLine(x-40, y, x+40, y, pt); 405 canvas.drawLine(x, y-40, x, y+40, pt); 406 canvas.drawCircle(x, y, 40, pt); 407 } 408 pt.setStyle(Paint.Style.STROKE); 409 if (mBody != null) { 410 float x = mBody.p.x; 411 float y = mBody.p.y; 412 float r = mBody.r; 413 pt.setColor(0xFF6699FF); 414 RectF bounds = new RectF(x-r, y-r, x+r, y+r); 415 canvas.drawOval(bounds, pt); 416 417 pt.setStrokeWidth(3); 418 drawVector(canvas, x, y, mBody.v.x/1000, mBody.v.y/1000, pt); 419 420 pt.setColor(0xFF0033FF); 421 for (World.Vec f : mBody.forces) { 422 drawVector(canvas, x, y, f.x/1000, f.y/1000, pt); 423 } 424 } 425 } 426 } 427 } 428 429 @Override 430 public void onStart() { 431 super.onStart(); 432 setContentView(new BouncyView(this, null)); 433 } 434} 435