Przykład: Imbryk

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

Plik TeapotNativeActivity 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;