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