Tillbaka till lektionslistan

Mobila applikationer med Android: Lektion 6

Idag: Accelerometer-API:et

Klicka på startknappen i den lilla mediaspelaren ovan för att lyssna på lektionen. (Man kan behöva vänta en stund på att ljudfilen laddas ner.) Om mediaspelaren inte syns, eller om det inte fungerar av något annat skäl, kan man klicka här för att ladda ner mp3-filen (ca 19 minuter, ca 9 megabyte). Beroende på hur webbläsaren är konfigurerad kan det kräva ett separat mp3-spelarprogram av något slag.

Bild 1: Simple Accelerometer Test

HTC Desire

Bild 2: Programmet i emulatorn

Hur det ska se ut (i emulatorn)

Bild 3: Skicka nya accelerationsvärden till emulatorn

Telnet

Ett tillägg våren 2013 (inte med på ljudspåret):
I en del versioner av Android och/eller utvecklingsmiljön fungerar det inte att styra accelerometern i emulatorn med kommandon via telnet. Det fungerade när jag körde en emulator med Android 2.2, men inte när jag körde en emulator med Android 2.3.3 eller 4.1. Man kan ersätta emulatorns sensor-API med ett annat (som heter SensorSimulator), men det är nog inte värt besväret. Om man har en riktigt telefon eller surfplatta bör man prova med den.

Ett annat tillägg våren 2012 (inte med på ljudspåret):
På en del modernare versioner av Windows, till exempel Windows 7, finns programmet telnet inte med som default, men det är enkelt att installera:
http://technet.microsoft.com/en-us/library/cc771275%28v=ws.10%29.aspx

Bild 4: Accelerationen syns

Hur det ska se ut (i emulatorn)

Bild 5: Sensor-API:et

Sensor-API:et

"Bild" 6: Koden för (den nya) BounceView

