Показаны сообщения с ярлыком Accelerometer. Показать все сообщения
Показаны сообщения с ярлыком Accelerometer. Показать все сообщения

суббота, 4 августа 2012 г.

С ног на голову

Пользовательский интерфейс, предоставляемый приложениями, разработанными для мобильных устройств (планшетов и телефонов) сильно отличается от привычного нам на платформе PC. Это закономерно, поскольку устройства ввода для PC, в стандартной комплектации это мышь (с двумя кнопками и колесиком) и клавиатура, а в случае планшета или телефона, основное устройство ввода - сенсорный экран.
Именно по этой причине, приложения (в основном игры) бездумно портированные с PC платформы, как правило, не успешны. Даже в том случае, если все действия можно выполнять только при помощи мыши, использование клавиатуры, как правило, позволяет оптимизировать их выполнение (хорошей иллюстрацией этому служит StarCraft, играть в который, с использованием только мыши, возможно, но довольно неудобно).
В свою очередь, сенсорный экран планшета имеет довольно мало общего с мышью. Так как у него нет кнопок мыши, портированное приложение не может различать клики левой и правой кнопками мыши (что может быть существенно для приложения), не говоря уже о ставших привычными манипуляциях с колесиком. Использовать Multitouch портированное приложение также не может, поскольку, в случае использования мыши, это понятие лишено всякого смысла. Можно долго говорить о том, как именно следует переносить приложения с PC на Android, но я хотел поговорить не об этом. 
Дело в том, что любое Android-устройство, в наиболее распространенной конфигурации, помимо сенсорного экрана, имеет устройства ввода, совершенно не представимые на PC-платформе, такие как GPS или акселерометр (действительно, было бы очень странно видеть их на стационарном компьютере). Эти устройства ввода нельзя назвать основным источником данных, но у них есть свои ниши применения, в рамках которых они незаменимы. 
В случае акселерометра, такой нишей является разработка разнообразных симуляторов, в которых возможность отслеживания положения устройства в пространстве, становится еще одним средством, позволяющим сделать управление более удобным и интуитивным (разумеется результат использования акселерометра при создании интуитивно-понятных интерфейсов не всегда бывает понятен и даже удобен, но это тема для другого разговора).
Я уже писал ранее о том как можно использовать показания акселерометра.Сегодня я хочу показать, как можно использовать акселерометр в приложении, моделирующем 2-мерную физику (разработанном в предыдущей статье). Начнем с манифеста:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.WhiteRabbit.YoYo"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="7" />
    <uses-feature android:name="android.hardware.sensor.accelerometer"
              android:required="true" />
   
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".YoYoActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Тег uses-feature показывает, что для корректной работы приложения, устройство должно быть оборудовано акселерометром. Далее, добавляем код работы с акселерометром в Activity:

package com.WhiteRabbit.YoYo;

import java.util.List;

import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.LinearLayout;

public class YoYoActivity extends Activity implements SensorEventListener {

    private MainView view;
    private Task     task = null;
   
    SensorManager mSensorManager = null;
    Sensor mSensor = null;
   
    public MainView getView() {
        return view;
    }
   
    public void createTask() {
        if (task == null) {
            task = new Task(this);
        }
    }
   
    @Override
    public void onAccuracyChanged(Sensor event, int value) {}

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (view == null) return;
        switch(event.sensor.getType()) {
            case Sensor.TYPE_ACCELEROMETER:
                view.setXYZ(-event.values[1], -event.values[0], event.values[2]);
                break;
        }
    }
   
    @Override
    public void onStart() {
        super.onStart();
        if (mSensor != null) {         
            mSensorManager.registerListener(this, mSensor, 
                SensorManager.SENSOR_DELAY_GAME);
        }
    }
   
    @Override
    protected void onPause() {
        if (task != null) {
            task.terminate();
        }
        super.onPause();
        if (mSensor != null) {
            mSensorManager.unregisterListener(this);
        }
    }
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       
        mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        if (mSensorManager != null) {
            List<Sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
            if(sensors.size() > 0) {
                for (Sensor sensor : sensors) {
                    switch(sensor.getType()) {
                        case Sensor.TYPE_ACCELEROMETER:
                            if(mSensor == null) mSensor = sensor;
                            break;
                        default:
                            break;
                    }
                }       
            }
        }
       
        LinearLayout.LayoutParams containerParams =
                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT, 0.0F);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);

        root.setBackgroundColor(Color.BLACK);
        root.setLayoutParams(containerParams);

        view = new MainView(this);
        root.addView(view);
        setContentView(root);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    }
}

