Próbka Teapot znajduje się w katalogu samples/Teapot/
, w katalogu głównym instalacji NDK. Ten przykład wykorzystuje bibliotekę OpenGL do renderowania kultowego czajnika z Utah. W szczególności prezentuje klasę pomocniczą ndk_helper
, czyli zbiór natywnych funkcji pomocniczych wymaganych do implementacji gier i podobnych aplikacji jak aplikacje natywne. W ramach tych zajęć omówimy:
- Warstwa abstrakcji,
GLContext
, która obsługuje określone zachowania specyficzne dla NDK. - funkcje pomocnicze, które są przydatne, ale nie ma ich w pakiecie NDK, np. wykrywanie kliknięć.
- Otoki JNI wywołują funkcje platformy, takie jak ładowanie tekstur.
AndroidManifest.xml
Deklaracja aktywności to nie sama NativeActivity
, ale jej podklasa: TeapotNativeActivity
.
<activity android:name="com.sample.teapot.TeapotNativeActivity" android:label="@string/app_name" android:configChanges="orientation|keyboardHidden">
Ostatecznie nazwa pliku współdzielonego obiektu, który skompilowa system kompilacji, to libTeapotNativeActivity.so
. System kompilacji dodaje prefiks lib
i rozszerzenie .so
. Żadna z tych wartości nie jest częścią wartości przypisanej pierwotnie do elementu android:value
w pliku manifestu.
<meta-data android:name="android.app.lib_name" android:value="TeapotNativeActivity" />
Plik Application.mk
Aplikacja używająca klasy platformy NativeActivity
nie może określać poziomu niższego niż 9 interfejsu Android API, który wprowadził tę klasę. Więcej informacji o klasie NativeActivity
znajdziesz w artykule o działaniach i aplikacjach natywnych.
APP_PLATFORM := android-9
Następny wiersz informuje system kompilacji pod kątem wszystkich obsługiwanych architektur.
APP_ABI := all
Następnie plik informuje system kompilacji, której biblioteki obsługującej środowisko wykonawcze C++ ma używać.
APP_STL := stlport_static
Implementacja po stronie Javy
PlikTeapotNativeActivity
znajduje się w lokalizacji teapots/classic-teapot/src/com/sample/teapot
, w katalogu głównym repozytorium NDK na GitHubie. Obsługuje zdarzenia cyklu życia aktywności, tworzy wyskakujące okienko, aby wyświetlać tekst na ekranie za pomocą funkcji ShowUI()
, oraz dynamicznie aktualizuje liczbę klatek za pomocą funkcji updateFPS()
. Ten kod może być interesujący, ponieważ przygotowuje aktywność w aplikacji do wyświetlania na pełnym ekranie, bez pasków nawigacyjnych systemu, bez pasków nawigacyjnych, dzięki czemu do wyświetlenia wyrenderowanych ramek czajnika można wykorzystać cały ekran:
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); }
Implementacja po stronie natywnej
W tej sekcji omawiamy część aplikacji Teapot zaimplementowaną w C++.
TeapotRenderer.h
Te wywołania funkcji wykonują rzeczywiste renderowanie czajnika. Wykorzystuje funkcję ndk_helper
do obliczania matrycy oraz zmiany położenia kamery w zależności od tego, gdzie użytkownik klika.
ndk_helper::Mat4 mat_projection_; ndk_helper::Mat4 mat_view_; ndk_helper::Mat4 mat_model_; ndk_helper::TapCamera* camera_;
TeapotNativeActivity.cpp
Poniższe wiersze zawierają ciąg ndk_helper
w natywnym pliku źródłowym i określają nazwę klasy pomocniczej.
#include "NDKHelper.h" //------------------------------------------------------------------------- //Preprocessor //------------------------------------------------------------------------- #define HELPER_CLASS_NAME "com/sample/helper/NDKHelper" //Class name of helper function
Pierwsze użycie klasy ndk_helper
to obsługa cyklu życia powiązanego z EGL i kojarzenie stanów kontekstu EGL (utworzonych/utraconych) ze zdarzeniami cyklu życia Androida. Klasa ndk_helper
umożliwia aplikacji zachowanie informacji kontekstowych, dzięki czemu system może przywrócić zniszczoną aktywność. Jest to przydatne na przykład w przypadku obrócenia maszyny docelowej (powodu jej zniszczenia i natychmiastowego przywrócenia jej w nowej orientacji) lub wyświetlenia ekranu blokady.
ndk_helper::GLContext* gl_context_; // handles EGL-related lifecycle.
Następnie ndk_helper
obsługuje sterowanie dotykowe.
ndk_helper::DoubletapDetector doubletap_detector_; ndk_helper::PinchDetector pinch_detector_; ndk_helper::DragDetector drag_detector_; ndk_helper::PerfMonitor monitor_;
Pozwala też sterować kamerą (funkcja widoku OpenGL).
ndk_helper::TapCamera tap_camera_;
Następnie aplikacja przygotowuje się do korzystania z czujników urządzenia, korzystając z natywnych interfejsów API wchodzących w skład pakietu NDK.
ASensorManager* sensor_manager_; const ASensor* accelerometer_sensor_; ASensorEventQueue* sensor_event_queue_;
Aplikacja wywołuje poniższe funkcje w odpowiedzi na różne zdarzenia cyklu życia Androida i zmiany stanu kontekstu EGL, korzystając z różnych funkcji zapewnianych przez ndk_helper
za pomocą klasy Engine
.
void LoadResources(); void UnloadResources(); void DrawFrame(); void TermDisplay(); void TrimMemory(); bool IsReady();
Następnie funkcja poniżej wywołuje interfejs Java, by zaktualizować wygląd interfejsu.
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; }
Następnie ta funkcja odwołuje się do strony Java, aby narysować pole tekstowe nałożone na ekran renderowany po stronie natywnej i wyświetlić liczbę klatek.
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; }
Aplikacja pobiera zegar systemowy i przesyła go do mechanizmu renderowania, by tworzyć animacje oparte na czasie rzeczywistym. Informacje te są używane na przykład do obliczania pędu, gdzie prędkość maleje jako funkcja czasu.
renderer_.Update( monitor_.GetCurrentTime() );
Aplikacja wyświetla teraz wyrenderowaną klatkę do przedniego bufora na potrzeby wyświetlania za pomocą funkcji GLcontext::Swap()
. Obsługuje też możliwe błędy, które wystąpiły podczas procesu odwracania.
if( EGL_SUCCESS != gl_context_->Swap() ) // swaps buffer.
Program przekazuje zdarzenia ruchu dotykowego do wykrywania gestów zdefiniowanego w klasie ndk_helper
. Wykrywanie gestów śledzi gesty wielodotyku, np. ściąganie palców i przeciąganie, a po ich wywołaniu wysyła powiadomienie.
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; }
Klasa ndk_helper
zapewnia też dostęp do biblioteki matematyki wektorowej (vecmath.h
), która może służyć do przekształcania współrzędnych dotyku.
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 ); }
Metoda HandleCmd()
obsługuje polecenia publikowane z biblioteki android_native_app_glue. Więcej informacji o znaczeniu komunikatów znajdziesz w komentarzach w plikach źródłowych android_native_app_glue.h
i .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; } }
Zajęcia ndk_helper
publikuje APP_CMD_INIT_WINDOW
, gdy android_app_glue
otrzyma od systemu wywołanie zwrotne onNativeWindowCreated()
.
Aplikacje mogą zwykle inicjować okna, np. EGL. Robią to poza cyklem życia aktywności, ponieważ aktywność nie jest jeszcze gotowa.
//Init helper functions ndk_helper::JNIHelper::Init( state->activity, HELPER_CLASS_NAME ); state->userData = &g_engine; state->onAppCmd = Engine::HandleCmd; state->onInputEvent = Engine::HandleInput;