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