Следует обратить внимание, что при вызове метода setXYZ мы меняем местами показания X и Y, полученные от акселерометра, а также меняем им знак. Делается это для того, чтобы привести систему координат, в которой работает акселерометр к системе координат экрана с интерфейсом, зафиксированным в SCREEN_ORIENTATION_PORTRAIT (фактически, мы размернули систему координат на 90 градусов). Теперь внесем необходимые дополнения в MainView:

package com.WhiteRabbit.YoYo;

. . .
public class MainView extends SurfaceView implements SurfaceHolder.Callback {
   
    ...   
    private float sum_x = 0;
    private float sum_y = 0;
    private int   g_cnt = 0;
   
    ...   
    public void setXYZ(float x, float y, float z) {
        if ((Math.abs(z) > Math.abs(x))&&(Math.abs(z) > Math.abs(y))) return;
        sum_x += x;
        sum_y += y;
        g_cnt++;
    }
   
    public Vec2 getGVect(float force) {
        if (g_cnt == 0) return null;
        float x = sum_x / g_cnt;
        float y = sum_y / g_cnt;
        float r = (float)Math.sqrt(x * x + y * y);
        sum_x = 0; sum_y = 0; g_cnt = 0;
        x = (x * force) / r;
        y = (y * force) / r;
        return new Vec2(x, y);
    }
          . . .
}
Большая часть кода остается неизменной.По сравнению с предыдущей версией, добавляются лишь методы setXYZ и getGVect. Здесь также следует обратить внимание на два нюанса, вызванные тем, что мы получаем показания акселерометра с некоторой случайной погрешностью, в результате которой полученные показания постоянно незначительно изменяются, даже в том случае, когда положение устройства остается неизменным. Особенно серьезно этот эффект проявляется в случае расположения устройства параллелно относительно поверхности Земли, когда величины полученных X и Y становятся соизмеримы со значением погрешности.
Чтобы сгладить эти нежелательные эффекты, мы усредняем полученные значения в интервалах между опросами getGVect. Кроме того, мы не обрабатываем показания в случае если абсолютные величины X и Y не превышают абсолютной величины Z (устройство расположено почти горизонтально). В мы getGVect строим результирующий вектор, этот код довольно тривиален. Осталось внести изменения в Task:

package com.WhiteRabbit.YoYo;
. . .
public class Task extends TimerTask {
   
    private final static int G_FORCE   = 10;
    ...

    private void configure() {
        MainView v = ctx.getView();
        float X = v.SZ_X / 2;
        float Y = v.SZ_Y - (v.SZ_Y / 5);
       
        // Создать мир
        Vec2 gravity = new Vec2(0.0f, G_FORCE);
        world = new World(gravity, true);
        BodyDef bd = new BodyDef();
        ground = world.createBody(bd);
        ...       

        // Запустить рассчет
        timer.scheduleAtFixedRate(this, 0, SECONDS_INTERVAL / FRAME_RATE);
    }

          . . .
    @Override
    public void run() {
        // Выполнить расчет итерации
        float timeStep = 2.0f / FRAME_RATE;
        world.step(timeStep, 10, 10);
        // Изменить направление силы гравитации
        Vec2 gravity = ctx.getView().getGVect(G_FORCE);
        if (gravity != null) {
            world.setGravity(gravity);
        }
        // Обновить view
        ctx.getView().update();
    }
}
Значение ускорения свободного падения выносим в костанту, для удобства. Перед выполнением очередного шага рассчета, опрашиваем усредененные показания от акселерометра и изменяем направление вектора действия силы гравитации.
Вот, собственно, и все. После экспорта apk на устройство, убеждаемся, что помимо возможности управления точками подвеса, мы можем управлять положением нити, наклоняя устройство в ту или другую сторону.

P.S. Как обычно, для ленивых, выкладываю весь проект целиком.

понедельник, 14 мая 2012 г.

Ориентируемся в пространстве

Сегодня мы научимся обрабатывать показания акселерометра. Поскольку за изменением трех чисел наблюдать как то не весело, сделаем это красиво.

Ориентируемся в пространстве


Поскольку мы решили делать красиво, для начала нам понадобиться View для наглядного отображения вертикали. Изобразим этакую матрешку-неваляшку из двух вложенных друг в друга кругляшков:

package com.WhiteRabbit.Sensors;

...
public class MainView extends View  {

    private int maxX = 10000;
    private int maxY = 10000;
    private int sz;
    private int x0;
    private int y0;
    private double alfa = Math.PI / 2;

