Пример: заварочный чайник

Пример Teapot находится в каталоге samples/Teapot/ корневого каталога установки NDK. В этом примере используется библиотека OpenGL для рендеринга знаменитого чайника из Юты. В частности, в нем демонстрируется вспомогательный класс ndk_helper – набор нативных вспомогательных функций, необходимых для реализации игр и аналогичных приложений в виде нативных приложений. Этот класс предоставляет:

  • Уровень абстракции GLContext, который обрабатывает определенные действия, связанные с NDK.
  • Вспомогательные функции, которые могут быть полезны, но отсутствуют в NDK, например распознавание нажатий.
  • Оболочки для вызовов JNI для функций платформы, таких как загрузка текстур.

AndroidManifest.xml

Объявление объекта activity здесь – это не сам класс NativeActivity, а его подкласс TeapotNativeActivity.

<activity android:name="com.sample.teapot.TeapotNativeActivity"
        android:label="@string/app_name"
        android:configChanges="orientation|keyboardHidden">

В итоге имя файла общего объекта, созданного системой сборки, будет libTeapotNativeActivity.so. Система сборки добавляет префикс lib и расширение .so. Ни один из этих элементов не является частью значения, которое манифест изначально присваивает android:value.

<meta-data android:name="android.app.lib_name"
        android:value="TeapotNativeActivity" />

Application.mk

Приложение, использующее класс фреймворка NativeActivity, должно иметь уровень API Android не ниже 9, в котором был представлен этот класс. Дополнительную информацию о классе NativeActivity можно найти в статье Нативные действия и приложения.

APP_PLATFORM := android-9

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

APP_ABI := all

Затем файл сообщает системе сборки, какую библиотеку поддержки среды выполнения C++ использовать.

APP_STL := stlport_static

Реализация на стороне Java

Файл TeapotNativeActivity находится в каталоге teapots/classic-teapot/src/com/sample/teapot, который расположен в корневом каталоге репозитория NDK на GitHub. Оно обрабатывает события жизненного цикла объекта activity, создает всплывающее окно для показа текста на экране с помощью функции ShowUI() и динамически обновляет частоту кадров с помощью функции updateFPS(). Следующий код может быть вам интересен, поскольку он подготавливает Activity приложения к полноэкранному режиму с эффектом погружения и без системных панелей навигации, чтобы весь экран можно было использовать для отображения визуализированных кадров чайника:

Kotlin

fun setImmersiveSticky() {
    window.decorView.systemUiVisibility = (
            View.SYSTEM_UI_FLAG_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                    or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            )
}

Java

void setImmersiveSticky() {
    View decorView = getWindow().getDecorView();
    decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN
            | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}

Реализация на стороне нативного приложения

В этом разделе рассматривается часть приложения Teapot, реализованная на C++.

TeapotRenderer.h

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

ndk_helper::Mat4 mat_projection_;
ndk_helper::Mat4 mat_view_;
ndk_helper::Mat4 mat_model_;


ndk_helper::TapCamera* camera_;

TeapotNativeActivity.cpp

В следующих строках исходного файла нативного объявления содержится ndk_helper и определяется название вспомогательного класса.

#include "NDKHelper.h"

//-------------------------------------------------------------------------
//Preprocessor
//-------------------------------------------------------------------------
#define HELPER_CLASS_NAME "com/sample/helper/NDKHelper" //Class name of helper
function

Класс ndk_helper используется для управления жизненным циклом EGL, связывая состояния контекста EGL (создано/потеряно) с событиями жизненного цикла Android. Класс ndk_helper позволяет приложению сохранять информацию о контексте, чтобы система могла восстановить уничтоженное действие. Это полезно, например, когда целевое устройство поворачивается (в результате чего действие уничтожается, а затем немедленно восстанавливается в новой ориентации) или когда появляется заблокированный экран.

ndk_helper::GLContext* gl_context_; // handles EGL-related lifecycle.

Далее, ndk_helper обеспечивает сенсорное управление.

ndk_helper::DoubletapDetector doubletap_detector_;
ndk_helper::PinchDetector pinch_detector_;
ndk_helper::DragDetector drag_detector_;
ndk_helper::PerfMonitor monitor_;

и управление камерой (пирамида видимости OpenGL).

ndk_helper::TapCamera tap_camera_;

Затем приложение готовится к использованию датчиков устройства, используя нативные API, предоставляемые в NDK.

ASensorManager* sensor_manager_;
const ASensor* accelerometer_sensor_;
ASensorEventQueue* sensor_event_queue_;

Приложение вызывает следующие функции в ответ на различные события жизненного цикла Android и изменения состояния контекста EGL, используя различные функции, предоставляемые ndk_helper через класс Engine.

void LoadResources();
void UnloadResources();
void DrawFrame();
void TermDisplay();
void TrimMemory();
bool IsReady();

Затем следующая функция выполняет обратный вызов Java, чтобы обновить интерфейс.

void Engine::ShowUI()
{
    JNIEnv *jni;
    app_->activity->vm->AttachCurrentThread( &jni, NULL );


    //Default class retrieval
    jclass clazz = jni->GetObjectClass( app_->activity->clazz );
    jmethodID methodID = jni->GetMethodID( clazz, "showUI", "()V" );
    jni->CallVoidMethod( app_->activity->clazz, methodID );


    app_->activity->vm->DetachCurrentThread();
    return;
}

