1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.example.android.accelerometerplay;
18
19import android.app.Activity;
20import android.content.Context;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.graphics.Canvas;
24import android.graphics.BitmapFactory.Options;
25import android.hardware.Sensor;
26import android.hardware.SensorEvent;
27import android.hardware.SensorEventListener;
28import android.hardware.SensorManager;
29import android.os.Bundle;
30import android.os.PowerManager;
31import android.os.PowerManager.WakeLock;
32import android.util.DisplayMetrics;
33import android.view.Display;
34import android.view.Surface;
35import android.view.View;
36import android.view.WindowManager;
37
38/**
39 * This is an example of using the accelerometer to integrate the device's
40 * acceleration to a position using the Verlet method. This is illustrated with
41 * a very simple particle system comprised of a few iron balls freely moving on
42 * an inclined wooden table. The inclination of the virtual table is controlled
43 * by the device's accelerometer.
44 *
45 * @see SensorManager
46 * @see SensorEvent
47 * @see Sensor
48 */
49
50public class AccelerometerPlayActivity extends Activity {
51
52    private SimulationView mSimulationView;
53    private SensorManager mSensorManager;
54    private PowerManager mPowerManager;
55    private WindowManager mWindowManager;
56    private Display mDisplay;
57    private WakeLock mWakeLock;
58
59    /** Called when the activity is first created. */
60    @Override
61    public void onCreate(Bundle savedInstanceState) {
62        super.onCreate(savedInstanceState);
63
64        // Get an instance of the SensorManager
65        mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
66
67        // Get an instance of the PowerManager
68        mPowerManager = (PowerManager) getSystemService(POWER_SERVICE);
69
70        // Get an instance of the WindowManager
71        mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
72        mDisplay = mWindowManager.getDefaultDisplay();
73
74        // Create a bright wake lock
75        mWakeLock = mPowerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, getClass()
76                .getName());
77
78        // instantiate our simulation view and set it as the activity's content
79        mSimulationView = new SimulationView(this);
80        setContentView(mSimulationView);
81    }
82
83    @Override
84    protected void onResume() {
85        super.onResume();
86        /*
87         * when the activity is resumed, we acquire a wake-lock so that the
88         * screen stays on, since the user will likely not be fiddling with the
89         * screen or buttons.
90         */
91        mWakeLock.acquire();
92
93        // Start the simulation
94        mSimulationView.startSimulation();
95    }
96
97    @Override
98    protected void onPause() {
99        super.onPause();
100        /*
101         * When the activity is paused, we make sure to stop the simulation,
102         * release our sensor resources and wake locks
103         */
104
105        // Stop the simulation
106        mSimulationView.stopSimulation();
107
108        // and release our wake-lock
109        mWakeLock.release();
110    }
111
112    class SimulationView extends View implements SensorEventListener {
113        // diameter of the balls in meters
114        private static final float sBallDiameter = 0.004f;
115        private static final float sBallDiameter2 = sBallDiameter * sBallDiameter;
116
117        // friction of the virtual table and air
118        private static final float sFriction = 0.1f;
119
120        private Sensor mAccelerometer;
121        private long mLastT;
122        private float mLastDeltaT;
123
124        private float mXDpi;
125        private float mYDpi;
126        private float mMetersToPixelsX;
127        private float mMetersToPixelsY;
128        private Bitmap mBitmap;
129        private Bitmap mWood;
130        private float mXOrigin;
131        private float mYOrigin;
132        private float mSensorX;
133        private float mSensorY;
134        private long mSensorTimeStamp;
135        private long mCpuTimeStamp;
136        private float mHorizontalBound;
137        private float mVerticalBound;
138        private final ParticleSystem mParticleSystem = new ParticleSystem();
139
140        /*
141         * Each of our particle holds its previous and current position, its
142         * acceleration. for added realism each particle has its own friction
143         * coefficient.
144         */
145        class Particle {
146            private float mPosX;
147            private float mPosY;
148            private float mAccelX;
149            private float mAccelY;
150            private float mLastPosX;
151            private float mLastPosY;
152            private float mOneMinusFriction;
153
154            Particle() {
155                // make each particle a bit different by randomizing its
156                // coefficient of friction
157                final float r = ((float) Math.random() - 0.5f) * 0.2f;
158                mOneMinusFriction = 1.0f - sFriction + r;
159            }
160
161            public void computePhysics(float sx, float sy, float dT, float dTC) {
162                // Force of gravity applied to our virtual object
163                final float m = 1000.0f; // mass of our virtual object
164                final float gx = -sx * m;
165                final float gy = -sy * m;
166
167                /*
168                 * F = mA <=> A = F / m We could simplify the code by
169                 * completely eliminating "m" (the mass) from all the equations,
170                 * but it would hide the concepts from this sample code.
171                 */
172                final float invm = 1.0f / m;
173                final float ax = gx * invm;
174                final float ay = gy * invm;
175
176                /*
177                 * Time-corrected Verlet integration The position Verlet
178                 * integrator is defined as x(t+dt) = x(t) + x(t) - x(t-dt) +
179                 * a(t).t^2 However, the above equation doesn't handle variable
180                 * dt very well, a time-corrected version is needed: x(t+dt) =
181                 * x(t) + (x(t) - x(t-dt)) * (dt/dt_prev) + a(t).t^2 We also add
182                 * a simple friction term (f) to the equation: x(t+dt) = x(t) +
183                 * (1-f) * (x(t) - x(t-dt)) * (dt/dt_prev) + a(t)t^2
184                 */
185                final float dTdT = dT * dT;
186                final float x = mPosX + mOneMinusFriction * dTC * (mPosX - mLastPosX) + mAccelX
187                        * dTdT;
188                final float y = mPosY + mOneMinusFriction * dTC * (mPosY - mLastPosY) + mAccelY
189                        * dTdT;
190                mLastPosX = mPosX;
191                mLastPosY = mPosY;
192                mPosX = x;
193                mPosY = y;
194                mAccelX = ax;
195                mAccelY = ay;
196            }
197
198            /*
199             * Resolving constraints and collisions with the Verlet integrator
200             * can be very simple, we simply need to move a colliding or
201             * constrained particle in such way that the constraint is
202             * satisfied.
203             */
204            public void resolveCollisionWithBounds() {
205                final float xmax = mHorizontalBound;
206                final float ymax = mVerticalBound;
207                final float x = mPosX;
208                final float y = mPosY;
209                if (x > xmax) {
210                    mPosX = xmax;
211                } else if (x < -xmax) {
212                    mPosX = -xmax;
213                }
214                if (y > ymax) {
215                    mPosY = ymax;
216                } else if (y < -ymax) {
217                    mPosY = -ymax;
218                }
219            }
220        }
221
222        /*
223         * A particle system is just a collection of particles
224         */
225        class ParticleSystem {
226            static final int NUM_PARTICLES = 15;
227            private Particle mBalls[] = new Particle[NUM_PARTICLES];
228
229            ParticleSystem() {
230                /*
231                 * Initially our particles have no speed or acceleration
232                 */
233                for (int i = 0; i < mBalls.length; i++) {
234                    mBalls[i] = new Particle();
235                }
236            }
237
238            /*
239             * Update the position of each particle in the system using the
240             * Verlet integrator.
241             */
242            private void updatePositions(float sx, float sy, long timestamp) {
243                final long t = timestamp;
244                if (mLastT != 0) {
245                    final float dT = (float) (t - mLastT) * (1.0f / 1000000000.0f);
246                    if (mLastDeltaT != 0) {
247                        final float dTC = dT / mLastDeltaT;
248                        final int count = mBalls.length;
249                        for (int i = 0; i < count; i++) {
250                            Particle ball = mBalls[i];
251                            ball.computePhysics(sx, sy, dT, dTC);
252                        }
253                    }
254                    mLastDeltaT = dT;
255                }
256                mLastT = t;
257            }
258
259            /*
260             * Performs one iteration of the simulation. First updating the
261             * position of all the particles and resolving the constraints and
262             * collisions.
263             */
264            public void update(float sx, float sy, long now) {
265                // update the system's positions
266                updatePositions(sx, sy, now);
267
268                // We do no more than a limited number of iterations
269                final int NUM_MAX_ITERATIONS = 10;
270
271                /*
272                 * Resolve collisions, each particle is tested against every
273                 * other particle for collision. If a collision is detected the
274                 * particle is moved away using a virtual spring of infinite
275                 * stiffness.
276                 */
277                boolean more = true;
278                final int count = mBalls.length;
279                for (int k = 0; k < NUM_MAX_ITERATIONS && more; k++) {
280                    more = false;
281                    for (int i = 0; i < count; i++) {
282                        Particle curr = mBalls[i];
283                        for (int j = i + 1; j < count; j++) {
284                            Particle ball = mBalls[j];
285                            float dx = ball.mPosX - curr.mPosX;
286                            float dy = ball.mPosY - curr.mPosY;
287                            float dd = dx * dx + dy * dy;
288                            // Check for collisions
289                            if (dd <= sBallDiameter2) {
290                                /*
291                                 * add a little bit of entropy, after nothing is
292                                 * perfect in the universe.
293                                 */
294                                dx += ((float) Math.random() - 0.5f) * 0.0001f;
295                                dy += ((float) Math.random() - 0.5f) * 0.0001f;
296                                dd = dx * dx + dy * dy;
297                                // simulate the spring
298                                final float d = (float) Math.sqrt(dd);
299                                final float c = (0.5f * (sBallDiameter - d)) / d;
300                                curr.mPosX -= dx * c;
301                                curr.mPosY -= dy * c;
302                                ball.mPosX += dx * c;
303                                ball.mPosY += dy * c;
304                                more = true;
305                            }
306                        }
307                        /*
308                         * Finally make sure the particle doesn't intersects
309                         * with the walls.
310                         */
311                        curr.resolveCollisionWithBounds();
312                    }
313                }
314            }
315
316            public int getParticleCount() {
317                return mBalls.length;
318            }
319
320            public float getPosX(int i) {
321                return mBalls[i].mPosX;
322            }
323
324            public float getPosY(int i) {
325                return mBalls[i].mPosY;
326            }
327        }
328
329        public void startSimulation() {
330            /*
331             * It is not necessary to get accelerometer events at a very high
332             * rate, by using a slower rate (SENSOR_DELAY_UI), we get an
333             * automatic low-pass filter, which "extracts" the gravity component
334             * of the acceleration. As an added benefit, we use less power and
335             * CPU resources.
336             */
337            mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI);
338        }
339
340        public void stopSimulation() {
341            mSensorManager.unregisterListener(this);
342        }
343
344        public SimulationView(Context context) {
345            super(context);
346            mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
347
348            DisplayMetrics metrics = new DisplayMetrics();
349            getWindowManager().getDefaultDisplay().getMetrics(metrics);
350            mXDpi = metrics.xdpi;
351            mYDpi = metrics.ydpi;
352            mMetersToPixelsX = mXDpi / 0.0254f;
353            mMetersToPixelsY = mYDpi / 0.0254f;
354
355            // rescale the ball so it's about 0.5 cm on screen
356            Bitmap ball = BitmapFactory.decodeResource(getResources(), R.drawable.ball);
357            final int dstWidth = (int) (sBallDiameter * mMetersToPixelsX + 0.5f);
358            final int dstHeight = (int) (sBallDiameter * mMetersToPixelsY + 0.5f);
359            mBitmap = Bitmap.createScaledBitmap(ball, dstWidth, dstHeight, true);
360
361            Options opts = new Options();
362            opts.inDither = true;
363            opts.inPreferredConfig = Bitmap.Config.RGB_565;
364            mWood = BitmapFactory.decodeResource(getResources(), R.drawable.wood, opts);
365        }
366
367        @Override
368        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
369            // compute the origin of the screen relative to the origin of
370            // the bitmap
371            mXOrigin = (w - mBitmap.getWidth()) * 0.5f;
372            mYOrigin = (h - mBitmap.getHeight()) * 0.5f;
373            mHorizontalBound = ((w / mMetersToPixelsX - sBallDiameter) * 0.5f);
374            mVerticalBound = ((h / mMetersToPixelsY - sBallDiameter) * 0.5f);
375        }
376
377        @Override
378        public void onSensorChanged(SensorEvent event) {
379            if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER)
380                return;
381            /*
382             * record the accelerometer data, the event's timestamp as well as
383             * the current time. The latter is needed so we can calculate the
384             * "present" time during rendering. In this application, we need to
385             * take into account how the screen is rotated with respect to the
386             * sensors (which always return data in a coordinate space aligned
387             * to with the screen in its native orientation).
388             */
389
390            switch (mDisplay.getRotation()) {
391                case Surface.ROTATION_0:
392                    mSensorX = event.values[0];
393                    mSensorY = event.values[1];
394                    break;
395                case Surface.ROTATION_90:
396                    mSensorX = -event.values[1];
397                    mSensorY = event.values[0];
398                    break;
399                case Surface.ROTATION_180:
400                    mSensorX = -event.values[0];
401                    mSensorY = -event.values[1];
402                    break;
403                case Surface.ROTATION_270:
404                    mSensorX = event.values[1];
405                    mSensorY = -event.values[0];
406                    break;
407            }
408
409            mSensorTimeStamp = event.timestamp;
410            mCpuTimeStamp = System.nanoTime();
411        }
412
413        @Override
414        protected void onDraw(Canvas canvas) {
415
416            /*
417             * draw the background
418             */
419
420            canvas.drawBitmap(mWood, 0, 0, null);
421
422            /*
423             * compute the new position of our object, based on accelerometer
424             * data and present time.
425             */
426
427            final ParticleSystem particleSystem = mParticleSystem;
428            final long now = mSensorTimeStamp + (System.nanoTime() - mCpuTimeStamp);
429            final float sx = mSensorX;
430            final float sy = mSensorY;
431
432            particleSystem.update(sx, sy, now);
433
434            final float xc = mXOrigin;
435            final float yc = mYOrigin;
436            final float xs = mMetersToPixelsX;
437            final float ys = mMetersToPixelsY;
438            final Bitmap bitmap = mBitmap;
439            final int count = particleSystem.getParticleCount();
440            for (int i = 0; i < count; i++) {
441                /*
442                 * We transform the canvas so that the coordinate system matches
443                 * the sensors coordinate system with the origin in the center
444                 * of the screen and the unit is the meter.
445                 */
446
447                final float x = xc + particleSystem.getPosX(i) * xs;
448                final float y = yc - particleSystem.getPosY(i) * ys;
449                canvas.drawBitmap(bitmap, x, y, null);
450            }
451
452            // and make sure to redraw asap
453            invalidate();
454        }
455
456        @Override
457        public void onAccuracyChanged(Sensor sensor, int accuracy) {
458        }
459    }
460}
461