Beispiel: Teekanne

Das Teapot-Beispiel befindet sich im Verzeichnis samples/Teapot/ im Stammverzeichnis der NDK-Installation. In diesem Beispiel wird die OpenGL-Bibliothek verwendet, um das legendäre Utah Teapot zu rendern. Insbesondere stellt er die Hilfsklasse ndk_helper vor, eine Sammlung nativer Hilfsfunktionen, die für die Implementierung von Spielen und ähnlichen Anwendungen als native Anwendungen erforderlich sind. Diese Klasse bietet:

  • Eine Abstraktionsschicht (GLContext), die bestimmte NDK-spezifische Verhaltensweisen verarbeitet.
  • Hilfsfunktionen, die nützlich sind, aber im NDK nicht vorhanden sind, z. B. die Tipperkennung.
  • Wrapper für JNI-Aufrufe für Plattformfunktionen wie das Laden von Texturen.

AndroidManifest.xml

Die Aktivitätsdeklaration hier ist nicht NativeActivity selbst, sondern eine abgeleitete Klasse davon: TeapotNativeActivity.

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

Letztendlich lautet der Name der gemeinsam genutzten Objektdatei, die vom Build-System erstellt wird, libTeapotNativeActivity.so. Das Build-System fügt das Präfix lib und die Erweiterung .so hinzu. Beide sind Teil des Werts, den das Manifest android:value ursprünglich zuweist.

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

Anwendung.Mk

Eine App, die die Framework-Klasse NativeActivity verwendet, darf kein Android API-Level unter 9 angeben, wodurch diese Klasse eingeführt wurde. Weitere Informationen zur Klasse NativeActivity finden Sie unter Native Aktivitäten und Anwendungen.

APP_PLATFORM := android-9

In der nächsten Zeile wird das Build-System angewiesen, für alle unterstützten Architekturen zu erstellen.

APP_ABI := all

Als Nächstes teilt die Datei dem Build-System mit, welche C++-Laufzeitunterstützungsbibliothek verwendet werden soll.

APP_STL := stlport_static

Java-seitige Implementierung

Die Datei TeapotNativeActivity befindet sich in teapots/classic-teapot/src/com/sample/teapot im NDK-Repository-Stammverzeichnis auf GitHub. Sie verarbeitet Lebenszyklusereignisse von Aktivitäten, erstellt mit der Funktion ShowUI() ein Pop-up-Fenster, um Text auf dem Bildschirm anzuzeigen, und aktualisiert die Framerate dynamisch mit der Funktion updateFPS(). Der folgende Code könnte für Sie interessant sein, da er die Activity App so vorbereitet, dass sie im Vollbildmodus und ohne Systemnavigationsleisten angezeigt wird, sodass der gesamte Bildschirm für die Anzeige gerenderter Teekannen-Frames verwendet werden kann:

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);
}

Implementierung auf nativer Seite

In diesem Abschnitt wird der Teil der Teapot-App beschrieben, der in C++ implementiert ist.

TeepotRenderer.h

Diese Funktionsaufrufe führen das eigentliche Rendering der Teekanne aus. Dabei wird ndk_helper für die Matrixberechnung und die Neupositionierung der Kamera basierend auf der Stelle verwendet, an der der Nutzer tippt.

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


ndk_helper::TapCamera* camera_;

TeapotNativeActivity.cpp

Die folgenden Zeilen enthalten ndk_helper in der nativen Quelldatei und definieren den Namen der Hilfsklasse.


#include "NDKHelper.h"

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

Die ndk_helper-Klasse wird zum ersten Mal für den EGL-bezogenen Lebenszyklus verwendet, wobei EGL-Kontextzustände (erstellt/verloren) mit Android-Lebenszyklusereignissen verknüpft werden. Mit der Klasse ndk_helper kann die Anwendung Kontextinformationen beibehalten, damit das System eine gelöschte Aktivität wiederherstellen kann. Das ist beispielsweise nützlich, wenn die Zielmaschine gedreht wird (wodurch eine Aktivität gelöscht und dann sofort in der neuen Ausrichtung wiederhergestellt wird) oder wenn der Sperrbildschirm angezeigt wird.

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

Als Nächstes ermöglicht ndk_helper die Touchbedienung.

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

Es bietet auch eine Kamerasteuerung (OpenGL-Ansichtswinkel).

ndk_helper::TapCamera tap_camera_;

Die App bereitet sich dann auf die Verwendung der Sensoren des Geräts vor. Dabei werden die nativen APIs des NDK verwendet.

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

Die App ruft die folgenden Funktionen als Reaktion auf verschiedene Android-Lebenszyklusereignisse und Änderungen des EGL-Kontextstatus auf. Dabei werden verschiedene Funktionen von ndk_helper über die Klasse Engine verwendet.


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

Anschließend ruft die folgende Funktion die Java-Seite zurück, um die UI-Anzeige zu aktualisieren.

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;
}

Als Nächstes ruft diese Funktion die Java-Seite auf, um ein Textfeld zu zeichnen, das den auf der nativen Seite gerenderten Bildschirm überlagert und die Anzahl der Frames anzeigt.

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;
}

Die Anwendung ruft die Systemuhr ab und stellt sie dem Renderer für zeitbasierte Animationen auf der Grundlage der Echtzeituhr bereit. Diese Informationen werden beispielsweise zur Berechnung des Impuls verwendet, wenn die Geschwindigkeit als Funktion der Zeit abnimmt.

renderer_.Update( monitor_.GetCurrentTime() );

Die Anwendung wandelt den gerenderten Frame jetzt zum Frontpuffer zur Anzeige über die GLcontext::Swap()-Funktion um. Außerdem verarbeitet sie mögliche Fehler, die beim Umblättern aufgetreten sind.

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

Das Programm übergibt Touch-Bewegungsereignisse an den in der ndk_helper-Klasse definierten Gestendetektor. Die Gestenerkennung erfasst Multitouch-Touch-Gesten, z. B. das Auseinander- und Zusammenziehen der Finger, und sendet eine Benachrichtigung, wenn sie durch eines dieser Ereignisse ausgelöst wird.

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;
}

Die Klasse ndk_helper bietet auch Zugriff auf eine Bibliothek für Vektoraufgaben (vecmath.h), die hier zum Transformieren von Berührungskoordinaten verwendet wird.

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 );
}

Die Methode HandleCmd() verarbeitet Befehle, die aus der Bibliothek „android_native_app_glue“ gepostet werden. Weitere Informationen zur Bedeutung der Nachrichten finden Sie in den Kommentaren in den Quelldateien android_native_app_glue.h und .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;
    }
}

Die Klasse ndk_helper postet APP_CMD_INIT_WINDOW, wenn android_app_glue einen onNativeWindowCreated()-Callback vom System erhält. Anwendungen können normalerweise Fensterinitialisierungen wie die EGL-Initialisierung durchführen. Sie tun dies außerhalb des Aktivitätslebenszyklus, da die Aktivität noch nicht bereit ist.

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

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