GrabDuck

Стабилизация экрана в Android

:

image

Пробовали ли Вы читать книгу или статью вроде этой в автобусе или идя по улице? Могу поспорить пробовали! В этом случае Вы должны были заметить что чтение текста таким образом является не лучшей идеей из-за постоянной тряски. Похоже что тряска экрана является достаточно серьезной проблемой и устранение ее может дать очень хорошее улучшение UX. Моя идея состоит в том, чтоб использовать датчики ускорения для компенсации тряски так-же как зеркальные камеры стабилизируют сенсор или линзы. Технически это возможно так что почему бы не попробовать сделать это самому!

Существующие решения


Для начала давайте посмотрим на существующие решения. В Сети есть несколько интересных статей на такую-же тематику.
  1. NoShake: Content Stabilization for Shaking Screens of Mobile Devices от Lin Zhong, Ahmad Rahmati и Clayton Shepard об стабилизации экрана для iPhone (3) опубликованная в 2009 г. Статья подытоживает что стабилизация экрана работает и дает заметные результаты, но алгоритм потребляет “в среднем 30% мощности у 620 МГц ARM процессора”. Это делает эту реализацию непрактичной для реального применения. И хотя современные айфоны могут легко справится с данной задачей авторы не предоставили ни исходников ни собранного приложения чтоб можно было попробовать это в деле.
  2. Walking with your Smartphone: Stabilizing Screen Content от Kevin Jeisy. Эта статься была опубликована в 2014 и имеет хорошее математическое обоснование. Статья подытоживает что «используя скрытую марковскую модель мы получили хорошую стабилизацию в теории». К сожалению не предоставлено ни исходные кодов, ни собранного приложения, так что посмотреть не получится.
  3. Shake-Free Screen. Исследуется тот же самый вопрос, но нету готовых результатов чтоб попробовать.

Эти статьи дают хорошее объяснение темы нашей статьи но к сожалению не дают ни исходных кодов, ни скомпилированных приложений чтоб посмотреть на это в живую. Давайте попробуем изобрести колесо заново и реализуем стабилизацию экрана по своему.

Теория


Датчик ускорения может быть использован для определения перемещения устройства. Но судя по названию этот датчик предназначен все таки для определения ускорения. Чтоб ответить на вопрос «как определить перемещение имея ускорение», давайте посмотрим на устройства с датчиками:
image
Как видно там есть есть три оси, соответственно датчик дает три значения на выходе. Технически датчик состоит из трех датчиков расположенных по разным осям, но давайте воспринимать его как единое целое.

Три значения на выходе обозначают ускорение вдоль соответствующей оси:

image

Ускорение меряется в “м/с2”. Как можно видеть там есть некоторое ускорение вдоль оси Y. На самом деле это ускорение свободного падения и любой поворот устройства изменит все три значения:

image

Вы можете представить себе его как шар привязанный к устройству веревкой. Это достаточно хорошее объяснение так как если замените шар стрелкой, то получите вектор ускорения.

Хорошо, но что насчет определения перемещения?


Я не могу показать какой-то наглядный пример, но если Вы немного переместите устройство, то вектор изменится: на самом деле он будет состоять из двух векторов: 1) вектор земного притяжения как и раньше; 2) вектор ускорения устройства из-за перемещения вдоль соответствующих осей. Самое интересное для нас это «чистый» вектор перемещения. Его достаточно просто просто получить путем вычитания вектора земного притяжения из результирующего вектора, но как определить истинный вектор земного притяжения? Эта задача может быть решена разными путями, но к счастью Андроид имеет специальный датчик линейного ускорения который делает как раз то, что нам нужно. В нормальных условиях выходные значения у датчика 0, и только перемещая устройство можно получить не нулевые значения. Здесь его исходный код если интересно. Мы на один шаг ближе к определению перемещения устройства. Давайте начнем программировать что нибудь.

Реализация


Чтоб найти как высчитать перемещение устройства давайте разработаем одно простое приложение с одной активити. Это приложение будет мониторить изменение ускорения и двигать специальный вью элемент соответствующим образом. Также оно будет показывать «сырые» значения ускорения на графике:

image

Я покажу только ключевые примеры кода. Полностью весь код есть в GIT репозитории. Ключевые вещи следующие:

1. Специальный элемент который мы будем двигать. Это синий блок с текстом внутри контейнера:

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/graph1"
android:background="@drawable/dots_repeat_bg"
android:clipChildren="false">

<LinearLayout
    android:id="@+id/layout_sensor"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="20dp"
    android:orientation="vertical"
    android:background="#5050FF">
        <ImageView
        android:id="@+id/img_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/txt_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/img_test"
        android:textSize="15sp"
        android:text="@string/test"/>
</LinearLayout>
</FrameLayout>

Для перемещения layout_sensor мы будем использовать методы View.setTranslationX и View.setTranslationY.

Также подпишемся на событие нажатия на какой-либо элемент для сброса внутренних значений в 0 потому что на первых порах они могут быть очень непослушными:

private void reset()
{
    position[0] = position[1] = position[2] = 0;
    velocity[0] = velocity[1] = velocity[2] = 0;
    timestamp = 0;

    layoutSensor.setTranslationX(0);
    layoutSensor.setTranslationY(0);
}

2. Подпишемся на события датчика ускорения:

sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
sensorManager.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_FASTEST);

3. И самое главное: слушатель изменений. Его базовая реализация:

private final float[] velocity = new float[3];
private final float[] position = new float[3];
private long timestamp = 0;
private final SensorEventListener sensorEventListener = new SensorEventListener()
{
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {}

    @Override
    public void onSensorChanged(SensorEvent event)
    {
   	 if (timestamp != 0)
   	 {
   		 float dt = (event.timestamp - timestamp) * Constants.NS2S;

   		 for(int index = 0; index < 3; ++index)
   		 {
   			 velocity[index] += event.values[index] * dt;
   			 position[index] += velocity[index] * dt * 10000;
   		 }
   	 }
   	 else
   	 {
   		 velocity[0] = velocity[1] = velocity[2] = 0f;
   		 position[0] = position[1] = position[2] = 0f;
   	 }
    }
};

Давайте разберемся что здесь происходит. Метод onSensorChanged вызывается каждый раз когда значение ускорения изменяется (прим. переводчика: ну на самом деле он вызывается по таймеру не зависимо от того какие значения ускорения). Первым делом вы проверяем инициализирована ли переменная timestamp. В этом случае мы просто инициализируем основные переменные. В случае если метод вызван повторно, мы производим вычисления использую следующую формулу:
deltaT = time() - lastTime;
velocity += acceleration * deltaT;
position += velocity * deltaT;
lastTime = time();

Вы должны были заметить интересную константу 10000. Воспринимайте ее как некое магическое число.

И результат:

Как видно текущая реализация имеет две проблемы:

  • Дрифтинг и уползание значений
  • Контрольный элемент не возвращается в 0

На самом деле решение для обоих проблем есть общее — нужно ввести в формулу торможение. Измененная формула выглядит так:
deltaT = time() - lastTime;
velocity += acceleration * deltaT - VEL_FRICTION * velocity;
position += velocity * deltaT - POS_FRICTION * position;
lastTime = time();

Хорошо. Текущая реализация выглядит неплохо. Я бы добавил некоторые косметические улучшения типа низкочастотного фильтра для сглаживания, отрезание недопустимых значений и настройки программы.

Готовое приложение находится в репозитории в ветке “standalone_app”.

AOSP


Мы разработали базовый алгоритм стабилизации и сделали демонстрационное приложение которое показывает, что стабилизация экрана возможна. Теперь мы можем применить нашу работу к устройству в целом. Это непростая задача, но тем интереснее будет ее решать.

Эта задача требует некоторого опыта в сборке AOSP. Google предоставляет всю необходимую документацию. В общем нужно скачать исходные коды Андроид для выбранного Nexus устройства. Собрать прошивку для Nexus и прошить ее. Не забывайте включить все необходимые драйвера перед сборкой.

Как только получится собрать стоковую прошивку, можно приступать к разработке и интеграции стабилизации экрана.

План реализации следующий:


  1. Найти способ смещения экрана в устройстве
  2. Разработать API во внутренностях AOSP чтоб дать возможность задавать смещение в стандартном Андроид приложении
  3. Разработать службу в демо приложении которая будет обрабатывать данные с датчика ускорения и задавать смещение используя API выше. Служба будет запускаться автоматически при включении устройства так что стабилизация будет работать сразу после включения