BounceView.java

    1   package se.nekotronic.simpleaccelerometer;
    2   
    3   import java.util.Timer;
    4   import java.util.TimerTask;
    5   
    6   import android.content.Context;
    7   import android.graphics.Canvas;
    8   import android.graphics.Color;
    9   import android.graphics.Paint;
   10   import android.hardware.Sensor;
   11   import android.hardware.SensorEvent;
   12   import android.hardware.SensorEventListener;
   13   import android.hardware.SensorManager;
   14   import android.util.AttributeSet;
   15   import android.util.Log;
   16   import android.view.Display;
   17   import android.view.Surface;
   18   import android.view.View;
   19   import android.view.WindowManager;
   20   
   21   public class BounceView extends View {
   22       // Ball position, measured in SCREENS. Starts in the middle.
   23       private float ball_x = 0.5f;
   24       private float ball_y = 0.5f;
   25       // Ball movement, measured in SCREENS PER SECOND
   26       private float ball_dx = 0.1f;
   27       private float ball_dy = 0.05f;
   28       // Measured in pixels
   29       private int ball_radius = 10;
   30           
   31       private int width_pixels;
   32       private int height_pixels;
   33           
   34       private Timer the_ticker;
   35       private SensorManager sensor_manager;
   36       private Sensor accelerometer;
   37       private SensorEventListener accelerometer_listener;
   38       private WindowManager window_manager;
   39       private Display display;
   40       
   41       private float acceleration_x = 0;
   42       private float acceleration_y = 0;
   43       private float acceleration_z = 0;
   44   
   45       private void init(Context context) {
   46           // We need the SensorManager to access the accelerometer
   47           sensor_manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
   48           accelerometer = sensor_manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
   49           // We need the WindowManager to get the Display, and the Display to find the rotation
   50           window_manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
   51           display = window_manager.getDefaultDisplay();
   52           accelerometer_listener = new SensorEventListener() {
   53               @Override
   54               public void onAccuracyChanged(Sensor sensor, int accuracy) {
   55                   // Who cares? Do nothing.               
   56               }
   57   
   58               @Override
   59               public void onSensorChanged(SensorEvent event) {
   60                   if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
   61                       // acceleration_x = event.values[0];
   62                       // acceleration_y = event.values[1];
   63                       acceleration_z = event.values[2];
   64                       
   65                       switch (display.getOrientation()) {
   66   
   67                           case Surface.ROTATION_0:
   68                               acceleration_x = event.values[0];
   69                               acceleration_y = event.values[1];
   70                               break;
   71                           case Surface.ROTATION_90:
   72                               acceleration_x = -event.values[1];
   73                               acceleration_y = event.values[0];
   74                               break;
   75                           case Surface.ROTATION_180:
   76                               acceleration_x = -event.values[0];
   77                               acceleration_y = -event.values[1];
   78                               break;
   79                           case Surface.ROTATION_270:
   80                               acceleration_x = event.values[1];
   81                               acceleration_y = -event.values[0];
   82                               break;
   83                       }
   84                   }
   85               }
   86           };
   87       }
   88       
   89       public BounceView(Context context) {
   90           super(context);
   91           init(context);
   92       }
   93           
   94       public BounceView(Context context, AttributeSet attrs) {
   95           super(context, attrs);
   96           init(context);
   97       }
   98   
   99       @Override
  100           protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  101           super.onSizeChanged(w, h, oldw, oldh);
  102           width_pixels = w;
  103           height_pixels = h;
  104       }
  105   
  106       public static final double roundDouble(double d, int places) {
  107           return Math.round(d * Math.pow(10, (double)places)) / Math.pow(10, (double)places);
  108       }
  109           
  110       private int frames = 0;
  111           
  112       @Override
  113           protected void onDraw(Canvas canvas) {
  114           super.onDraw(canvas);
  115           Paint p = new Paint();
  116           int w = width_pixels > 0 ? width_pixels : canvas.getHeight();
  117           int h = height_pixels > 0 ? height_pixels : canvas.getWidth();
  118           
  119           // The ball is green when in the crosshairs
  120           if (Math.sqrt(Math.pow(ball_x * w - w/2.0f, 2) + Math.pow(ball_y * h - h/2.0f, 2)) < ball_radius)
  121               p.setColor(Color.GREEN);
  122           else
  123               p.setColor(Color.BLUE);
  124           canvas.drawCircle(ball_x * w, ball_y * h, ball_radius, p);
  125           
  126           // Show acceleration as a white ball
  127           float ax = 0.5f - 0.04f * acceleration_x;
  128           float ay = 0.5f + 0.04f * acceleration_y;
  129           p.setColor(Color.WHITE);
  130           canvas.drawCircle(ax * w, ay * h, ball_radius, p);
  131           canvas.drawLine(w * 0.5f, h * 0.5f, w * ax, h * ay, p);
  132           
  133           p.setColor(Color.RED);
  134           canvas.drawText("Ball: x = " + String.format("%.2f", ball_x) +
  135                           ", y = " + String.format("%.2f", ball_y) +
  136                           ", dx = " + String.format("%.2f", ball_dx) +
  137                           ", dy = " + String.format("%.2f", ball_dy),
  138                           20.0f, 20.0f, p);
  139           canvas.drawText("w = " + w + " (" + width_pixels + ")" +
  140                           ", h = " + h + " (" + height_pixels + ")",
  141                           20.0f, 40.0f, p);
  142           canvas.drawText("Acc: X = " + String.format("%.2f", acceleration_x) +
  143                           ", Y = " + String.format("%.2f", acceleration_y) +
  144                           ", Z = " + String.format("%.2f", acceleration_z),
  145                           20.0f, 60.0f, p);
  146           ++frames;
  147           canvas.drawText("frames = " + frames,
  148                           20.0f, 80.0f, p);
  149           
  150           // A crosshair at the middle of the screen
  151           float crosswidth = Math.min(w, h) * 0.1f;
  152           canvas.drawLine(w * 0.5f - crosswidth, h * 0.5f, w * 0.5f + crosswidth, h * 0.5f, p);
  153           canvas.drawLine(w * 0.5f, h * 0.5f - crosswidth, w * 0.5f, h * 0.5f + crosswidth, p);
  154           
  155           canvas.drawText("basen.oru.se/android",
  156                           20.0f, h - 5.0f, p);
  157       } // onDraw
  158   
  159       // Called when the app is visible(possibly: again). We should start moving.
  160       public void resume() {
  161           the_ticker = new Timer();
  162           TimerTask task = new TimerTask() {
  163                   @Override
  164                       public void run() {
  165                       update_simulation();
  166                       // We get CalledFromWrongThreadException if we just just call invalidate().
  167                       // It must be done in the right thread!
  168                       Runnable r = new Runnable() {
  169                               @Override
  170                                   public void run() {
  171                                   invalidate();
  172                               }
  173                           };
  174                       getHandler().post(r);
  175                   }
  176               };
  177           // Give it a full second to set things up, before we start ticking
  178           // (It crashed with java.lang.NullPointerException when starting after 30 ms, but worked with 40.)
  179           the_ticker.schedule(task, 1000, 10);
  180           // Start listening to the accelerometer
  181           sensor_manager.registerListener(accelerometer_listener, accelerometer, SensorManager.SENSOR_DELAY_UI);
  182       } // resume
  183   
  184       // Called when the app has been hidden. We should stop moving.
  185       public void pause() {
  186           nanos_when_paused = java.lang.System.nanoTime(); 
  187           the_ticker.cancel();
  188           the_ticker = null;
  189           sensor_manager.unregisterListener(accelerometer_listener);
  190       }
  191           
  192       // -1 is not guaranteed to never happen, but we ignore that 
  193       private long previous_nanos = -1;
  194       private long nanos_when_paused = -1;
  195   
  196       // Calculate the ball's new position and speed
  197       private void update_simulation() {
  198           long now_nanos = java.lang.System.nanoTime();
  199           if (previous_nanos == -1 || now_nanos < previous_nanos) {
  200               // First time, or overflow, so don't update the game
  201               previous_nanos = now_nanos; 
  202               return;
  203           }
  204   
  205           long nanos = now_nanos - previous_nanos; 
  206           if (nanos_when_paused != -1) {
  207               // We have been paused!
  208               nanos = nanos_when_paused - previous_nanos;
  209               nanos_when_paused = -1;
  210           }
  211   
  212           previous_nanos = now_nanos;
  213           double seconds = nanos / 1e9;
  214   
  215           ball_dx -= acceleration_x / 4000;
  216           ball_dy += acceleration_y / 4000;
  217   
  218           ball_x += ball_dx * seconds;
  219           ball_y += ball_dy * seconds;
  220   
  221           // Yes, this ignores that the ball has a radius.
  222           
  223           if (ball_x < 0) {
  224               Log.d("Bounce", "Bounce on left wall");
  225               ball_x = -ball_x;
  226               ball_dx = -ball_dx;
  227           }
  228           if (ball_x > 1) {
  229               Log.d("Bounce", "Bounce on right wall");
  230               ball_x = 2 - ball_x;
  231               ball_dx = -ball_dx;
  232           }
  233           if (ball_y < 0) {
  234               Log.d("Bounce", "Bounce on ceiling");
  235               ball_y = -ball_y;
  236               ball_dy = -ball_dy;
  237           }
  238           if (ball_y > 1) {
  239               Log.d("Bounce", "Bounce on floor");
  240               ball_y = 2 - ball_y;
  241               ball_dy = -ball_dy;
  242           }        
  243       } // update_simulation      
  244   } // class BounceView

Ett tillägg hösten 2014 (inte med på ljudspåret):
Numera bör man hellre använda metoden getRotation än metoden getOrientation. Se API-dokumentationen.

Bild 7: Vrid så startar den om - dåligt!

På en riktigt telefon med accelerometer: Vrid den.
I emulatorn: CTRL-F11.

Hur det inte ska se ut (i emulatorn)

Ett tillägg våren 2014 (inte med på ljudspåret):
I en del nyare versioner av Android verkar rotering av emulatorn med CTRL-F11 inte fungera. Riktiga device fungerar, men inte i emulatorn.

"Bild" 8: Stäng av skärmrotationen

Ändra i AndroidManifest.xml. Funkar i telefonen (HTC Desire), men av någon anledning inte i emulatorn.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="se.nekotronic.simpleaccelerometer"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="7" />

    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".SimpleAccelerometerTestActivity"
                  android:label="@string/app_name"
                  android:screenOrientation="nosensor">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

Övningar

Tillbaka till lektionslistan


Thomas Padron-McCarthy (thomas.padron-mccarthy@oru.se), 24 september 2014