    public MainView(Context context) {
        super(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if ((MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) ||
            (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED)) {
            setMeasuredDimension(maxX, maxY);
        } else {
            int szX = MeasureSpec.getSize(widthMeasureSpec);
            int szY = MeasureSpec.getSize(heightMeasureSpec);
            if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
                if (szX > maxX) {
                    szX = maxX;
                }
            }
            if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
                if (szY > maxY) {
                    szY = maxY;
                }
            }
            setMeasuredDimension(szX, szY);
            x0 = szX / 2;
            y0 = szY / 2;
            if (szX < szY) {
                sz = szX / 3;
            } else {
                sz = szY / 3;
            }
        }
    }

    @Override
    protected void onDraw(Canvas c) {
        c.drawColor(Color.BLACK);
        Paint p = new Paint();
        p.setColor(Color.WHITE);
        p.setAntiAlias(true);
        c.drawCircle(x0, y0, sz, p);
        p.setColor(Color.BLACK);
        double x1 = x0 - (sz /2) * Math.cos(alfa);
        double y1 = y0 - (sz /2) * Math.sin(alfa);
        c.drawCircle((float)x1, (float)y1, sz / 2, p);
    }   
}


В этом коде, в переменной sz сохраняется радиус большого кругляшка, в x0 и y0 его координаты, а в alfa угол (в радианах), в направлении которого будет изображен маленький кругяшок. Добавим созданное View в Activity:

package com.WhiteRabbit.Sensors;

...
public class SensorsActivity extends Activity {

    MainView view;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        LinearLayout.LayoutParams containerParams =
                new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT, 0.0F);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setBackgroundColor(Color.LTGRAY);
        root.setLayoutParams(containerParams);
        view = new MainView(this);
        root.addView(view);
        setContentView(root);
    }
}


и, запустив на выполнение, пронаблюдаем результат:



Далее, нам нужен какой-то инструмент, для изменения угла alfa при отображении маленького кружочка. От акселерометра мы будем получать три числовых значения, одно из которых (z) будем игнорировать. Судорожно вспоминаем тригонометрию и понимаем, что alfa = arctg(y/x), а также то, что его аргумент будет претерпевать особенности при x = 0. К счастью, он нас позаботились разработчики Java, определив в Math замечательную функцию atan2:

package com.WhiteRabbit.Sensors;

...
public class MainView extends View  {

    ...
    private double eps = 0.1;

    ...
    public void setXY(float x, float y) {
        if (Math.sqrt(x*x + y*y) < eps) {
            alfa = Math.PI / 2;
        } else {
            alfa = -Math.atan2(y, x);
        }
        invalidate();
    }
}


Константу eps определяем на случай получения очень уж маленьких значений x и y. После этого, проводим на эмуляторе небольшое тестирование, передавая в setXY различные значения для проверки корректности расчета alfa. Убедившись, что все считается правильно, переходим к самой интересной части, работе с акселерометром.
На самом деле, нам требуется всего лишь получить акселерометр из списка предоставляемых устройством сенсоров, написать обработчики его событий и добавить код подписки/отписки на события акселерометра. Попутно зафиксируем ориентацию экрана, чтобы они не вертелся и не мешал нам эстетически наслаждаться:

package com.WhiteRabbit.Sensors;

public class SensorsActivity extends Activity implements SensorEventListener {

    ...
    SensorManager mSensorManager = null;
    Sensor mSensor = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {

    ...
        mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        if (mSensorManager != null) {
            List<Sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
            if(sensors.size() > 0) {
                for (Sensor sensor : sensors) {
                    switch(sensor.getType()) {
                        case Sensor.TYPE_ACCELEROMETER:
                            if(mSensor == null) mSensor = sensor;
                            break;
                        default:
                            break;
                    }
                }       
            }
        }
    }

    @Override
    public void onAccuracyChanged(Sensor event, int value) {}

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (view == null) return;
        switch(event.sensor.getType()) {
            case Sensor.TYPE_ACCELEROMETER:
                view.setXY(event.values[0], event.values[1]);
                break;
        }
    }   
   
    @Override
    public void onStart() {
        super.onStart();
        if (mSensor != null) {
            mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_GAME);
        }
    }
   
    @Override
    public void onPause() {
        super.onPause();
        if (mSensor != null) {
            mSensorManager.unregisterListener(this);
        }
    }   
}


Осталось добавить в манифест uses-feature, чтобы никто не запустил наше приложение при отсутствии акселерометра:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.WhiteRabbit.Sensors"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="15" />
    <uses-feature android:name="android.hardware.sensor.accelerometer"
              android:required="true" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".SensorsActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


и можно экспортировать apk-ку.