Сейчас я просто расскажу как я решил эти задачи.


1. Первый файл для исследования DisplayDevice.cpp который контролирует параметры экрана. Метод на который нужно смотреть void DisplayDevice::setProjection(int orientation, const Rect& newViewport, const Rect& newFrame). Самое интересное находится в строке 483:

image

где финальная матрица преобразований образуется из других компонентов. Все эти переменные являются экземплярами класса Transform. Этот класс предназначен для обработки преобразований и имеет несколько перегруженных операторов (например *). Чтоб добавить сдвиг добавим новый элемент:

image

Если Вы скомпилируете и прошьете Ваше устройство, экран там будет смещен на translateX пикселей по горизонтали и translateY пикселей по вертикали. В конечном итоге нам нужно добавить новый метод void setTranslate(int x, int y); который будет отвечать за матрицу сдвига.

2. Второй интересный файл SurfaceFlinger.cpp. Этот файл есть ключевым в создании API для доступа к параметрам экрана. Просто добавим новый метод:

image

который будет вызывать метод setTranslate для всех дисплеев. Другая часть выглядит немного странной, но я объясню это позже. Нам нужно модифицировать метод status_t SurfaceFlinger::onTransact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) добавив новую секцию в конструкцию switch:

image

Этот код является точкой входа в наше улучшение.

3. Служба обработки данных достаточно простая: она использует алгоритм разработанный ранее для получения значений смещения. Дальше эти значения через IPC передаются в SurfaceFlinger:

image

ServiceManager не распознается Android Studio потому что он недоступен для не системных приложений. Системные приложения должны собираться вместе с AOSP с помощью системы сборки makefile. Это позволит нашему приложению получить необходимые права доступа в скрытым API Андроид. Чтоб получить доступ к службе SurfaceFlinger приложение должно обладать правами “android.permission.ACCESS_SURFACE_FLINGER”. Эти права могут иметь только системные приложения (см. далее). Чтоб иметь право вызывать наше API с кодом 2020, приложение должно иметь правами “android.permission.HARDWARE_TEST”. Эти права также могут иметь только системные приложения. И что в конце концов сделать наше приложение системным, модифицируйте его манифест следующим образом:

image

Также создаете соответствующий makefile:

image

Остальные вещи в приложении (broadcast receiver загрузки, настройки, другое) достаточно стандартные и я не буду из касаться здесь. Осталась показать как сделать это приложение предустановленным (т.е. вшитым в прошивку). Просто разместите исходный код в каталоге {aosp}/packages/apps и измените файл core.mk так чтоб он включал наше приложение:

image

Финальная демонстрация:

Вы можете найти детальную информацию и исходный код на GitHub

Там есть приложение ScreenStabilization которое должно быть размещено в каталоге {aosp}/packages/apps, AOSP патч-файлы: 0001-ScreenStabilization-application-added.patch должен быть применен к каталогу {aosp}/build, 0001-Translate-methods-added.patch должен быть применен к каталогу {aosp}/frameworks/native.

Прошивка для Nexus 2013 Mobile собрана в конфигурации “ userdebug” так что она больше подходит для тестирования. Чтоб прошить прошивку загрузитесь в режим загружчика удерживая кнопку “volume down” и нажимая кнопку “power” одновременно. Дальше введите:

fastboot -w update aosp_deb_screen_stabilization.zip

Эта процедура удалит все существующие данные на Вашем устройстве. Имейте ввиду что для того чтоб прошить любую нестандартную прошивку Вы должны разблокировать загружчик командой:
fastboot oem unlock

Заключение


Эта статья показывает как реализовать простой алгоритм стабилизации экрана и применить его ко всему устройству путем модификации исходных кодов Андроид и сборки пользовательской прошивки. Алгоритм не идеальный но достаточный для целей демонстрации. Мы создали модифицированную прошивку для устройства Nexus 2013 Mobile, но наш исходный код может быть применен к любому Nexus устройству и даже к любой AOSP системе типа CyanogenMod что делает возможным интеграцию стабилизации экрана в новые устройства.

P.S. На самом деле я также являюсь и автором оригинальной англоязычной версии статьи, которая была опубликована на blog.lemberg.co.uk, так что могу ответить на технические вопросы.