понедельник, 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-ку.

четверг, 10 мая 2012 г.

Осваиваем Multitouch

Сегодня мы рассмотрим животрепещущую тему обработки событий MultiTouch-а. При грамотном подходе, он обеспечит нам не только набивший оскомину "зум двумя пальцами", но и заставит заиграть наши приложения новыми красками (ну или, во всяком случае, сделает их чуть более юзабельными).

Осваиваем Multitouch

Собственно говоря, никакой тайной магии надцатого уровня в мультитаче нет. Мы попрежнему будем отрабатывать события MotionEvent, например при помощи обработчика View.onTouchEvent. Просто есть несколько моментов, про которые следует помнить:

  1. Событие MotionEvent содержит описание нескольких точек касания, количество которых можно получить, используя метод getPointerCount(). Каждая точка касания получает уникальный идентификатор, который не изменяется при удалении точек из списка (после отработки события ACTION_POINTER_UP) и позволяет индивидуально отслеживать изменения по каждой точке касания.
  2. Все методы, возвращающие свойства точек касания (getX, getY, getPressure и getSize) принимают ИНДЕКС (не идентификатор!) точки касания. Методы возвращающие значения свойств события вцелом (getDownTime, getEventTime) никаких индексов не принимают и существуют в единственном экземпляре :)
  3.  Значение, возвращаемое методом getAction(), на самом деле составное и помимо собственно типа события содержит ИДЕНТИФИКАТОР точки касания. Выделить код события можно использовав битовую маску MotionEvent.ACTION_MASK, а значение идентификатора используя маску ACTION_POINTER_ID_MASK и сдвиг ACTION_POINTER_ID_SHIFT (которые зачем-то сделали depricated :( ).
  4. Пока getPointerCount() > 1, вместо кодов ACTION_DOWN и ACTION_UP (ACTION_CANCEL) мы получаем ACTION_POINTER_DOWN и ACTION_POINTER_UP, соответственно. ACTION_MOVE по прежнему приходит с тем-же кодом.
  5. Главная засада: В целях обеспечения обратной совместимости, при getPointerCount() = 1, action формируется не так как описано в п.3, а так как если бы это был SingleTouch :), то есть без идентификатора точки касания, что вносит некоторый сумбур в логику обработки.
Осмыслив все эти правила, приступаем к кодингу. Создадим Android-проект, обозвав его, например Multitouch (с воображением в части названий у нас совсем туго). В MotiontouchActivity, по привычке, пишем следующий код:

package com.WhiteRabbit.Multitouch;

...
public class MultitouchActivity extends Activity {
   
    @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 view = new MainView(this);
        root.addView(view);
        setContentView(root);
    }
}


Также, по привычке, создаем MainView, расширяющий функциональность View:

package com.WhiteRabbit.Multitouch;

...
public class MainView extends View {

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

    private int maxX = 10000;
    private int maxY = 10000;
    private int szX;
    private int szY;

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if ((MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) ||
            (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED)) {
            setMeasuredDimension(maxX, maxY);
        } else {
            szX = MeasureSpec.getSize(widthMeasureSpec);
            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);
        }
    }
}


Здесь мы объявляем, что, по возможности, хотим получить в свое распоряжение регион для рисования пикселов так 1000x1000, а когда вышестоящий Layout сообщает, что у него стока нет, соглашаемся с тем, что дают.
Далее, нам нужно как-то визуализировать Multitouch, для чего потребуется хранить набор точек, ассоциированных с идентификаторами точек касания. Создаем элементарный класс Point, для хранения требуемой нам информации:

package com.WhiteRabbit.Multitouch;

import android.graphics.Color;

public class Point {
   
    private float x;
    private float y;
    private int color = Color.WHITE;
   
    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }
   
    public void setColor(int color) {
        this.color = color;
    }
   
    public int getColor() {
        return color;
    }
   
    public void setXY(float x, float y) {
        this.x = x;
        this.y = y;
    }
   
    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }
}


и добавляем метод onDraw в наш View:

package com.WhiteRabbit.Multitouch;

...
public class MainView extends View {

    private Map<Integer, Point> points = new HashMap<Integer, Point>();

    ...
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.BLACK);
        Paint p = new Paint();
        p.setAntiAlias(true);
        for (Point pt: points.values()) {
            p.setColor(pt.getColor());
            canvas.drawLine(0, pt.getY(), szX, pt.getY(), p);
            canvas.drawLine(pt.getX(), 0, pt.getX(), szY, p);
            canvas.drawCircle(pt.getX(), pt.getY(), 20, p);
        }
    }
}


После чего, при запуске, видим экран радикально черного цвета.
Хорошо-бы как-то начать обрабатывать события TouchPad-а:

package com.WhiteRabbit.Multitouch;

...
public class MainView extends View {
   
    private Map<Integer, Point> points = new HashMap<Integer, Point>();
    private Set<Integer> mfd = new HashSet<Integer>();

    @Override
    ...
    public boolean onTouchEvent(MotionEvent event) {
        mfd.clear();
        for (Integer i: points.keySet()) {
            mfd.add(i);
        }
        int cnt = event.getPointerCount();
        for (int i = 0; i < cnt; i++) {
            Integer id = event.getPointerId(i);
            Point pt = points.get(id);
            if (pt == null) {
                pt = new Point(event.getX(i), event.getY(i));
                points.put(id, pt);
            } else {
                pt.setXY(event.getX(i), event.getY(i));
                mfd.remove(id);
            }
        }
        for (Integer i: mfd) {
            points.remove(i);
        }
        invalidate();
        return true;
    }
}


Этот, не претендующий на особую гениальность, код делает следующее:

  1. Очищает список точек подлежащих удалению, после чего, вносит в него все имеющиеся точки
  2. Для каждой точки касания, выполняет ее поиск по ID и, если находит, изменяет ее координаты, удаляя ее из списка точек, подлежащих удалению. В случае если точка не найдена, она создается и добавляется в массив известных нам точек
  3. Удаляет все точки, идентификаторы которых содержаться в списке точек, подлежащих удалению
  4. Инвалидирует View, инициируя его перерисовку
Запускаем этот код на выполнение и наблюдаем радостно бегающий по экрану кругляшочек :) Плохо только, что при завершении касания, кружочек не исчезает, а останавливается на месте как вкопанный :(






что, впрочем, не удивительно, поскольку мы ДАЖЕ НЕ ВЫЗЫВАЕМ getAction. Исправляем это досадное недоразумение, попутно добавляя в мир красок:

package com.WhiteRabbit.Multitouch;

...
public class MainView extends View {

    private Map<Integer, Point> points = new HashMap<Integer, Point>();
    private Set<Integer> mfd = new HashSet<Integer>();
    private Map<Integer, Integer> colors = new HashMap<Integer, Integer>();

    public MainView(Context context) {
        super(context);
        colors.put(0, Color.WHITE);
        colors.put(1, Color.GREEN);
        colors.put(2, Color.BLUE);
        colors.put(3, Color.RED);
        colors.put(4, Color.YELLOW);
    }

    ...
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mfd.clear();
        for (Integer i: points.keySet()) {
            mfd.add(i);
        }
        int cnt = event.getPointerCount();
        for (int i = 0; i < cnt; i++) {
            Integer id = event.getPointerId(i);
            Point pt = points.get(id);
            if (pt == null) {
                pt = new Point(event.getX(i), event.getY(i));
                Integer color = colors.get(id % colors.size());
                if (color != null) {
                    pt.setColor(color);
                }
                points.put(id, pt);
            } else {
                pt.setXY(event.getX(i), event.getY(i));
                mfd.remove(id);
            }
        }
        if (cnt == 1) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mfd.add(event.getPointerId(0));
                    break;
            }
        }
        for (Integer i: mfd) {
            points.remove(i);
        }
        invalidate();
        return true;
    }
}



