Пример 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;