Именно по этой причине, приложения (в основном игры) бездумно портированные с 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>
<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) {
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);
}
}
}
}
@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);
}
. . .
}
...
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;
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();
}
}
@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 на устройство, убеждаемся, что помимо возможности управления точками подвеса, мы можем управлять положением нити, наклоняя устройство в ту или другую сторону.
Комментариев нет:
Отправить комментарий