Запускаем и убеждаемся, что кружочек появляется и исчезает там где (и когда) это нужно. Разумеется, пронаблюдать все красочное многоцветие на эмуляторе нам не удастся. Кружочек будет одинок и бел, поскольку вряд-ли кому нибудь удастся сэмулировать мультитач мышью :) (да и тормозит эмулятор нещадно).
Посему экспортируем apk-ку, устанавливаем на устройство и тихо наслаждаемся ...

пятница, 4 мая 2012 г.

Анализируем это

Сегодня я предлагаю отвлечься от платформы Android и заняться чем нибудь полезным ... для всех Java (и не только) разработчиков.

Анализируем это


Известно, что языки выполняющие компиляцию кода значительно повышают производительность труда программиста, просто за счет того, что обнаруживают часть ошибок еще на этапе сборки. 
Разумеется, на этот счет имеется особое мнение у апологетов Perl-а, Python-а и даже Java, которое, в целом сводится к тому, что ошибки просто не надо делать, а код необходимо равномерно покрывать UnitTest-ами :)

Ничего не имея против UnitTest-ов, я все-же считаю, что в борьбе с ошибками все средства хороши и никакая помощь (в том числе и оказываемая компиляторами) не будет лишней. Несколько дальше чем компиляторы идут всевозможные анализаторы кода. 
Они помогают не просто найти ошибки, препятствующие сборке приложения, а позволяют обнаружить фрагменты кода небезупречные с точки зрения принятого Code Style или потенциально способные привести к ошибкам. Особенно полезными эти инструменты становятся при групповой разработке.

Инструментов, выполняющих подобный анализ, довольно много, и, на мой взгляд, грех ими не пользоваться. В этой статье я хочу остановиться на Yasca. Это свободно распространяемое ПО является не анализатором кода, а интегрирующей платформой, поддерживающей интеграцию с множеством различных кодечекеров таких как PMD, JLint или RATS.
Оно будет полезно не только Java-разработчику (хотя наиболее эффективный анализ кода возможен именно для Java), но и разработчикам использующим C, С++, PHP или JavaScript.

Попробуем скачать и запустить Yasca "из коробки" (благо код для анализа у нас где-то завалялся). Идем на страницу закачки и скачиваем собственно yasca, а также какой нибудь из прилагающихся анализаторов кода, например PMD или все сразу :)

Скачав архивы, создаем локальный каталог (например c:/yasca) и просто распаковываем в него все архивы по очереди. Сразу же после этого, инструмент готов к работе :) 
Набираем в командной строке (или сохраняем в cmd-файл, чтобы не набирать одно и то-же сто раз) команду:

yasca.exe -o ./Report C:\work\Games\tags\src\puzzle

и ждем пару минут (здесь флаг -o указывает имя файла в котором следует формировать отчет, а далее следует каталог анализируемого проекта), открываем сформировавшийся Report.html и тихо наслаждаемся :)

В принципе, все довольно неплохо. Единственное, что напрягает, это то, что ссылки на код загружают непосредственно java-файлы, искомую строку в которых приходится искать самому по номеру :(

Давайте это исправим :) Пользуясь тем, что Yasca, фактически, представляет собой специализированный PHP-интерпретатор (не самый удачный выбор языка, но сойдет), заходим в папку plugins и пишем там следующий скрипт (обозвав его, например, Mirror.php):

<?php

/**
 * @extends Plugin
 * @package Yasca
 */
