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