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;
Vec()53                 public Vec() {
54                     x = y = 0;
55                 }
Vec(float _x, float _y)56                 public Vec(float _x, float _y) {
57                     x = _x;
58                     y = _y;
59                 }
add(Vec v)60                 public Vec add(Vec v) {
61                     return new Vec(x + v.x, y + v.y);
62                 }
mul(float a)63                 public Vec mul(float a) {
64                     return new Vec(x * a, y * a);
65                 }
sub(Vec v)66                 public Vec sub(Vec v) {
67                     return new Vec(x - v.x, y - v.y);
68                 }
mag()69                 public float mag() {
70                     return (float) Math.hypot(x, y);
71                 }
norm()72                 public Vec norm() {
73                     float k = 1/mag();
74                     return new Vec(x*k, y*k);
75                 }
toString()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>();
Body(float _m, Vec _p)86                 public Body(float _m, Vec _p) {
87                     m = _m;
88                     p = _p;
89                 }
applyForce(Vec f)90                 public void applyForce(Vec f) {
91                     forces.add(f);
92                 }
applyImpulse(Vec f)93                 public void applyImpulse(Vec f) {
94                     impulses.add(f);
95                 }
clearForces()96                 public void clearForces() {
97                     forces.clear();
98                 }
removeForce(Vec f)99                 public void removeForce(Vec f) {
100                     forces.remove(f);
101                 }
step(float dt)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                 }
toString()112                 public String toString() {
113                     return "Body(m=" + m + " p=" + p + " v=" + v + ")";
114                 }
115             }
116             LinkedList<Body> mBodies = new LinkedList<Body>();
addBody(Body b)117             public void addBody(Body b) {
118                 mBodies.add(b);
119             }
120 
step(float dt)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 
BouncyView(Context context, AttributeSet as)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 
setDebug(boolean d)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 
reset()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
onAttachedToWindow()320         protected void onAttachedToWindow() {
321             super.onAttachedToWindow();
322             setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
323 
324             reset();
325             mAnim.start();
326         }
327 
328         @Override
onDetachedFromWindow()329         protected void onDetachedFromWindow() {
330             super.onDetachedFromWindow();
331             mAnim.cancel();
332         }
333 
334         @Override
onTouchEvent(MotionEvent event)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
isOpaque()364         public boolean isOpaque() {
365             return true;
366         }
367 
368         class DebugView extends View {
DebugView(Context ct)369             public DebugView(Context ct) {
370                 super(ct);
371             }
372 
drawVector(Canvas canvas, float x, float y, float vx, float vy, Paint pt)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
onDraw(Canvas canvas)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
onStart()433     public void onStart() {
434         super.onStart();
435         setContentView(new BouncyView(this, null));
436     }
437 }
438