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 
17 package com.example.android.accelerometerplay;
18 
19 import android.app.Activity;
20 import android.content.Context;
21 import android.graphics.Bitmap;
22 import android.graphics.BitmapFactory;
23 import android.graphics.Canvas;
24 import android.graphics.BitmapFactory.Options;
25 import android.hardware.Sensor;
26 import android.hardware.SensorEvent;
27 import android.hardware.SensorEventListener;
28 import android.hardware.SensorManager;
29 import android.os.Bundle;
30 import android.os.PowerManager;
31 import android.os.PowerManager.WakeLock;
32 import android.util.DisplayMetrics;
33 import android.view.Display;
34 import android.view.Surface;
35 import android.view.View;
36 import 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 
50 public 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
onCreate(Bundle savedInstanceState)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
onResume()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
onPause()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 
Particle()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 
computePhysics(float sx, float sy, float dT, float dTC)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              */
resolveCollisionWithBounds()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 
ParticleSystem()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              */
updatePositions(float sx, float sy, long timestamp)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              */
update(float sx, float sy, long now)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 
getParticleCount()316             public int getParticleCount() {
317                 return mBalls.length;
318             }
319 
getPosX(int i)320             public float getPosX(int i) {
321                 return mBalls[i].mPosX;
322             }
323 
getPosY(int i)324             public float getPosY(int i) {
325                 return mBalls[i].mPosY;
326             }
327         }
328 
startSimulation()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 
stopSimulation()340         public void stopSimulation() {
341             mSensorManager.unregisterListener(this);
342         }
343 
SimulationView(Context context)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
onSizeChanged(int w, int h, int oldw, int oldh)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
onSensorChanged(SensorEvent event)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
onDraw(Canvas canvas)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
onAccuracyChanged(Sensor sensor, int accuracy)457         public void onAccuracyChanged(Sensor sensor, int accuracy) {
458         }
459     }
460 }
461