Затем эта функция вызывает обратный вызов на стороне Java, чтобы нарисовать текстовое поле, наложенное на экран, отображаемый на нативной стороне, и показывающее количество кадров.

void Engine::UpdateFPS( float fFPS )
{
    JNIEnv *jni;
    app_->activity->vm->AttachCurrentThread( &jni, NULL );


    //Default class retrieval
    jclass clazz = jni->GetObjectClass( app_->activity->clazz );
    jmethodID methodID = jni->GetMethodID( clazz, "updateFPS", "(F)V" );
    jni->CallVoidMethod( app_->activity->clazz, methodID, fFPS );


    app_->activity->vm->DetachCurrentThread();
    return;
}

Приложение получает системное время и передает его в модуль визуализации для анимации на основе времени. Эта информация используется, например, для расчета импульса, когда скорость уменьшается в зависимости от времени.

renderer_.Update( monitor_.GetCurrentTime() );

Теперь приложение переносит отрисованный кадр в передний буфер для отображения с помощью функции GLcontext::Swap(), а также обрабатывает возможные ошибки, возникшие в процессе переноса.

if( EGL_SUCCESS != gl_context_->Swap() )  // swaps
buffer.

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

if( AInputEvent_getType( event ) == AINPUT_EVENT_TYPE_MOTION )
{
    ndk_helper::GESTURE_STATE doubleTapState =
        eng->doubletap_detector_.Detect( event );
    ndk_helper::GESTURE_STATE dragState = eng->drag_detector_.Detect( event );
    ndk_helper::GESTURE_STATE pinchState = eng->pinch_detector_.Detect( event );

    //Double tap detector has a priority over other detectors
    if( doubleTapState == ndk_helper::GESTURE_STATE_ACTION )
    {
        //Detect double tap
        eng->tap_camera_.Reset( true );
    }
    else
    {
        //Handle drag state
        if( dragState & ndk_helper::GESTURE_STATE_START )
        {
             //Otherwise, start dragging
             ndk_helper::Vec2 v;
             eng->drag_detector_.GetPointer( v );
             eng->TransformPosition( v );
             eng->tap_camera_.BeginDrag( v );
        }
        // ...else other possible drag states...

        //Handle pinch state
        if( pinchState & ndk_helper::GESTURE_STATE_START )
        {
            //Start new pinch
            ndk_helper::Vec2 v1;
            ndk_helper::Vec2 v2;
            eng->pinch_detector_.GetPointers( v1, v2 );
            eng->TransformPosition( v1 );
            eng->TransformPosition( v2 );
            eng->tap_camera_.BeginPinch( v1, v2 );
        }
        // ...else other possible pinch states...
    }
    return 1;
}

Класс ndk_helper также предоставляет доступ к библиотеке векторной математики (vecmath.h), которая используется здесь для преобразования координат касания.

void Engine::TransformPosition( ndk_helper::Vec2& vec )
{
    vec = ndk_helper::Vec2( 2.0f, 2.0f ) * vec
            / ndk_helper::Vec2( gl_context_->GetScreenWidth(),
            gl_context_->GetScreenHeight() ) - ndk_helper::Vec2( 1.f, 1.f );
}

Метод HandleCmd() обрабатывает команды, отправленные из библиотеки android_native_app_glue. Дополнительную информацию о значении сообщений можно найти в комментариях к исходным файлам android_native_app_glue.h и .c.

void Engine::HandleCmd( struct android_app* app,
        int32_t cmd )
{
    Engine* eng = (Engine*) app->userData;
    switch( cmd )
    {
    case APP_CMD_SAVE_STATE:
        break;
    case APP_CMD_INIT_WINDOW:
        // The window is being shown, get it ready.
        if( app->window != NULL )
        {
            eng->InitDisplay();
            eng->DrawFrame();
        }
        break;
    case APP_CMD_TERM_WINDOW:
        // The window is being hidden or closed, clean it up.
        eng->TermDisplay();
        eng->has_focus_ = false;
        break;
    case APP_CMD_STOP:
        break;
    case APP_CMD_GAINED_FOCUS:
        eng->ResumeSensors();
        //Start animation
        eng->has_focus_ = true;
        break;
    case APP_CMD_LOST_FOCUS:
        eng->SuspendSensors();
        // Also stop animating.
        eng->has_focus_ = false;
        eng->DrawFrame();
        break;
    case APP_CMD_LOW_MEMORY:
        //Free up GL resources
        eng->TrimMemory();
        break;
    }
}

Класс ndk_helper публикует APP_CMD_INIT_WINDOW, когда android_app_glue получает обратный вызов onNativeWindowCreated() от системы. Приложения обычно могут выполнять инициализацию окон, например инициализацию EGL. Это происходит вне жизненного цикла объекта activity, поскольку он ещё не готов.

//Init helper functions
ndk_helper::JNIHelper::Init( state->activity, HELPER_CLASS_NAME );

state->userData = &g_engine;
state->onAppCmd = Engine::HandleCmd;
state->onInputEvent = Engine::HandleInput;