class Plugin_Mirror extends Plugin {
    public $valid_file_types = array("java", "c", "cpp", "h", "cs", "sql");
        function rmdir_recurse($path) {
            $path= rtrim($path, '/').'/';
            $handle = opendir($path);
            for (;false !== ($file = readdir($handle));)
                if($file != "." and $file != ".." ) {
                    $fullpath= $path.$file;
                    if( is_dir($fullpath) ) {
                        $this->rmdir_recurse($fullpath);
                        rmdir($fullpath);
                    }
                    else
                      unlink($fullpath);
                }
            closedir($handle);
        }
    function execute() {
        $yasca =& Yasca::getInstance();
                static $once = true;
                if ($once) {
                    $this->rmdir_recurse('./Mirror');
                }
                $once = false;
                if (!check_in_filetype($this->filename, $this->valid_file_types)) {
                    return;
                }
                $filename = preg_replace('/\w:/', './Mirror', $this->filename) . ".html";
                $dir_name = preg_replace('/[\\\\\\/][^\\\\\\/]+$/', '', $filename);
                if (!file_exists($dir_name)) {
                    if (!mkdir($dir_name, 0777, true)) return;
                }
                if (file_exists($filename)) {
                    unlink($filename);
                }
                if (!$handle = fopen($filename, 'w+', true) ) return;
                fwrite($handle,"<html><meta http-equiv=\"Content-Type\" content=\"text/html;charset=windows-1251\" /><head></head><body><pre>\n");
                $line = 1;
                foreach ($this->file_contents as $file_line) {
                        $str = $line;
                        while (strlen($str)<5) {
                           $str = " " . $str;
                        }
            fwrite($handle,"<a name=$line></a>$str: $file_line<br>\n");
                        $line++;
                }
                fwrite($handle,"</pre></body></html>");
                fclose($handle);
    }
}
?>


Здесь нет какой-то тайной магии. Мы просто копируем все анализируемые файлы, имеющие заданное расширение в некое подобие html-файла, расставив закладки и, для удобства, пронумеровав строки, в каталог Mirror (который заблаговременно создаем руками).

Теперь, после очередного запуска yasca мы получим копии всех проанализированных файлов в каталоге Mirror в препарированном в html виде.
Прикольно и бесполезно :) Для того чтобы этот скрипт стал полезным, нам необходимо внести изменения в формируемый файл отчета. И это тоже возможно :)

Идем в каталог lib и находим там файл HTMLGroupReport.php используемый для создания отчета, по умолчанию. В этом файле, находим фрагмент:

fwrite($handle,    
       "<a style=\"margin-right: 12px;\" source_code_link=\"true\" href=\"file://$filename\" target=\"_blank\" title=\"$filename\">$filename_base$line_number_field</a>" .
       "</td>");

он там такой один. И заменяем его на:

$chg_file_name = getcwd();
$chg_file_name = preg_replace('/\\\\/', '/', $chg_file_name);
$chg_file_name = preg_replace('/^\w:/', $chg_file_name . '/Mirror', $filename);
       
if (preg_match('/\.java$|\.c$|\.cpp$|\.h$|\.cs$|\.sql$/i', $chg_file_name)) {
    fwrite($handle,       
          "<a style=\"margin-right: 12px;\" source_code_link=\"true\" href=\"file://$chg_file_name.html#$line_number\" target=\"code\" title=\"$filename\">$filename_base$line_number_field</a>" .
          "</td>");
} else {
    fwrite($handle,    
          "<a style=\"margin-right: 12px;\" source_code_link=\"true\" href=\"file://$filename\" target=\"_blank\" title=\"$filename\">$filename_base$line_number_field</a>" .
          "</td>");
}


Код возможно не безупречен, зато вполне понятен :) Если файл, на который должна указывать ссылка, имеет нужное нам расширение, мы формируем ссылку на закладку в html-копии файла в каталоге Mirror. В противном случае, оставляем как было.

Запускаем анализ еще раз и убеждаемся, что навигация работает.

В принципе, в плане расширения функционала yasca мы мало чем ограничены. В частности, никто не помешает нам дописать плагин, автоматизирующий процесс сборки исходников при помощи Ant или Maven получив, тем самым, интегрированную среду сборки и анализа кода (вывод ant или maven будет полезно распарсить, добавляя сообщения об ошибках компиляции в общий отчет). Туда-же можно добавить и UnitTest-ы.

Полезный инструмент, я считаю :)

P.S. Кстати, вместо каталога с исходниками, можно анализировать Jar-ник :)
P.P.S. Правда наш Mirror.php, в этом случае, работать не будет, но это уже совсем другая история ...